How WebSocket protocol designs bidirectional messaging and implements in Go
Tags: webdev, websocket, go
Published: 2021-12-13 04:59:20

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

"Handshake" is explained in the following two articles.

- [Deep dive into WebSocket opening handshake protocol with Go](https://dev.to/hgsgtk/websocket-client-3gni)
- [Learn WebSocket handshake protocol with gorilla/websocket server](https://dev.to/hgsgtk/learn-websocket-handshake-protocol-with-gorillawebsocket-server-10k9)
- [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)

This post will focus on "Bidirectional messages"

## WebSocket server

Let's learn the WebSocket bidirectional messaging protocol with a specific Go implementation, which is available on a [GitHub repository](https://github.com/hgsgtk/go-snippets/blob/e756b7f76d58a23e17d150b77bec9bb798fb2db2/goproxy/wsproxy/server/main.go).

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

    log.Println("Success to handshake with client")

    // Start bidirectional messages
    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
        }
    }
    // End bidirectional messages
}
```

## Open handshake

The first half of the code is the server implementation for "Handshake".

```go
// 1. Initializes parameters for upgrading an HTTP connection to a WebSocket connection.
var upgrader = websocket.Upgrader{}

// 2. Starts WebSocket server which waits client HTTP requests on port 12345
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) {
    // 3. Start handshaking
    c, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Printf("Upgrading error: %#v\n", err)
        return
    }
    defer c.Close()
    // End

    log.Println("Success to handshake with client")

    // ...(Bidirectional messages part)
```

As noted in the comments in the code, to start up a WebSocket server, do the following:

1. Initializes parameters for upgrading an HTTP connection to a WebSocket connection.
2. Starts WebSocket server which waits client HTTP requests on port 12345
3. Start handshaking

When you run this program, it will start an HTTP server on port 12345.

```
$ go run server/main.go
2021/12/13 11:54:22 Starting websocket echo server on port 12345
```

You can check if the handshake is actually working by making a curl request (but it's not perfect).

```
$ curl -X GET http://localhost:12345 \
-H "Connection: upgrade" \
-H "Upgrade: websocket" \
-H "Sec-Websocket-version: 13" \
-H "Sec-Websocket-Key: 08kp54j1E3z4IfuM1m75tQ==" \
-H "Host: localhost:12345" \
-H "Origin: http://localhost:12345"
```

When you send it, you will see that the request was accepted in stdout.

```
$ go run main.go
(omit)...
2021/12/13 12:07:19 Success to handshake with client
```

See [Learn WebSocket handshake protocol with gorilla/websocket server](https://dev.to/hgsgtk/learn-websocket-handshake-protocol-with-gorillawebsocket-server-10k9) for more information.

## WebSocket connection

The key to understanding the message exchanging is the return value of this line.

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

The variable `c` is a pointer of [websocket.Conn](https://pkg.go.dev/github.com/gorilla/websocket#Conn) struct. The Conn type represents a WebSocket connection.

When the handshake is completed, you can start exchanging messages over the established WebSocket connection.

You can receive and send messages by calling the Conn's [ReadMessage](https://pkg.go.dev/github.com/gorilla/websocket#Conn.ReadMessage) and [WriteMessage](https://pkg.go.dev/github.com/gorilla/websocket#Conn.WriteMessage).

```go
// Start bidirectional messages
for {
	mt, message, err := c.ReadMessage()
	if err != nil {
		log.Printf("Reading error: %#v\n", err)
		break
	}
	log.Printf("recv: message %q", message)

	// (omit...writing message part)
}
// End bidirectional messages
```

Use `for` loop so that the server keeps waiting for messages from the client. 

## Data Framing

In the WebSocket protocol, data is transmitted using a sequence of frames. This wire format for the data transfer part is described by the [ABNF](https://datatracker.ietf.org/doc/html/rfc5234) (Augmented BNF for Syntax Specification). A overview of the framing is given in the following figure.

![A figure of base framing protocol](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ya8sxy582lm88yp9rssk.png)

Server and client exchange data in accordance with this method.

## Read messages from the peer

[ReadMessage](https://pkg.go.dev/github.com/gorilla/websocket#Conn.ReadMessage) is a method to read messages from the peer which interprets data in base framing protocol.

```go
mt, message, err := c.ReadMessage()
```

The implementation of [ReadMessage](https://github.com/gorilla/websocket/blob/v1.4.2/conn.go#L1062) is as follow:

```go
func (c *Conn) ReadMessage() (messageType int, p []byte, err error) {
	var r io.Reader
	messageType, r, err = c.NextReader()
	if err != nil {
		return messageType, nil, err
	}
	p, err = ioutil.ReadAll(r)
	return messageType, p, err
}
```

The [Conn.NextReader](https://pkg.go.dev/github.com/gorilla/websocket#Conn.NextReader) function reads bytes received from the peer. 

```go
messageType, r, err = c.NextReader()
```

## Opcode

First return value is `messageType` which represents WebSocket opcode numbers defined in [RFC 6455 - 11.8.  WebSocket Opcode Registry](https://datatracker.ietf.org/doc/html/rfc6455#section-11.8).

| Opcode | Meaning |
| -- | -- |
| 0 | Continuation Frame |
| 1 | Text Frame |
| 2 | Binary Frame |
| 8 | Connection Close Frame |
| 9 | Ping Frame |
| 10 | Pong Frame |

There are two types of opcode: message for exchanging data and controlling WebSocket connection.

- For exchanging data
    - Text Frame
    - Binary Frame
- For controlling WebSocket connection
    - Connection Close Frame
    - Ping Frame
    - Pong Frame
    - Contination Frame

The messages for exchanging data distinguishes between text and binary data messages. And, the WebSocket protocol defines four types of control messages: close, ping, pong, and continuation.

## Conclusion

This article explains WebSocket bidirectional message using gorilla/websocket server implementation.

Related articles are here.

- [Learn WebSocket handshake protocol with gorilla/websocket server](https://dev.to/hgsgtk/learn-websocket-handshake-protocol-with-gorillawebsocket-server-10k9)
- [Deep dive into WebSocket opening handshake protocol with Go](https://dev.to/hgsgtk/websocket-client-3gni)