Learn WebSocket handshake protocol with gorilla/websocket server
Tags: go, websocket
Published: 2021-12-09 10:40:09

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](https://en.wikipedia.org/wiki/Duplex_(telecommunications)#FULL-DUPLEX) communication on Web, which protocol was standardized as [RFC 6455](https://datatracker.ietf.org/doc/html/rfc6455).

The following diagram, quoted by [Wikipedia](https://en.wikipedia.org/wiki/WebSocket), describe a communication using WebSocket between client and server.

![A diagram describing a connection using WebSocket](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z7qiia95sgoly2rya998.png)

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](https://github.com/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](https://datatracker.ietf.org/doc/html/rfc6455).

## WebSocket server

First, let's learn the WebSocket protocol with server implementation. The sample code is available on [GitHub repository](https://github.com/hgsgtk/go-snippets/blob/e756b7f76d58a23e17d150b77bec9bb798fb2db2/goproxy/wsproxy/server/main.go). I'll explain it line by line.

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

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.

```go
var upgrader = websocket.Upgrader{}
```

`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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/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](https://datatracker.ietf.org/doc/html/rfc6455#section-1.2) 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

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

Fields of [Upgrader](https://pkg.go.dev/github.com/gorilla/websocket#Upgrader) struct is as follow:

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

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z7qiia95sgoly2rya998.png)

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

## Subprotocols

[Subprotocols](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#subprotocols) is like a custom XML schema or doctype declaration.

        Sec-WebSocket-Protocol: soap, wamp

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.

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

`echo` function first performs WebSocket handshake procedure in response to a request from client. The detailed implementation is in [Upgrader.Upgrade](https://pkg.go.dev/github.com/gorilla/websocket#Upgrader.Upgrade) function.

```go
c, err := upgrader.Upgrade(w, r, nil)
```

`Upgrade` upgrades the HTTP server connection to the WebSocket protocol.

## Handshake request validation

`Upgrade` function begins with [gorilla/websocket codes](https://github.com/gorilla/websocket/blob/v1.4.2/server.go#L123) which validate client HTTP request.

```go
	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")
	}
```

The specification of handshake is as follow:

- The request MUST contain a [Connection](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/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](https://datatracker.ietf.org/doc/html/rfc6455#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](https://www.iana.org/assignments/websocket/websocket.xml#version-number). RFC 6415 is registered as Version 13, so basically, only 13 should be supported.

## Sec-Websocket-Key

There is also [gorilla/websocket codes](https://github.com/gorilla/websocket/blob/b65e62901fc1c0d968042419e74789f6af455eb9/server.go#L154) on the way in `Upgrade` function.

```go
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")
}
```

`Sec-Websocket-Key` is used mainly for two purposes according to RFC 6455 and the discussion on [stack overflow](https://stackoverflow.com/questions/18265128/what-is-sec-websocket-key-for).

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](https://dev.to/hgsgtk/how-decided-a-value-set-in-sec-websocket-keyaccept-header-l79) for more information.

## Return HTTP status code 101 (Switching Protocols)

`Upgrade` function ends with [gorilla/websocket codes](https://github.com/gorilla/websocket/blob/v1.4.2/server.go#L217) which write HTTP headers.

```go
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"...)
```

The HTTP [101 Switching Protocols](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/101) 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.

- [Deep dive into WebSocket opening handshake protocol with Go](https://dev.to/hgsgtk/websocket-client-3gni)
    - It explains the WebSocket handshaking from the client perspective.
- [How WebSocket protocol designs bidirectional messaging and implements in Go](https://dev.to/hgsgtk/how-websocket-protocol-designs-bidirectional-messaging-and-implements-in-go-260f)