Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

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.

Konrad Reiche

November 10, 2017
Tweet

More Decks by Konrad Reiche

Other Decks in Technology

Transcript

  1. 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
  2. Server ws://… Client Messages Text/Binary - Message-oriented protocol - Messages

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

    are split into frames - Payload can be binary or text - Supports fragmentation
  4. 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) } }
  5. 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) } }
  6. 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) // …
  7. 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) // …
  8. 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 } } }
  9. 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
  10. 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)) }
  11. 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)) }
  12. 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 }
  13. 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
  14. 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 }
  15. 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 }
  16. 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.
  17. 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
  18. 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) }
  19. 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) }
  20. 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
  21. Client Server Server Server Load Balancer Load Balancer Client Client

    Client Client Client Client Client Server Server Message Bus DNS Server
  22. 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)
  23. 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