It doesn't have to be REST: WebSockets in Go

It doesn't have to be REST: WebSockets in Go

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. There are different WebSocket implementation in the Go ecosystem. This talk will give you headspace to consider WebSockets as an alternative communication protocol for your application by giving an overview of the current state of the art of WebSockets in Go.

661bb4dee881617e676c4d954ce97a70?s=128

Konrad Reiche

August 18, 2017
Tweet

Transcript

  1. 3.
  2. 4.

    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
  3. 6.

    REST (Representational State Transfer) - Defined by Roy T. Fielding

    in his dissertation: “Architectural Styles and the Design of Network-based Software Architectures” - Fielding: there are two perspectives on the process of architectural design - Build the architecture from familiar components until needs are satisfied - Start with system needs; incrementally add constraints when needed - First one emphasizes creativity and unbound vision - Second one emphasizes restraint and understanding the system context
  4. 7.
  5. 16.
  6. 17.
  7. 18.
  8. 19.

    GET ws://localhost:4000/ HTTP/1.1 Origin: http://localhost:4000 Host: localhost:4000 Connection: Upgrade Upgrade:

    websocket Sec-WebSocket-Key: zy6Dy9mSAIM7GJZNf9rI1A== Sec-WebSocket-Version: 13 Open Handshake Request
  9. 22.

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

    are split into frames - Payload can be binary or text - Supports fragmentation
  10. 23.

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

    are split into frames - Payload can be binary or text - Supports fragmentation
  11. 24.

    package main import ( "net/http" ) func main() { err

    := http.ListenAndServe(":4000", nil) if err != nil { panic(err) } }
  12. 25.

    package main import ( "net/http" "golang.org/x/net/websocket" ) func main() {

    http.Handle("/websocket", websocket.Handler(nil)) err := http.ListenAndServe(":4000", nil) if err != nil { panic(err) } }
  13. 26.

    package main import ( "net/http" "golang.org/x/net/websocket" ) func Handle(ws *websocket.Conn)

    { // TODO } func main() { http.Handle("/websocket", websocket.Handler(Handle)) err := http.ListenAndServe(":4000", nil) if err != nil { panic(err) } }
  14. 27.

    package main import ( "net/http" "golang.org/x/net/websocket" ) func Handle(ws *websocket.Conn)

    { // TODO } func main() { http.Handle("/websocket", websocket.Handler(Handle)) err := http.ListenAndServe(":4000", nil) if err != nil { panic(err) } } io.Reader io.Writer
  15. 28.

    package main import ( "net/http" "golang.org/x/net/websocket" ) func Handle(ws *websocket.Conn)

    { ws.Write([]byte("I am alive")) } func main() { http.Handle("/websocket", websocket.Handler(Handle)) err := http.ListenAndServe(":4000", nil) if err != nil { panic(err) } }
  16. 29.

    package main import ( "net/http" "golang.org/x/net/websocket" ) func main() {

    origin := "http://localhost/" _, err := websocket.Dial("ws://localhost:4000/websocket", "", origin) if err != nil { panic(err) } }
  17. 30.

    package main import ( "net/http" "golang.org/x/net/websocket" ) func main() {

    origin := "http://localhost/" ws, err := websocket.Dial("ws://localhost:4000/websocket", "", origin) if err != nil { panic(err) } _, err := ws.Write([]byte("Hello? Anybody here?")) if err != nil { panic(err) } }
  18. 31.

    package main import ( "net/http" "golang.org/x/net/websocket" ) func main() {

    origin := "http://localhost/" ws, err := websocket.Dial("ws://localhost:4000/websocket", "", origin) if err != nil { panic(err) } _, err := ws.Write([]byte("Hello? Anybody here?")) if err != nil { panic(err) } msg := make([]byte, 512) _, err = ws.Read(msg) // …
  19. 34.
  20. 35.

    github.com/gorilla/websocket golang.org/x/net RFC 6455 Features Passes Autobahn Test Suite Yes

    No Receive fragmented message Yes No Send close message Yes No Send pings and receive pongs Yes No Get the type of a received data message Yes Yes Other Features Read message using io.Reader Yes No? Write message using io.WriterCloser Yes No?
  21. 36.
  22. 41.

    package main import ( "net/http" "golang.org/x/net/websocket" ) func main() {

    origin := "http://localhost/" ws, err := websocket.Dial("ws://localhost:4000/websocket", "", origin) if err != nil { panic(err) } _, err := ws.Write([]byte("Hello?")) if err != nil { panic(err) } msg := make([]byte, 512) _, err = ws.Read(msg) // …
  23. 43.

    Frame 1 Frame 2 Frame 3 Frame 1 Message 1

    Frame 2 Message 2 Read() / Write() golang.org/x/net/websocket
  24. 44.

    Frame 1 Frame 2 Frame 3 Frame 1 Message 1

    Frame 2 Message 2 Read() / Write() Read() / Write() golang.org/x/net/websocket
  25. 45.

    Frame 1 Frame 2 Frame 3 Frame 1 Message 1

    Frame 2 Message 2 Read() / Write() Read() / Write() Read() / Write() golang.org/x/net/websocket
  26. 46.

    Frame 1 Frame 2 Frame 3 Frame 1 Message 1

    Frame 2 Message 2 github.com/gorilla/websocket Read() / Write() Read() / Write() Read() / Write() Read() / Write() Read() / Write() golang.org/x/net/websocket
  27. 47.

    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) } }
  28. 48.

    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) } }
  29. 49.

    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) // …
  30. 50.

    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) // …
  31. 51.

    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 } } }
  32. 52.
  33. 53.
  34. 54.

    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)
  35. 62.

    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)) }
  36. 63.

    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)) }
  37. 64.

    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 }
  38. 65.

    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
  39. 66.

    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 }
  40. 67.

    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 }
  41. 68.

    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 - Bad compatibility to the outside world