Yalantis
Drive in the true real-time functionality to your app with WebSockets in Go. In out post, we explain what WebSockets are and how to add them to your app.

How to implement instant messaging with WebSockets in Go

  • Aleksandr Ryzhyi

    Golang developer

Share

Sending a message and getting an instant response without refreshing the page is something we take for granted. But in the past, enabling real-time functionality was a real challenge for developers. The developer community has come a long way from HTTP long polling and AJAX and has finally found a solution for building truly real-time apps.

This solution comes in the form of WebSockets, which make it possible to open an interactive session between a user’s browser and a server. WebSockets allow a browser to send messages to a server and receive event-driven responses without having to poll the server for a reply.

For now, WebSockets are the number one solution for building real-time applications: online games, instant messengers, tracking apps, and so on. This guide explains how WebSockets operate and shows how we can build WebSocket applications in the Go programming language. We also compare the most popular WebSocket libraries so you can choose the best one for your needs.

Network sockets vs WebSockets

To discover how to get started with WebSockets in the GO, let’s begin by drawing the line between network sockets and WebSockets.

Network socket

A network socket, or simply a socket, serves as an internal endpoint for exchanging data between applications running on the same computer or on different computers on the same network.

Sockets are a key part of Unix and Windows-based operating systems, and they make it easier for developers to create network-enabled software. Instead of constructing network connections from scratch, app developers can include sockets in their programs. Since network sockets are used for several network protocols (HTTP, FTP, etc.), multiple sockets can be used simultaneously.

Sockets are created and used with a set of function calls defined by a socket’s application programming interface (API).

There are several types of network sockets:

Datagram sockets (SOCK_DGRAM), also known as connectionless sockets, use the User Datagram Protocol (UDP). Datagram sockets support a bidirectional flow of messages and preserve record boundaries.

Stream sockets (SOCK_STREAM), also known as connection-oriented sockets, use the Transmission Control Protocol (TCP), Stream Control Transmission Protocol (SCTP), or Datagram Congestion Control Protocol (DCCP). These sockets provide a bidirectional, reliable, sequenced, and unduplicated flow of data with no record boundaries.

Raw sockets (or raw IP sockets) are typically available in routers and other networking equipment. These sockets are normally datagram-oriented, although their exact characteristics depend on the interface provided by the protocol. Raw sockets are not used by most applications. They’re provided to support the development of new communication protocols and to provide access to more esoteric facilities of existing protocols.

Socket communication

Each network socket is identified by the address, which is a triad of a transport protocol, IP address, and port number.

There are two major protocols for communicating between hosts: TCP and UDP. Let’s see how your app can connect to TCP and UDP sockets.

  • Connecting to a TCP socket

To establish a TCP connection, a Go client uses the DialTCP function in the net package. DialTCP returns a TCPConn object. When a connection is established, the client and server begin exchanging data: the client sends a request to the server through a TCPConn object, the server parses the request and sends a response, and the TCPConn object receives the response from the server.

This connection remains valid until the client or server closes it. The functions for creating a connection are as follows:

Client side:

 // init
   tcpAddr, err := net.ResolveTCPAddr(resolver, serverAddr)
   if err != nil {
        // handle error
   }
   conn, err := net.DialTCP(network, nil, tcpAddr)
   if err != nil {
           // handle error
   }
   // send message
    _, err = conn.Write({message})
   if err != nil {
        // handle error
   }
   // receive message
   var buf [{buffSize}]byte
   _, err := conn.Read(buf[0:])
   if err != nil {
        // handle error
   }

Server side:

// init
   tcpAddr, err := net.ResolveTCPAddr(resolver, serverAddr)
       if err != nil {
           // handle error
       }
   
       listener, err := net.ListenTCP("tcp", tcpAddr)
    if err != nil {
        // handle error
    }
    
    // listen for an incoming connection
    conn, err := listener.Accept()
    if err != nil {
        // handle error
    }
    
    // send message
    if _, err := conn.Write({message}); err != nil {
        // handle error
    }    
    // receive message
    buf := make([]byte, 512)
    n, err := conn.Read(buf[0:])
    if err != nil {
        // handle error
    }
  • Connecting to a UDP socket

In contrast to a TCP socket, with a UDP socket, the client just sends a datagram to the server. There’s no Accept function, since the server doesn’t need to accept a connection and just waits for datagrams to arrive.

Other TCP functions have UDP counterparts; just replace TCP with UDP in the functions above.

Client side:

// init
    raddr, err := net.ResolveUDPAddr("udp", address)
    if err != nil {
        // handle error
    }
       
    conn, err := net.DialUDP("udp", nil, raddr)
    if err != nil {
        // handle error
    }
        ....... 
    // send message
    buffer := make([]byte, maxBufferSize)
    n, addr, err := conn.ReadFrom(buffer)
    if err != nil {
        // handle error
    }
         .......            
    // receive message
    buffer := make([]byte, maxBufferSize)
    n, err = conn.WriteTo(buffer[:n], addr)
    if err != nil {
        // handle error
    }

Server side:

    // init
    udpAddr, err := net.ResolveUDPAddr(resolver, serverAddr)
    if err != nil {
        // handle error
    }
    
    conn, err := net.ListenUDP("udp", udpAddr)
    if err != nil {
        // handle error
    }
        .......
    // send message
    buffer := make([]byte, maxBufferSize)
    n, addr, err := conn.ReadFromUDP(buffer)
    if err != nil {
        // handle error
    }
         .......
    // receive message
    buffer := make([]byte, maxBufferSize)
    n, err = conn.WriteToUDP(buffer[:n], addr)
    if err != nil {
        // handle error
    }

What WebSockets are

The WebSocket communication package provides a full-duplex communication channel over a single TCP connection. That means that both the client and the server can simultaneously send data whenever they need without any request.

WebSockets are a good solution for services that require continuous data exchange – for instance, instant messengers, online games, and real-time trading systems. You can find complete information about the WebSocket protocol in the Internet Engineering Task Force (IETF) RFC 6455 specification.

WebSocket connections are requested by browsers and are responded to by servers, after which a connection is established. This process is often called a handshake. The special kind of header in WebSockets requires only one handshake between a browser and server for establishing a connection that will remain active throughout its lifetime.

The WebSocket protocol uses port 80 for an unsecure connection and port 443 for a secure connection. The WebSocket specification determines which uniform resource identifier schemes are required for the ws (WebSocket) and wss (WebSocket Secure) protocols.

WebSockets solve many of the headaches of developing real-time web applications and have several benefits over traditional HTTP:

  • The lightweight header reduces data transmission overhead.
  • Only one TCP connection is required for a single web client.
  • WebSocket servers can push data to web clients.

The WebSocket protocol is relatively simple to implement. It uses the HTTP protocol for the initial handshake. After a successful handshake, a connection is established and the WebSocket essentially uses raw TCP to read/write data.

This is what a client request looks like:

  GET /chat HTTP/1.1
    Host: server.example.com
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
    Sec-WebSocket-Protocol: chat, superchat
    Sec-WebSocket-Version: 13
    Origin: http://example.com

And here’s the server response:

    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
    Sec-WebSocket-Protocol: chat

Comparing WebSocket with HTTP

To understand the benefits WebSocket provides to software developers, it’s worth comparing it with HTTP, the basic protocol used for communication on the internet. In a nutshell, WebSocket provides more opportunities by optimizing the process of communication. Let’s compare WebSocket- and HTTP-based connection types in detail.

HTTP WebSocket Explanation
Unidirectional connection Unidirectional connection WebSocket allows making the server “independent” from the client. It allows for avoiding delays caused when the server is not answering the request by getting real-time data about the request.
Connection is terminated after request/response Connection is kept alive until terminated by the client or server In HTTP, getting any updates from the server is only possible by making another request. WebSocket doesn’t allow for dropping the connection, thus ensuring continuous and effective communication.
HTTP data requests use simple RESTful API. They send a one-time state request for a query. Real-time data is received on a single communication channel and can be continuously updated With WebSocket, you get a single communication channel for different types of messages.
Best used for applications that don’t require quick, two-way connections. Best used for applications in need of quick connections and real-time data. As a result, WebSocket allows to speed up client-server communication, which provides better opportunities for creating complex apps based on real-time data and improved request-response process (examples of such are a multiplayer game or a messaging app).

WebSocket is a result of connection protocols’ evolution. Its advent is called to overcome the burdens that may have prevented developers from creating more complex and easy-to-use solutions.

So, let’s overview what it takes to create a WebSocket app in Golang.

How to create a WebSocket app in Go

To write a simple WebSocket echo server based on the net/http library, you need to:

  1. Initiate a handshake
  2. Receive data frames from the client
  3. Send data frames to the client
  4. Close the handshake

First, let’s create an HTTP handler with a WebSocket endpoint:

// HTTP server with WebSocket endpoint
        func Server() {
        http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
            ws, err := NewHandler(w, r)
            if err != nil {
                 // handle error
            }
            if err = ws.Handshake(); err != nil {
                // handle error
            }
        …

Then initialize the WebSocket structure. 

The initial handshake request always comes from the client. Once the server has defined a WebSocket request, it needs to reply with a handshake response. 

Bear in mind that you can’t write the response using the http.ResponseWriter, since it will close the underlying TCP connection once you start sending the response. 

So you need to use HTTP hijacking. Hijacking allows you to take over the underlying TCP connection handler and bufio.Writer. This gives you the possibility to read and write data without closing the TCP connection.

// NewHandler initializes a new handler
        func NewHandler(w http.ResponseWriter, req *http.Request) (*WS, error) {
        hj, ok := w.(http.Hijacker)
        if !ok {
            // handle error
        }                  .....
}

To complete the handshake, the server must respond with the appropriate headers.

// Handshake creates a handshake header
    func (ws *WS) Handshake() error {
        
        hash := func(key string) string {
            h := sha1.New()
            h.Write([]byte(key))
            h.Write([]byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
        return base64.StdEncoding.EncodeToString(h.Sum(nil))
        }(ws.header.Get("Sec-WebSocket-Key"))
      .....
}

“Sec-WebSocket-key” is generated randomly and is Base64-encoded. The server needs to append this key to a fixed string after accepting a request. Assume you have the x3JJHMbDL1EzLkh9GBhXDw== key. In this case, you can use SHA-1 to compute the binary value and use Base64 to encode it. You’ll get HSmrc0sMlYUkAGmm5OPpG2HaGWk=. Use this as the value of the Sec-WebSocket-Accept response header.

Transferring the data frame

When the handshake has been successfully completed, your app can read and write data from and to the client. The WebSocket specification defines a specific frame format that’s used between a client and a server. Here is the bit pattern of the frame:

Use the following code to decode the client payload: 

// Recv receives data and returns a Frame
    func (ws *WS) Recv() (frame Frame, _ error) {
        frame = Frame{}
        head, err := ws.read(2)
        if err != nil {
            // handle error
        }

In turn, these lines of code allow for encoding data: 

// Send sends a Frame
    func (ws *WS) Send(fr Frame) error {
        // make a slice of bytes of length 2
        data := make([]byte, 2)
    
        // Save fragmentation & opcode information in the first byte
        data[0] = 0x80 | fr.Opcode
        if fr.IsFragment {
            data[0] &= 0x7F
        }
        .....

Read also: Go vs Rust article

Closing a handshake

A handshake is closed when one of the parties sends a close frame with a close status as the payload. Optionally, the party sending the close frame can send a close reason in the payload. If closing is initiated by the client, the server should send a corresponding close frame in response.

// Close sends a close frame and closes the TCP connection
func (ws *Ws) Close() error {
f := Frame{}
f.Opcode = 8
f.Length = 2
f.Payload = make([]byte, 2)
binary.BigEndian.PutUint16(f.Payload, ws.status)
if err := ws.Send(f); err != nil {
return err
}
return ws.conn.Close()
}

List of WebSocket libraries

There are several third-party libraries that ease developers’ lives and greatly facilitate working with WebSockets.

  • STDLIB ( x/net/websocket)

This WebSocket library is part of the standard Go library. It implements a client and server for the WebSocket protocol, as described in the RFC 6455 specification. It doesn’t need to be installed and has good official documentation. On the other hand, it still lacks some features that can be found in other WebSocket libraries. Golang WebSocket implementations in the /x/net/websocket package do not allow users to reuse I/O buffers between connections in a clear way.

Let’s check how the STDLIB package works. Here’s an example of code for performing basic functions like creating a connection and sending and receiving messages.
First of all, to install and use this library, you should add this line of code:

import "golang.org/x/net/websocket"

Client side:

// create connection
// schema can be ws:// or wss://
// host, port – WebSocket server
conn, err := websocket.Dial("{schema}://{host}:{port}", "", op.Origin)
if err != nil {
// handle error
}
defer conn.Close()
.......
// send message
if err = websocket.JSON.Send(conn, {message}); err != nil {
// handle error
}
.......
// receive message
// messageType initializes some type of message
message := messageType{}
if err := websocket.JSON.Receive(conn, &message); err != nil {
// handle error
}
.......

Server side:

// Initialize WebSocket handler + server
mux := http.NewServeMux()
mux.Handle("/", websocket.Handler(func(conn *websocket.Conn) {
func() {
for {// do something, receive, send, etc.
}
}
.......
// receive message
// messageType initializes some type of message
message := messageType{}
if err := websocket.JSON.Receive(conn, &message); err != nil {
// handle error
}
.......
// send message
if err := websocket.JSON.Send(conn, message); err != nil {
// handle error
}
........

  • GORILLA

The WebSocket package in the Gorilla web toolkit boasts a complete and tested implementation of the WebSocket protocol as well as a stable package API. The WebSocket package is well-documented and easy to use. You can find documentation on the official Gorilla website.

Installation:

go get github.com/gorilla/websocket
Examples of code
Client side:
// init
// schema – can be ws:// or wss://
// host, port – WebSocket server
u := url.URL{
Scheme: {schema},
Host:   {host}:{port},
Path:   "/",
}
c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
if err != nil {
// handle error
}
.......
// send message
err := c.WriteMessage(websocket.TextMessage, {message})
if err != nil {
// handle error
}
.......
// receive message
_, message, err := c.ReadMessage()
if err != nil {
// handle error
}
.......

Server side:

// init
u := websocket.Upgrader{}
c, err := u.Upgrade(w, r, nil)
if err != nil {
// handle error
}
.......
// receive message
messageType, message, err := c.ReadMessage()
if err != nil {
// handle error
}
.......
// send message
err = c.WriteMessage(messageType, {message})
if err != nil {
// handle error
}
.......

Read also: What projects should you use Go for?

  • Gobwas

​​​​This tiny WebSocket package has a powerful list of features, such as zero-copy upgrading and a low-level API that allows for building custom packet handling logic. Gobwas requires no intermediate allocations during I/O. It also boasts high-level wrappers and helpers around the API in the wsutil package, allowing developers to start fast without digging into the internals of the protocol. This library has a flexible API, but it comes at the cost of usability and clarity.

You can check the GoDoc website for documentation. You can install Gobwas by including the following line of code:

go get github.com/gobwas/ws

Client side:

// init
// schema – can be ws or wss
// host, port – ws server
conn, _, _, err := ws.DefaultDialer.Dial(ctx, {schema}://{host}:{port})
if err != nil {
// handle error
}
.......
// send message
err = wsutil.WriteClientMessage(conn, ws.OpText, {message})
if err != nil {
// handle error
}

//.......
// receive message
msg, _, err := wsutil.ReadServerData(conn)
if err != nil {
// handle error
.......
}

Server side:

// init
listener, err := net.Listen("tcp", op.Port)
if err != nil {
// handle error
}
conn, err := listener.Accept()
if err != nil {
// handle error
}
upgrader := ws.Upgrader{}
if _, err = upgrader.Upgrade(conn); err != nil {
// handle error
}
.......
// receive message
for {
reader := wsutil.NewReader(conn, ws.StateServerSide)
_, err := reader.NextFrame()
if err != nil {
// handle error
}
data, err := ioutil.ReadAll(reader)
if err != nil {
// handle error
}
.......
}
.......
// send message
msg := "new server message"
if err := wsutil.WriteServerText(conn, {message}); err != nil {
// handle error
}
.......

  • GOWebsockets

This tool offers a wide range of easy-to-use features. It allows for concurrency control, data compression, and setting request headers. GOWebsockets supports proxies and subprotocols for emitting and receiving text and binary data. Developers can also enable or disable SSL verification.

You can find documentation for and examples of how to use GOWebsockets on the GoDoc website and on the project’s GitHub page. Install the package by adding the following line of code:

go get github.com/sacOO7/gowebsocket

Client side:

// init
// schema – can be ws or wss
// host, port – ws server
socket := gowebsocket.New({schema}://{host}:{port})
socket.Connect()
.......
// send message
socket.SendText({message})
or
socket.SendBinary({message})
.......
// receive message
socket.OnTextMessage = func(message string, socket gowebsocket.Socket) {
// hande received message
};
or
socket.OnBinaryMessage = func(data [] byte, socket gowebsocket.Socket) {
// hande received message
};
.......

Server side:

// init
// schema – can be ws or wss
// host, port – ws server
conn, _, _, err := ws.DefaultDialer.Dial(ctx, {schema}://{host}:{port})
if err != nil {
// handle error
}
.......
// send message
err = wsutil.WriteClientMessage(conn, ws.OpText, {message})
if err != nil {
// handle error
}
.......
// receive message
msg, _, err := wsutil.ReadServerData(conn)
if err != nil {
// handle error
}

Comparing existing solutions

We’ve described four of the most widely used WebSocket libraries for Golang. The table below contains a detailed comparison of these tools.

To better analyze their performance, we also conducted a couple of benchmarks.

Read also: Monitoring the Performance of Your Go Application: Why and How You Should Do It

The results are the following:

Read also: Best Practices for Speeding Up JSON Encoding and Decoding in Go

  • As you can see, Gobwas has a significant advantage over other libraries. It has fewer allocations per operation and uses less memory and time per allocation. Plus, it has zero I/O allocation. Besides, Gobwas has all the methods you need to create WebSocket client–server interactions and receive message fragments. You can also use it to easily work with TCP sockets.
  • If you really don’t like Gobwas, you can use Gorilla. It’s quite simple and has almost all the same features. You can also use STDLIB, but it’s not as good in production because it lacks many necessary features and, as you can see in the benchmarks, offers weaker performance. GOWebsocket is about the same as STDLIB. But if you need to quickly build a prototype or MVP, it can be a reasonable choice.

Besides these tools, there are also several alternative implementations that allow you to build powerful streaming solutions. Among them are:

The constant development of streaming technologies and the availability of well-documented tools such as WebSockets make it easy for developers to create truly real-time applications. Write us if you need advice on or help with creating a real-time app using WebSockets. We hope this tutorial helped you a lot.

What to develop a real-time communication solution?

We are eager to help you with that

Check our expertise

Rate this article

Share this article

4.4/5.0

based on 918 reviews