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}
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. This tutorial uses gorilla/websocket v1.5.1.
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
- Receive the http request from the client
- 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 the text string “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 log.Printf("Error %s when reading message from client", err)
5 return
6 }
7 if mt == websocket.BinaryMessage {
8 err = c.WriteMessage(websocket.TextMessage, []byte("server doesn't support binary messages"))
9 if err != nil {
10 log.Printf("Error %s when sending message to client", err)
11 }
12 return
13 }
14 log.Printf("Receive message %s", string(message))
15 if strings.Trim(string(message), "\n") != "start" {
16 err = c.WriteMessage(websocket.TextMessage, []byte("You did not say the magic word!"))
17 if err != nil {
18 log.Printf("Error %s when sending message to client", err)
19 return
20 }
21 continue
22 }
23 log.Println("start responding to client...")
24 i := 1
25 for {
26 response := fmt.Sprintf("Notification %d", i)
27 err = c.WriteMessage(websocket.TextMessage, []byte(response))
28 if err != nil {
29 log.Printf("Error %s when sending message to client", err)
30 return
31 }
32 i = i + 1
33 time.Sleep(2 * time.Second)
34 }
35}
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 log.Printf("Error %s when reading message from client", err)
31 return
32 }
33 if mt == websocket.BinaryMessage {
34 err = c.WriteMessage(websocket.TextMessage, []byte("server doesn't support binary messages"))
35 if err != nil {
36 log.Printf("Error %s when sending message to client", err)
37 }
38 return
39 }
40 log.Printf("Receive message %s", string(message))
41 if strings.Trim(string(message), "\n") != "start" {
42 err = c.WriteMessage(websocket.TextMessage, []byte("You did not say the magic word!"))
43 if err != nil {
44 log.Printf("Error %s when sending message to client", err)
45 return
46 }
47 continue
48 }
49 log.Println("start responding to client...")
50 i := 1
51 for {
52 response := fmt.Sprintf("Notification %d", i)
53 err = c.WriteMessage(websocket.TextMessage, []byte(response))
54 if err != nil {
55 log.Printf("Error %s when sending message to client", err)
56 return
57 }
58 i = i + 1
59 time.Sleep(2 * time.Second)
60 }
61 }
62}
63
64func main() {
65 webSocketHandler := webSocketHandler{
66 upgrader: websocket.Upgrader{},
67 }
68 http.Handle("/", webSocketHandler)
69 log.Print("Starting server...")
70 log.Fatal(http.ListenAndServe("localhost:8080", nil))
71}
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.