Websocket
By ppcamp
| 13 minutes read | 2743 wordsWhat’s websocket?
Websocket is a technology that is used to swap messages between two or more
peers without create a HTTP request connection every time.
It’s usually used to create real-time web applications, social network feeds,
PWA and push notifications, chats, etc. In Ubuntu based distributions, for
example, you can use a websocket server that will have a thread to every amount
of time, refresh some data and trigger a new event, making system-calls to the
notify-send program, which will
show you a notification in your desktop. In notify-send
you also can
change the icons.
1# Install notify-send if you don't have it yet
2sudo apt-get install libnotify-bin
3
4# Send a message
5# notify-send [OPTIONS...] "TITLE" "MESSAGE"
6notify-send "Some Title" "Some message"
Which produces the following float message:

In youtube you’ll find great explanations videos about websocket, bellow you can check one of them
Why you should use websocket?
According to the book Websocket LightWeight Client-Server Communications:
WebSocket gives you the ability to use an upgraded HTTP request (Chapter 8 covers the particulars), and send data in a message-based way, similar to UDP and with all the reliability of TCP. This means a single connection, and the ability to send data back and forth between client and server with negligible penalty in resource utiliza‐ tion. You can also layer another protocol on top of WebSocket, and provide it in a secure way over TLS. Later chapters dive deeper into these and other features such as heartbeating, origin domain, and more.
In this same book, the author claims that using a long polling technique will overhead your server resources. Basically, the flow that exemplifies the long polling is described as
Another way of achieving this is to control the refresh rate in the client side
by using tickers
and basic requests, for example, imagine that you wanna make
a social media feed.
To implement this you can, from time to time, make GET
requests in a simple code using the setTimeout
approach1.
1const refresh = {
2 'REFRESH_TIME_MS': 3e3, // 3s
3 'refresh_handler': null,
4 'stop': async function () {
5 clearInterval(this.refresh_handler);
6 },
7 'start': async function () {
8 // fetching data
9 // Default options are marked with *
10 const response = await fetch(url, {
11 method: 'POST', // *GET, POST, PUT, DELETE, etc.
12 mode: 'cors', // no-cors, *cors, same-origin
13 cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
14 credentials: 'same-origin', // include, *same-origin, omit
15 headers: {
16 'Content-Type': 'application/json'
17 // 'Content-Type': 'application/x-www-form-urlencoded',
18 },
19 redirect: 'follow', // manual, *follow, error
20 referrerPolicy: 'no-referrer', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
21 body: JSON.stringify(data) // body data type must match "Content-Type" header
22 });
23
24 console.log('Got data');
25
26 // enqeue a new call to refresh fn
27 this.refresh_handler = setInterval(refresh, this.REFRESH_TIME_MS);
28
29 return response;
30 }
31}
32
33// make requests from time to time
34refresh.start()
35
36// clear the object from the queue (stop polling)
37// refresh.stop()
Anyway, basically, when using a websocket connection you’ll be able to:
- keep a connection alive, without overhead your server
- send biderectional messages anytime, giving you a connection “based on events”2
- due to the item above, you basically have a real-time environment.
- low latency
However, the websocket approach will force you to have more goroutines open in a Golang environment.
Basically, at this point, I’m assuming that you already searched about websockets, and you do know that every technology has a treadoff3.
What I’m trying to say is:
“Don’t use a hammer for a screw”
Let’s talk about the code itself.
Part 1
In this part, I plan to reach the project described in the Websocket LightWeight Client-Server Communications book until chapter 4. The code itself can be found in this link.
HTML/Client view
The author gave to us a basic html, which uses Bootstrap v3.2.0 (link). Since that I ain’t focusing on web/client, I won’t rewrite it into Bootstrap v5.0. However, if you have some free time, take a look into it.
With the HTML/Client code in hands, you’ll need to make the server part.
Server
The server uses the gorilla library. In the project, you can find some samples about how to implement a websocket server. In this project, I took the chat example as a base to delop the code.
Here is the folder structure until now:
.
├── public
│ └── index.html
├── README.md
└── src
├── cmd
│ └── main.go
├── go.mod
├── go.sum
├── internal
│ ├── app
│ │ └── run.go
│ ├── config
│ │ ├── app.go
│ │ ├── date.go
│ │ ├── flags.go
│ │ └── log.go
│ └── controllers
│ ├── home.go
│ └── websocket.go
└── pkg
├── helpers
│ ├── log.go
│ └── websocket.go
├── models
├── repository
├── services
│ └── websocket
│ ├── client.go
│ ├── events.go
│ ├── model.go
│ └── server.go
└── utils
├── must.go
└── string.go
In the main func I use the cli to read the environment variables and create helpers to the binary.
file: src/cmd/main.go
1package main
2
3import (
4 "github.com/urfave/cli/v2"
5 "os"
6 "src/internal/app"
7 "src/internal/config"
8)
9
10func main() {
11 application := cli.NewApp()
12 application.Name = "go-websocket"
13 application.Description = "A Golang simple websocket server"
14 application.Usage = "go-websocket server || go-websocket client 'name'"
15 application.Flags = config.Flags
16 application.Action = app.Run
17 application.Run(os.Args)
18}
The flags/env variables are defined in the file src/internal/config/flags.go.
To make a tutorial more concise, I won’t show all files. I’ll focusing only in the websocket part.
At first, we need to expose a Http connection, that later will be “Upgraded” into a Websocket connection.
First of all, we need to create the server logic.
file: src/internal/app/run.go
1package app
2
3import (
4 "log"
5 "net/http"
6 "src/internal/config"
7 "src/internal/controllers"
8 "src/pkg/services/websocket"
9
10 "github.com/urfave/cli/v2"
11)
12
13func Run(_ *cli.Context) error {
14 config.SetupLoggers()
15
16 ws := websocket.NewServer()
17 go ws.Start()
18 wrap := func(w http.ResponseWriter, r *http.Request) { controllers.Websocket(ws, w, r) }
19
20 http.HandleFunc("/", controllers.Home)
21 http.HandleFunc("/ws", wrap)
22
23 err := http.ListenAndServe(config.App.Address, nil)
24 if err != nil {
25 log.Fatalln(err)
26 }
27 return nil
28}
The websocket.NerServer return to us an object, that is actually, the server itself, the “main” hub.
file: src/pkg/services/websocket/events|model.go
1package websocket
2
3type SocketEventType string
4
5const (
6 Message SocketEventType = "message"
7 Notification SocketEventType = "notification"
8 NickUpdate SocketEventType = "nick_update"
9)
10
11const (
12 OpChangeNick = "/nick"
13)
14
15
16
17type SocketMessage struct {
18 Type SocketEventType `json:"type"`
19 Id string `json:"id"`
20 Nickname string `json:"nickname"`
21 Message *string `json:"message"`
22}
Clients
The clients are defined in the
file: src/pkg/services/websocket/client.go
1package websocket
2
3import (
4 "bytes"
5 "fmt"
6 "src/pkg/helpers"
7 "src/pkg/utils"
8 "strings"
9 "time"
10
11 "github.com/google/uuid"
12 "github.com/gorilla/websocket"
13)
14
15var logClient = helpers.NewModuleLogger("WebSocketClient")
16
17const (
18 writeWait = 10 * time.Second // Time allowed to Write a message
19 pongWait = 60 * time.Second // Time allowed to Read the next pong
20 pingPeriod = (pongWait * 9) / 10 // Send pings to peer with this period
21 maxMessageSize = bytes.MinRead // Maximum message size (in bytes)
22)
23
24type Client struct {
25 socket *websocket.Conn
26
27 Uuid string
28 Nick string
29 addr string
30 Message chan string
31 hub *Server
32}
33
34func NewClient(socket *websocket.Conn, server *Server) *Client {
35 addr := socket.RemoteAddr()
36 uuid := utils.Must(uuid.NewRandom()).(uuid.UUID)
37 nick := fmt.Sprintf("AnonymousUser%d", server.UsersLength())
38
39 return &Client{
40 Uuid: strings.Replace(uuid.String(), "-", "", -1),
41 socket: socket,
42 hub: server,
43 Message: make(chan string),
44 Nick: nick,
45 addr: utils.Must(helpers.WebsocketAddress(addr)).(string),
46 }
47}
48
Like in the book, we’ll implement the function of change nick
1func (c *Client) changeNick(msg string) {
2 contentArray := strings.Split(msg, " ")
3 if len(contentArray) >= 2 {
4 old := c.Nick
5 c.Nick = contentArray[1]
6 message := fmt.Sprintf("Client %s changed to %s", old, c.Nick)
7
8 // Broadicasting the message to the clients. It'll be explained later on
9 c.hub.Send(NickUpdate, &c.Uuid, &c.Nick, message)
10 }
11}
Differently of the NodeJS implementation, which we don’t need to worry about the inner methods and communication, in Golang we need to, therefore, it’s necessary to create to methods, one of them will be responsable for read a message channel and write the data into the current peer, the other one, will be responsable to read the received websocket message and send it to the HUB
1// read Opens a thread that will be listening to the
2// websocket. It'll be responsible to send the messages got into the Server/hub
3func (c *Client) read() {
4 defer func() {
5 c.hub.UnregisterClient(c)
6 c.socket.Close()
7 }()
8
9 // set the maximum amount of bytes that will be allowed to read from the
10 // websocket. If it's greater than this, it'll close the connection
11 c.socket.SetReadLimit(maxMessageSize)
12
13 // set the maximum amount of time that is acceptable to receive the answer from
14 // the client
15 c.socket.SetReadDeadline(time.Now().Add(pongWait))
16
17 // set the function when receive a pong.
18 // the websocket sent ping and receive pong messages from time to time
19 // to check if the client is still connected
20 // when we receive a pong message, we'll reset the reading deadline, to allow
21 // the client to still send messages.
22 c.socket.SetPongHandler(func(string) error { c.socket.SetReadDeadline(time.Now().Add(pongWait)); return nil })
23
24 for {
25 _, message, err := c.socket.ReadMessage()
26 if err != nil {
27 // note that I ain't checking the type of the error here, but you can
28 // use the websocket.IsUnexpectedCloseError(err, ...errorsArray)
29 // function to validate it
30 c.socket.WriteMessage(websocket.TextMessage, []byte("Fail to read message"))
31 return
32 }
33 msg := string(message)
34
35 // Client/Socket operations
36 if utils.StartsWith(msg, OpChangeNick) {
37 c.changeNick(msg)
38 } else {
39 c.hub.Send(Message, &c.Uuid, &c.Nick, msg)
40 }
41 }
42}
By now, we are able to receive any messages sent from the html page (client). However, we aren’t sent messages to the client yet. To make this, we need to spawn the write thread.
1// write the messages read from the message channel
2// and write it into the socket. This method close the socket connection
3// when some errors occurs, therefore, it'll raise an error in the read,
4// which will unregister the client
5func (c *Client) write() {
6 // the ticker will be used to check if we still have connection with the
7 // websocket client
8 ticker := time.NewTicker(pingPeriod)
9
10 defer func() {
11 ticker.Stop()
12 c.socket.Close()
13 }()
14
15
16 for {
17 select {
18
19 case message, ok := <-c.Message:
20 // we receive a message from the HUB to be sent to the current user
21 if !ok {
22 logClient.WithField("client", c.Uuid).Warn("The server closed the channel")
23 c.socket.WriteMessage(websocket.CloseMessage, []byte{})
24 return
25 }
26
27 // the maximum amount of time to write the message into the websocket client
28 c.socket.SetWriteDeadline(time.Now().Add(writeWait))
29 err := c.socket.WriteMessage(websocket.TextMessage, []byte(message))
30 if err != nil {
31 logClient.WithField("client", c.Uuid).Warn(err)
32 return
33 }
34
35 case <-ticker.C:
36 // in the current loop, the channel who send a message was the ticker
37 // in this case, we'll send a ping to check if the client (html page) are
38 // still connected. For example, if we close our browser tab, the conn
39 // will be lost, and when the server send the ping, it'll raise an error
40 // and then, will remove the client from our hub and free the memory and
41 // goroutines.
42 c.socket.SetWriteDeadline(time.Now().Add(writeWait))
43 if err := c.socket.WriteMessage(websocket.PingMessage, nil); err != nil {
44 logClient.WithField("client", c.Uuid).
45 WithError(err).
46 Warn("A ping message was sent and the client didn't respond. Removing the client...")
47 return
48 }
49 }
50 }
51}
The start function is just a method to spawn those functions concurrently.
1// Start two goroutines to handle with read and write to the websocket clients
2// connected
3func (c *Client) Start() {
4 go c.read()
5 go c.write()
6
7 logClient.WithField("client", c.Uuid).Info("Started")
8}
9
10// Close the Message channel used to swap messages between clients and server
11// management
12func (c *Client) Close() {
13 logClient.WithField("client", c.Uuid).Info("Closed")
14 close(c.Message)
15}
16
17// Locals gets the client local port
18func (c *Client) Locals() *string { return &c.addr }
19
20// Id gets the client unique id
21func (c *Client) Id() *string { return &c.Uuid }
Server
file: src/pkg/services/websocket/server.go
Now, getting back to the Server implementation,
we need to define the Start
method, which is spawned by the Run
.
1package websocket
2
3import (
4 "encoding/json"
5 "src/pkg/helpers"
6 "src/pkg/utils"
7
8 "github.com/sirupsen/logrus"
9)
10
11type Server struct {
12 clients map[*string]*Client
13
14 // messages that will be sent to the others (all)
15 broadcast chan string
16
17 // register a new client
18 register chan *Client
19
20 // unregister a client
21 unregister chan *Client
22
23 log *logrus.Entry
24}
25
26// NewServer create and returns a new websocket server
27func NewServer() *Server {
28 return &Server{
29 clients: make(map[*string]*Client),
30 broadcast: make(chan string),
31 register: make(chan *Client),
32 unregister: make(chan *Client),
33 log: helpers.NewModuleLogger("WebSocketServer"),
34 }
35}
36
1func (s *Server) addClient(c *Client) {
2 s.clients[c.Id()] = c
3}
4
5func (s *Server) removeClient(c *Client) {
6 if _, ok := s.clients[c.Id()]; ok {
7 delete(s.clients, c.Id())
8 c.Close()
9 }
10}
11
12func (s *Server) RegisterClient(c *Client) {
13 s.log.Infof("client %s registered", *c.Id())
14 s.register <- c
15}
16func (s *Server) UnregisterClient(c *Client) {
17 s.log.Infof("client %s removed", *c.Id())
18 message := c.Nick + " has disconnected"
19 s.unregister <- c
20 s.Send(Notification, &c.Uuid, &c.Nick, message)
21}
22
23// Start the websocket server
24// TODO: must implement a stop to the server
25//
26// Example:
27// ws := websocket.NewServer()
28// go ws.Start()
29func (s *Server) Start() {
30 for {
31 select {
32 case client := <-s.register:
33 s.addClient(client)
34
35 case client := <-s.unregister:
36 s.removeClient(client)
37
38 case message := <-s.broadcast:
39 // broadcast to clients
40 for name := range s.clients {
41 select {
42 case s.clients[name].Message <- message:
43 default:
44 s.removeClient(s.clients[name])
45 }
46 }
47 }
48 }
49}
50
51func (s *Server) Send(t SocketEventType, clientId, nick *string, message string) {
52 obj := utils.Must(json.Marshal(SocketMessage{t, *clientId, *nick, &message})).([]byte)
53 s.broadcast <- string(obj)
54}
55
56func (s *Server) UsersLength() int { return len(s.clients) }
57
Serving the html page
Finally, we need to serve the html to the user connect into it.
file: src/internal/controllers/home.go
1package controllers
2
3import (
4 "log"
5 "net/http"
6 "src/internal/config"
7)
8
9func Home(w http.ResponseWriter, r *http.Request) {
10 log.Println(r.URL)
11
12 if r.URL.Path != "/" {
13 http.Error(w, "Not found", http.StatusNotFound)
14 return
15 }
16
17 if r.Method != http.MethodGet {
18 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
19 return
20 }
21
22 http.ServeFile(w, r, config.App.PublicFolder)
23}
24
Results
The results can be seen in the image below

Conclusions
With this tutorial I expect that you can have a clear understanding of how to implement websocket, when use, and the treadoffs of using it.
Feel free to send me a message improving this code or making some appointments about my text.
Best regards, @ppcamp-
Here, or here you can see a good way to visualise it. You must avoid use the
setInterval
because it can, sometimes, block your main thread. Furthermore, it may force your program to keep open, even if you closed your app ‘cause it’ll stay in the threadpool/mainloop. Check it out here, or here to read more about it. ↩︎