DEV Community

Kazuki Higashiguchi
Kazuki Higashiguchi

Posted on

How WebSocket protocol designs bidirectional messaging and implements 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

"Handshake" is explained in the following two articles.

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.

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
}
Enter fullscreen mode Exit fullscreen mode

Open handshake

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

// 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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

See Learn WebSocket handshake protocol with gorilla/websocket server for more information.

WebSocket connection

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

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

The variable c is a pointer of 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 and WriteMessage.

// 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
Enter fullscreen mode Exit fullscreen mode

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 (Augmented BNF for Syntax Specification). A overview of the framing is given in the following figure.

A figure of base framing protocol

Server and client exchange data in accordance with this method.

Read messages from the peer

ReadMessage is a method to read messages from the peer which interprets data in base framing protocol.

mt, message, err := c.ReadMessage()
Enter fullscreen mode Exit fullscreen mode

The implementation of ReadMessage is as follow:

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
}
Enter fullscreen mode Exit fullscreen mode

The Conn.NextReader function reads bytes received from the peer.

messageType, r, err = c.NextReader()
Enter fullscreen mode Exit fullscreen mode

Opcode

First return value is messageType which represents WebSocket opcode numbers defined in RFC 6455 - 11.8. WebSocket Opcode Registry.

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.

Discussion (0)