DEV Community

Kazuki Higashiguchi
Kazuki Higashiguchi

Posted on • Updated on

Learn WebSocket handshake protocol with gorilla/websocket server

Let's learn the WebSocket protocol with a simple implementation of echo WebSocket server and client in Go.

WebSocket

WebSocket is a mechanism for low-cost, full-duplex communication on Web, which protocol was standardized as RFC 6455.

The following diagram, quoted by Wikipedia, describe a communication using WebSocket between client and server.

A diagram describing a connection using WebSocket

Let's see how to implement the above design, especially handshaking, using the code which works.

WebSocket in Go

WebSocket server and client can be implemented in various programming languages, but in this article, I use Go language because it is simple and easy to read.

In Go project, gorilla/websocket is widely used to implement WebSocket, and many samples using this library are available on the internet.

I'll explain what gorilla/websocket does in its internal implementation while reading RFC 6455.

WebSocket server

First, let's learn the WebSocket protocol with server implementation. The sample code is available on GitHub repository. I'll explain it line by line.

package main

import (
    "fmt"
    "log"
    "net/http"

    "github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{}

func main() {
    p := 12345
    log.Printf("Starting websocket echo server on port %d", p)

    http.HandleFunc("/", echo)
    if err := http.ListenAndServe(fmt.Sprintf(":%d", p), nil); err != nil {
        log.Panicf("Error while starting to listen: %#v", err)
    }
}

func echo(w http.ResponseWriter, r *http.Request) {
    // Start handshaking
    c, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Printf("Upgrading error: %#v\n", err)
        return
    }
    defer c.Close()
    // End

    for {
        mt, message, err := c.ReadMessage()
        if err != nil {
            log.Printf("Reading error: %#v\n", err)
            break
        }
        log.Printf("recv: message %q", message)
        if err := c.WriteMessage(mt, message); err != nil {
            log.Printf("Writing error: %#v\n", err)
            break
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This article focuses on the handshake on the WebSocket protocol from WebSocket server perspective.

"Upgrade" an HTTP to a WebSocket

Here is a first line, excluding the import statement.

var upgrader = websocket.Upgrader{}
Enter fullscreen mode Exit fullscreen mode

Upgrader struct specifies parameters for upgrading an HTTP connection to a WebSocket connection. The naming itself, "Upgrader", is also meaningful.

WebSocket is designed to work over HTTP. To achieve compatibility with HTTP, the WebSocket handshake uses the HTTP Upgrade header in order to change from the HTTP protocol to the WebSocket protocol.

The Upgrade header can be used to upgrade an already established client/server connection to a different protocol. To use Upgrade header, Connection: update must be set.

RFC 6455 - 1.2 Protocol Overview illustrates an example of WebSocket handshake.

From client:

    GET /chat HTTP/1.1
    Host: server.example.com
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
    Origin: http://example.com
    Sec-WebSocket-Protocol: chat, superchat
    Sec-WebSocket-Version: 13
Enter fullscreen mode Exit fullscreen mode

Then server will respond as follow:

    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
    Sec-WebSocket-Protocol: chat
Enter fullscreen mode Exit fullscreen mode

Fields of Upgrader struct is as follow:

type Upgrader struct {
    HandshakeTimeout time.Duration
    ReadBufferSize, WriteBufferSize int
    Subprotocols []string
    Error func(w http.ResponseWriter, r *http.Request, status int, reason error)
    CheckOrigin func(r *http.Request) bool
    EnableCompression bool
}
Enter fullscreen mode Exit fullscreen mode

Important fields to understand WebSocket protocol are HandshakeTimeout and Subprotocols.

Handshake

HandshakeTimeout specifies the duration for the handshake to complete.

A diagram describing a connection using WebSocket

The first step to exchange message bidirectionally is handshake where opens a new connection between client and server.

Subprotocols

Subprotocols is like a custom XML schema or doctype declaration.

    Sec-WebSocket-Protocol: soap, wamp
Enter fullscreen mode Exit fullscreen mode

For example, if you're using a subprotocol json, all data is passed as JSON (of course, you needs extra code on the server to parse JSON data).

As a WebSocket server, it specifies the server's supported protocols. Therefore, when the server specifies supported protocol is only soap, server will reject client's handshake with other subprotocols such as json.

Wait handshake from client

Here is a http handler which waits HTTP requests to URI path / on port 12345.

func main() {
    http.HandleFunc("/", echo)
    if err := http.ListenAndServe(fmt.Sprintf(":%d", 12345), nil); err != nil {
        log.Panicf("Error while starting to listen: %#v", err)
    }
}

func echo(w http.ResponseWriter, r *http.Request) {
    c, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Printf("Upgrading error: %#v\n", err)
        return
    }
    defer c.Close()

    // ...(omit)
}
Enter fullscreen mode Exit fullscreen mode

echo function first performs WebSocket handshake procedure in response to a request from client. The detailed implementation is in Upgrader.Upgrade function.

c, err := upgrader.Upgrade(w, r, nil)
Enter fullscreen mode Exit fullscreen mode

Upgrade upgrades the HTTP server connection to the WebSocket protocol.

Handshake request validation

Upgrade function begins with gorilla/websocket codes which validate client HTTP request.

    if !tokenListContainsValue(r.Header, "Connection", "upgrade") {
        return u.returnError(w, r, http.StatusBadRequest, badHandshake+"'upgrade' token not found in 'Connection' header")
    }

    if !tokenListContainsValue(r.Header, "Upgrade", "websocket") {
        return u.returnError(w, r, http.StatusBadRequest, badHandshake+"'websocket' token not found in 'Upgrade' header")
    }

    if r.Method != "GET" {
        return u.returnError(w, r, http.StatusMethodNotAllowed, badHandshake+"request method is not GET")
    }

    if !tokenListContainsValue(r.Header, "Sec-Websocket-Version", "13") {
        return u.returnError(w, r, http.StatusBadRequest, "websocket: unsupported version: 13 not found in 'Sec-Websocket-Version' header")
    }
Enter fullscreen mode Exit fullscreen mode

The specification of handshake is as follow:

  • The request MUST contain a Connection header field whose value MUST include the upgrade token.
    • A Connection header is often set to keep-alive or close in normal HTTP communication.
  • The request MUST contain a Upgrade header field whose value MUST include the websocket keyword.
    • A Upgrade header is used to upgrade an already established client/server connection to a different protocol.
  • The method of the request MUST be GET as described on RFC 6455 - page 16
  • Sec-Websocket-Version is the WebSocket protocol version which is supported by server. Available versions are listed in IANA WebSocket Version Number Registry. RFC 6415 is registered as Version 13, so basically, only 13 should be supported.

Sec-Websocket-Key

There is also gorilla/websocket codes on the way in Upgrade function.

challengeKey := r.Header.Get("Sec-Websocket-Key")
if challengeKey == "" {
    return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: 'Sec-WebSocket-Key' header is missing or blank")
}
Enter fullscreen mode Exit fullscreen mode

Sec-Websocket-Key is used mainly for two purposes according to RFC 6455 and the discussion on stack overflow.

  1. Ensure that server understands WebSocket protocol
  2. Prevent clients accidentally requesting WebSocket upgrade not expecting it

See How decided a value set in Sec-WebSocket-Key/Accept header for more information.

Return HTTP status code 101 (Switching Protocols)

Upgrade function ends with gorilla/websocket codes which write HTTP headers.

p = append(p, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "...)
p = append(p, computeAcceptKey(challengeKey)...)
p = append(p, "\r\n"...)
Enter fullscreen mode Exit fullscreen mode

The HTTP 101 Switching Protocols response code indicates the protocol the server is switching to as requested by a client. This code includes in this response an Upgrade response header field whose value is websocket.

When the client receives this response, it understands that the communication protocol has been upgraded to WebSocket, and the first step, handshake, is completed.

Conclusion

This article explains WebSocket handshake protocol using gorilla/websocket server implementation.

Additional articles will be published on the WebSocket protocol from the client perspective and data frame processing when exchanging messages and etc.

Oldest comments (0)