It doesn't have to be REST: WebSockets in Go (Gopherfest Edition)

661bb4dee881617e676c4d954ce97a70?s=47 Konrad Reiche
November 10, 2017

It doesn't have to be REST: WebSockets in Go (Gopherfest Edition)

The speed of Go should make us reconsider well established paradigms. When developing APIs it is practically impossible not to mention REST yet many applications would benefit from real-time interactivity. This talk will give you headspace to consider WebSockets as an alternative communication protocol for your application over commercial messaging and data synchronization services, how to manage sessions and answer why it is so hard to scale a WebSocket architecture.

661bb4dee881617e676c4d954ce97a70?s=128

Konrad Reiche

November 10, 2017
Tweet

Transcript

  1. It doesn’t have to be REST WebSockets in Go GOPHERFEST

    EDITION
  2. REST GET DELETE POST PUT Resources ?

  3. None
  4. Client-Server

  5. Client-Server Stateless

  6. Client-Server Stateless Cache

  7. Client-Server Stateless Cache Uniform Interface

  8. Client-Server Stateless Cache Uniform Interface Layered System

  9. Client-Server Stateless Cache Uniform Interface Layered System Code on Demand

  10. HTTP 0.9 HTTP 1.0 HTTP 1.1 HTTP/2 GraphQL WebSocket 1991

    1996 1999 2000 2005 2008 2013 2015 2016 2018 REST React High React Adoption Towards Real-Time Rendering & Updating AJAX Expected Real-Time Singularity
  11. WebSockets

  12. Chats Games

  13. None
  14. None
  15. None
  16. Server ws://… Client Messages Text/Binary

  17. Server ws://… Client Messages Text/Binary - Message-oriented protocol - Messages

    are split into frames - Payload can be binary or text - Supports fragmentation
  18. Server ws://… Client Messages Text/Binary - Message-oriented protocol - Messages

    are split into frames - Payload can be binary or text - Supports fragmentation
  19. https://godoc.org/?q=websocket

  20. github.com/gorilla/websocket 3958 stars, 2393 imports golang.org/x/net/websocket 1412 imports

  21. None
  22. package main import ( "log" "net/http" ) func handleUpgrade(w http.ResponseWriter,

    r *http.Request) { // TODO } func main() { http.HandleFunc("/websocket", handleUpgrade) err := http.ListenAndServe(":4000", nil) if err != nil { panic(err) } }
  23. package main import ( "log" "net/http" ) var upgrader =

    websocket.Upgrader{} func handleUpgrade(w http.ResponseWriter, r *http.Request) { // TODO } func main() { http.HandleFunc("/websocket", handleUpgrade) err := http.ListenAndServe(":4000", nil) if err != nil { panic(err) } }
  24. package main import ( "log" "net/http" ) var upgrader =

    websocket.Upgrader{} func handleUpgrade(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { panic(err) } defer conn.Close() // TODO } func main() { http.HandleFunc("/websocket", handleUpgrade) // …
  25. package main import ( "log" "net/http" ) var upgrader =

    websocket.Upgrader{} func handleUpgrade(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { panic(err) } defer conn.Close() handleConnection(conn) } func main() { http.HandleFunc("/websocket", handleUpgrade) // …
  26. func handleConnection(conn *websocket.Conn) { for { messageType, message, err :=

    c.ReadMessage() if err != nil { break } log.Println(message) err = c.WriteMessage(messageType, message) if err != nil { break } } }
  27. This is a solved problem!

  28. Parse, Firebase, Pusher - Data synchronization services offer an off-the-shelf

    software solution - Trade the cost of implementation against the cost of learning their API - Force an opinionated structure on your data - Limited querying capabilities - They manage sessions for you but you still need to integrate them - Upside: offers scalability out of the box
  29. Backend Clients API Database HTTP

  30. Backend Clients API Database HTTP Polling

  31. Push Backend Clients API Database WebSocket Bus

  32. Backend Clients API Database WebSocket Bus Subscribe Publish State change

    Push
  33. Backend Clients API Database WebSocket Bus Subscribe Publish State change

    Push
  34. Backend Clients API Database WebSocket {"cmd”:"listUsers"} Push

  35. Backend Clients API Database WebSocket "CONNECT" "LIST_USERS" Push

  36. type MessageEndpoint func(ctx context.Context, m Message, w *Writer) error type

    Router struct { endpoints map[string]MessageEndpoint writer map[string]*Writer } func (r *Router) HandleConnection(conn *websocket.Conn) { w := r.NewWriter(conn) for { _, message, err := conn.ReadMessage() if err != nil { break } request := string(message) r.writer[w.ID] = w r.Route(context.Background(), w, request) } delete(r.writer, r.ID(conn)) }
  37. type MessageEndpoint func(ctx context.Context, m Message, w *Writer) error type

    Router struct { endpoints map[string]MessageEndpoint writer map[string]*Writer } func (r *Router) HandleConnection(conn *websocket.Conn) { w := r.NewWriter(conn) for { _, message, err := conn.ReadMessage() if err != nil { break } request := string(message) r.writer[w.ID] = w go r.Route(context.Background(), w, request) } delete(r.writer, r.ID(conn)) }
  38. type Writer struct { ID int conn *websocket.Conn } func

    (w *Writer) Write(data []byte) (int, error) { err := w.conn.WriteMessage(websocket.TextMessage, data) if err != nil { return 0, err } return len(data), nil }
  39. type Writer struct { ID int conn *websocket.Conn } func

    (w *Writer) Write(data []byte) (int, error) { err := w.conn.WriteMessage(websocket.TextMessage, data) if err != nil { return 0, err } return len(data), nil } panic: concurrent write to websocket connection
  40. type Writer struct { ID int conn *websocket.Conn } func

    (w *Writer) Write(data []byte) (int, error) { err := w.conn.WriteMessage(websocket.TextMessage, data) if err != nil { return 0, err } return len(data), nil }
  41. type Writer struct { ID int mu sync.RWMutex conn *websocket.Conn

    } func (w *Writer) Write(data []byte) (int, error) { w.mu.Lock() defer w.mu.Unlock() err := w.conn.WriteMessage(websocket.TextMessage, data) if err != nil { return 0, err } return len(data), nil }
  42. Is it safe to send passwords in clear text over

    WebSockets?
  43. Security - There are two modes: unencrypted ws:// and encrypted

    wss:// - Encrypted mode uses TLS/SSL encryption to encrypt all data sent - This includes the initial hand-shake What about sessions? - WebSockets are stateful: we can and should make use of this.
  44. Sessions - Naive approach: be stateless, send a session ID

    with every request - Alternative: make use of the persistent connection - Authenticate once through query parameter when connecting wss://example.com/api?sessionID=token - Protects much better against CSRF attacks
  45. package main import ( "log" "net/http" ) var upgrader =

    websocket.Upgrader{} func handleUpgrade(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { panic(err) } defer conn.Close() handleConnection(conn) }
  46. package main import ( "log" "net/http" ) var upgrader =

    websocket.Upgrader{} func handleUpgrade(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { panic(err) } sessionID := req.URL.Query().Get("sessionID") defer conn.Close() handleConnection(conn, sessionID) }
  47. Scaling WebSocket Architectures When Client 1 persists data through Server

    1 how will Client 2 learn about this who is connected to Server 2? - Publish/Subscribe Broker to implement message bus - This also takes care of handling failover - Load balancers like AWS ELB support Proxy Protocol for TCP mode - There is a hard limit of 65,535 ports per IP address - Combination of DNS and software load balancer
  48. Client Server Server Server Load Balancer Load Balancer Client Client

    Client Client Client Client Client Server Server Message Bus DNS Server
  49. https://github.com/gobwas/ws - Efficient low-level WebSocket library - Allow users to

    reuse I/O buffers between connections - Export efficient low-level interface for working with the protocol Zero-Copy Upgrade - Skips the need to use HTTP for upgrading - ws.Upgrade() accepts io.ReadWriter (net.Conn)
  50. Summary - REST can be implemented using WebSockets - WebSockets

    allow us to implement a push architecture - Freedom in designing a sub-protocol tailored to your system’s needs - Also useful for connecting backend components - Gorilla WebSocket is a great implementation of RFC 6455 - Rolling your own implementation can be costly - Scaling WebSockets is nontrivial - Bad compatibility to the outside world
  51. Thank you. @konradreiche

  52. Questions? @konradreiche