Go WebSocket Tutorial

What are WebSockets and what are their advantages over traditional HTTP?

HTTP is simply a request response protocol. This means a client such as a web browser sends a request and the server works on this request and sends a response. This request and response uses a TCP connection underneath. Once the response is received, the TCP connection is closed and can’t be reused. The client has to initiate a new TCP connection if it wants to make a new request.

HTTP is half duplex which means only one entity can send data at a time. The client can send a request on a TCP connection and only after the client has finished sending the request can the server send a response on the same connection.

The HTTP protocol stood the test of time and it’s widely used even today. But as technology matured, HTTP couldn’t keep up with the demands of real time applications. Real time applications like web chat(facebook messenger), twitter news feed and push notifications for smart phones required the client to be updated with data from the server in near real time. This could be achieved in HTTP by making the client poll the server at regular intervals. This method would mean that bandwidth is wasted. The server will also be flooded with requests even when it has no new updates for a client wasting server resources such as CPU, memory and bandwidth. To solve these issues, the WebSocket protocol was introduced.

Web Socket protocol provides bidirectional communication on a single TCP connection. The same TCP connection can be used for sending and receiving at the same time. The TCP connection is kept open and it can be used either by the client or by the server until it is closed by either party. This allows real time applications like chat and news feed easily implementable using WebSocket. Whenever there is a new tweet in the timeline of the user, the server can just push this tweet to the client instead of the client polling at regular intervals.

WebSocket Protocol

Now that we have understood what WebSocket is, let’s try to understand the actual protocol before implementing it using Go. This will help us to easily understand the steps that are needed to implement a WebSocket server using Go.

Initial Handshake

The WebSocket protocol uses HTTP for the initial handshake. You might be wondering why? The reason is because this enables WebSocket to use the existing HTTP infrastructure such as proxies, authentication and so on without the need for new intermediaries. Websocket protocol uses port 80 for non encrypted connections and port 443 for encrypted connections. It uses scheme ws for non encrypted connections and wss for encrypted connections.

Once the initial handshake between the client and server is established, the HTTP connection is converted to a WebSocket connection and data transfer starts.

Client Handshake

We will go one step further and try to understand what headers are exchanged between the client and the server during handshake.

GET /
Host: websocket.golangbot.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: YSBzYW1wbGUgMTYgYnl0ZQ==
Sec-Websocket-Version: 13

Above are few of the critical headers in the handshake initiated by the client.

The Host header specifies the host name to connect to.
The Upgrade header indicates that the client wishes to upgrade the http connection to a WebSocket after the handshake.
The Connection header with value Upgrade must always be sent if there is a Upgrade header.
The Sec-WebSocket-Key is 16 byte string and it must be base64 encoded. In this case, the string a sample 16 byte is base64 encoded.
Running echo -n "a sample 16 byte" | base64 in the terminal will output YSBzYW1wbGUgMTYgYnl0ZQ== which is used as the Sec-WebSocket-Key: header. We will discuss more about the use of this header in the next section.

The Sec-Websocket-Version header specifies the WebSocket version the client wishes to use.

Server’s Response to Client’s Handshake

The server sends the following response to the client handshake request.

HTTP 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: zkTGI6zVrOIDXiC4vnn1Rf37YFw=

The server responds with a HTTP status code of 101 indicating that it is switching protocols to the one specified in the client’s Upgrade request header. In our case, the server is switching to websocket.

The Sec-WebSocket-Accept header is calculated by appending YSBzYW1wbGUgMTYgYnl0ZQ== which was the Sec-WebSocket-Key: header sent by the client along with the GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 and computing the sha1 hash of the appended value. The Go program to do this is provided below.

 1package main
 2
 3import (
 4	"crypto/sha1"
 5	"encoding/base64"
 6	"io"
 7	"os"
 8)
 9
10func main() {
11	h := sha1.New()
12	io.WriteString(h, "YSBzYW1wbGUgMTYgYnl0ZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
13	encoder := base64.NewEncoder(base64.StdEncoding, os.Stdout)
14	encoder.Write(h.Sum(nil))
15	encoder.Close()
16}

Run in playground

The above program will print zkTGI6zVrOIDXiC4vnn1Rf37YFw= which is used as the Sec-WebSocket-Accept response header.

The use of Sec-WebSocket-Key and the Sec-WebSocket-Accept header is to ensure that the server has understood that this is a WebSocket upgrade request and doesn’t accept connections that are not WebSocket.

This brings us to the end of the WebSocket handshake phase. After the handshake is finished, the client and the server exchange messages in a full duplex connection.

Creating a WebSocket Server in Go

We will be developing a simple push notification server. The server will at random intervals send notifications to the client. We could implement the complete WebSocket server from the ground up, but we already have libraries in Go which implement the WebSocket protocol. The gorilla/websocket library will be used to implement our WebSocket server.

Let’s create a folder first and initialize a go module.

1mkdir ~/Documents/go-websocket
2cd ~/Documents/go-websocket/
3go mod init github.com/golangbot/go-websocket

The first step in implementing a WebSocket server is the initial handshake. The steps to implement the initial handshake are provided below

  1. Receive the http request from the client
  2. Upgrade the http connection to a WebSocket

The server program for performing the above steps in provided below.

 1package main
 2
 3import (
 4	"log"
 5	"net/http"
 6
 7	"github.com/gorilla/websocket"
 8)
 9
10type webSocketHandler struct {
11	upgrader websocket.Upgrader
12}
13
14func (wsh webSocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
15	c, err := wsh.upgrader.Upgrade(w, r, nil)
16	if err != nil {
17		log.Printf("error %s when upgrading connection to websocket", err)
18		return
19	}
20	defer c.Close()
21}
22
23func main() {
24	webSocketHandler := webSocketHandler{
25		upgrader: websocket.Upgrader{},
26	}
27	http.Handle("/", webSocketHandler)
28	log.Print("Starting server...")
29	log.Fatal(http.ListenAndServe("localhost:8080", nil))
30}

In line no. 10 of the above program, we define the webSocketHandler handler which will be the http handler for the WebSocket server. In the next line, we define upgrader which is responsible for upgrading the http connection to a WebSocket. This upgrader struct field is of type websocket.Upgrader which is provided by the gorilla WebSocket package.

In line no. 14, the http handler which will serve the WebSocket endpoint by implementing the ServeHTTP method is defined. In line no.15 we use the Upgrade method of the upgrader to upgrade the http connection to a WebSocket. We then Close() the connection at the end.

In the main function, we define our http handler and use the gorilla WebSocket package’s default upgrader in line no. 25. We then register the handler in line no. 27 and start the http server.

Save the above program as main.go in go-websocket folder. Run go mod tidy followed by go run main.go to run the program.

Running this program and will print

1Starting server...

Amazing. We have started our WebSocket server. The next step is to test whether it works.

Testing the WebSocket Server using curl

We can use curl to test whether our WebSocket server works. If we recollect the WebSocket protocol, the client should first initiate a handshake with the following headers

Host: websocket.golangbot.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: YSBzYW1wbGUgMTYgYnl0ZQ==
Sec-Websocket-Version: 13

Let’s create this request with curl

1curl -i --header "Upgrade: websocket" \
2--header "Connection: Upgrade" \
3--header "Sec-WebSocket-Key: YSBzYW1wbGUgMTYgYnl0ZQ==" \
4--header "Sec-Websocket-Version: 13" \
5localhost:8080

The curl request above sets the required headers. Running the above command will print

1HTTP/1.1 101 Switching Protocols
2Upgrade: websocket
3Connection: Upgrade
4Sec-WebSocket-Accept: zkTGI6zVrOIDXiC4vnn1Rf37YFw=
5
6curl: (52) Empty reply from server

Bingo! The first line of the output says that the server is switching protocols to WebSocket. The server also responds with the Sec-WebSocket-Accept. If you notice carefully, zkTGI6zVrOIDXiC4vnn1Rf37YFw= matches the sha1 hash that we computed earlier when discussing the servers response to the client’s handshake message.

Sending and Receiving Messages

In the next step we will receive messages from the client and push a few messages in response.
The server will wait for the client to send a message start. Once the server receives this message, it will start responding to the client.

Add the following code to the ServeHTTP method in the server.

 1for {
 2	mt, message, err := c.ReadMessage()
 3	if err != nil {
 4		return
 5	}
 6	if mt == websocket.BinaryMessage {
 7		err = c.WriteMessage(websocket.TextMessage, []byte("server doesn't support binary messages"))
 8		if err != nil {
 9			log.Printf("Error %s when sending message to client", err)
10		}
11		return
12	}
13	log.Printf("Receive message %s", string(message))
14	if strings.Trim(string(message), "\n") != "start" {
15		err = c.WriteMessage(websocket.TextMessage, []byte("You did not say the magic word!"))
16		if err != nil {
17			log.Printf("Error %s when sending message to client", err)
18			return
19		}
20		continue
21	}
22	log.Println("start responding to client...")
23	i := 1
24	for {
25		response := fmt.Sprintf("Notification %d", i)
26		err = c.WriteMessage(websocket.TextMessage, []byte(response))
27		if err != nil {
28			return
29		}
30		i = i + 1
31		time.Sleep(2 * time.Second)
32	}
33}

The code is in a infinite loop to ensure that we can keep sending/receiving multiple messages using the same TCP connection. In line no. 2, the message sent by the client is read. The WebSocket protocol supports both text and binary message types. In line no. 6 we check whether the message type is binary. If that’s the case, we send a error message to the client and return.

In line no. 14, we compare whether the message sent by the client is start. If it’s not start, we return an error to the client.
If it’s start, we start sending messages like “Notification 1”, “Notification 2” every 2 seconds to the client.

The rest of the program is self explanatory.

The entire program is provided below for reference.

 1package main
 2
 3import (
 4	"fmt"
 5	"log"
 6	"net/http"
 7	"strings"
 8	"time"
 9
10	"github.com/gorilla/websocket"
11)
12
13type webSocketHandler struct {
14	upgrader websocket.Upgrader
15}
16
17func (wsh webSocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
18	c, err := wsh.upgrader.Upgrade(w, r, nil)
19	if err != nil {
20		log.Printf("error %s when upgrading connection to websocket", err)
21		return
22	}
23	defer func() {
24		log.Println("closing connection")
25		c.Close()
26	}()
27	for {
28		mt, message, err := c.ReadMessage()
29		if err != nil {
30			return
31		}
32		if mt == websocket.BinaryMessage {
33			err = c.WriteMessage(websocket.TextMessage, []byte("server doesn't support binary messages"))
34			if err != nil {
35				log.Printf("Error %s when sending message to client", err)
36			}
37			return
38		}
39		log.Printf("Receive message %s", string(message))
40		if strings.Trim(string(message), "\n") != "start" {
41			err = c.WriteMessage(websocket.TextMessage, []byte("You did not say the magic word!"))
42			if err != nil {
43				log.Printf("Error %s when sending message to client", err)
44				return
45			}
46			continue
47		}
48		log.Println("start responding to client...")
49		i := 1
50		for {
51			response := fmt.Sprintf("Notification %d", i)
52			err = c.WriteMessage(websocket.TextMessage, []byte(response))
53			if err != nil {
54				return
55			}
56			i = i + 1
57			time.Sleep(2 * time.Second)
58		}
59	}
60}
61
62func main() {
63	webSocketHandler := webSocketHandler{
64		upgrader: websocket.Upgrader{},
65	}
66	http.Handle("/", webSocketHandler)
67	log.Print("Starting server...")
68	log.Fatal(http.ListenAndServe("localhost:8080", nil))
69}

The next step is to test whether our server works as expected. Unfortunately curl has limited support for WebSocket, so we will not be able to use curl to test our WebSocket server.

Testing the WebSocket Server using websocat

websocat is a feature rich WebSocket client and it can be downloaded from https://github.com/vi/websocat. Follow the steps at https://github.com/vi/websocat?tab=readme-ov-file#installation to install websocat

Once websocat is downloaded and installed, try running it with the following command

1./websocat ws://localhost:8080/

After the above command is run, we will get a prompt to type our input. Type wrongmsg in that prompt. The server will respond with You did not say the magic word! since it is expecting the message start.
Now type start in the prompt. The server will start sending notifications to the client and we can see the following output.

1wrongmsg
2You did not say the magic word!
3start
4Notification 1
5Notification 2
6Notification 3

Amazing. Our server works!.

In the next tutorial, we will learn how to implement our own WebSocket client using Go and also how to write a front end for our server using HTML and JavaScript. If we do that, we need not rely on websocat to test our server.

I hope you liked this tutorial. Please leave your feedback and comments. Please consider sharing this tutorial on twitter or LinkedIn. Have a good day.