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. It doesn’t have
    to be REST
    WebSockets in Go
    GOPHERFEST EDITION

    View Slide

  2. REST
    GET
    DELETE
    POST
    PUT
    Resources
    ?

    View Slide

  3. View Slide

  4. Client-Server

    View Slide

  5. Client-Server
    Stateless

    View Slide

  6. Client-Server
    Stateless
    Cache

    View Slide

  7. Client-Server
    Stateless
    Cache
    Uniform Interface

    View Slide

  8. Client-Server
    Stateless
    Cache
    Uniform Interface
    Layered System

    View Slide

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

    View Slide

  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

    View Slide

  11. WebSockets

    View Slide

  12. Chats Games

    View Slide

  13. View Slide

  14. View Slide

  15. View Slide

  16. Server
    ws://…
    Client
    Messages
    Text/Binary

    View Slide

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

    View Slide

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

    View Slide

  19. https://godoc.org/?q=websocket

    View Slide

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

    View Slide

  21. View Slide

  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)
    }
    }

    View Slide

  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)
    }
    }

    View Slide

  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)
    // …

    View Slide

  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)
    // …

    View Slide

  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
    }
    }
    }

    View Slide

  27. This is a solved problem!

    View Slide

  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

    View Slide

  29. Backend Clients
    API
    Database
    HTTP

    View Slide

  30. Backend Clients
    API
    Database
    HTTP
    Polling

    View Slide

  31. Push
    Backend Clients
    API
    Database
    WebSocket
    Bus

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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))
    }

    View Slide

  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))
    }

    View Slide

  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
    }

    View Slide

  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

    View Slide

  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
    }

    View Slide

  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
    }

    View Slide

  42. Is it safe to send passwords in
    clear text over WebSockets?

    View Slide

  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.

    View Slide

  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

    View Slide

  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)
    }

    View Slide

  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)
    }

    View Slide

  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

    View Slide

  48. Client
    Server Server Server
    Load Balancer Load Balancer
    Client Client Client Client Client Client Client
    Server Server
    Message Bus
    DNS
    Server

    View Slide

  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)

    View Slide

  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

    View Slide

  51. Thank you.
    @konradreiche

    View Slide

  52. Questions?
    @konradreiche

    View Slide