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.
Read also: Best Practices for Speeding Up JSON Encoding and Decoding in Go
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
How to create a WebSocket app in Go
To write a simple WebSocket echo server based on the net/http library, you need to:
- Initiate a handshake
- Receive data frames from the client
- Send data frames to the client
- 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
}
.....
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:
- 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:
-
go-socket.io
-
Apache Thrift
-
gRPC
-
Package rpc
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.
Ten articles before and after
Which Tool Your Logistics App Needs: Mapbox or Google Maps Platform
Top Reasons for Cloud Migration and Step-by-Step Instructions
Six Cloud Migration Strategies Based on Yalantis’ Experience
Full List of the Best Android App Development Tools in 2021
How to Choose the Best-Fit Tools for Logistics Software Optimization
Eight Tips on How to Ensure Core Data Migration