WebSocket in Go and a gorilla here

WebSocket in Go and a gorilla here

gorilla

Hello, Habre!

WebSocket allows you to open an interactive communication session between the browser and the server. This is a big difference from traditional HTTP, which is limited to a request-response model and is not suitable for scenarios that require constant data exchange

Go, with its simplicity and support for concurrency, makes it a good candidate to work with WebSockets.

The value of Go concurrency for working with WebSockets.

Goroutines are lightweight threads of execution managed by the Go runtime. They are significantly more efficient than traditional operating system threads due to lower memory consumption and lower overhead to create and manage them. Coroutines allow you to write asynchronous code that can handle multiple connections or tasks simultaneously without blocking or incurring significant overhead.

Channels in Go is a means of exchanging data between goroutines that provides synchronization without the explicit use of locks or state conditions. They provide a safe and convenient way to pass messages between goroutines, which is important for real-time data processing, as in the case of WebSockets.

WebSocket servers often require simultaneous processing of many active connections. Each WebSocket connection requires constant activity to maintain real-time communication and data exchange. Using goroutines, Go allows you to manage multiple parallel connections, where each WebSocket connection can be handled by a separate goroutine. This provides very good performance and responsiveness with minimal headroom.

WebSocket provides two-way communication, which can come from both the client and the server. The asynchronous nature of goroutines makes it easy to implement the processing of incoming and outgoing messages simultaneously. Go channels can be used to pass messages between connection-handling goroutines and application business logic.

WebSocket server on Go

It assumes you already have golang ^^

Create a new directory for your project and initialize it as a Go module using the command go mod init your_project_name. This will create a new file go.modyour project’s dependency manager.

About the gorilla

To work with WebSocket, we will use the pops library gorilla/websocket. Yes, that’s what the gorilla is for. A little about her:

websocket.Upgrader used to update an HTTP connection to the WebSocket protocol. This is the main component of creating a WebSocket server. It allows you to configure various settings such as read and write buffer sizes, output request validation, and other security options.

websocket.Conn there is a WebSocket connection. This type provides interfaces for reading and writing WebSocket messages. It supports text and binary messages and allows you to control details such as timeout, connection closing and ping/pong management.

A little about ways with these features:

Upgrader.Upgrade used to convert an HTTP request to a WebSocket connection. This method returns *websocket.Conn and is used on the server side to initiate a WebSocket session.

Conn.ReadMessage() and Conn.WriteMessage() are used to read and write messages. ReadMessage blocks the calling thread until a message is received or an error occurs. WriteMessage used to send messages to the client.

NextWriter and NextReaderprovide lower-level access to WebSocket read and write streams. NextWriter returns the writer for the next message, a NextReader obviously returns a reader to read the next message.

Also:

The library supports connection closure management, including sending and processing the appropriate WebSocket control messages. The library supports ping/pong handlers to keep the connection active and determine its state.

Supports compression of WebSocket messages, which can be useful for reducing the amount of data transferred.

Allows you to manage data frames.

To add it to your project, run the command go get github.com/gorilla/websocket.

Learn more about the library on GitHub.

Let’s write the code to create a simple server

We import our gorilla:

import (
    "net/http"
    "github.com/gorilla/websocket"
)

websocket.Upgraderwill be used to update HTTP connections to the WebSocket protocol

var upgrader = websocket.Upgrader{
    ReadBufferSize:  1024,
    WriteBufferSize: 1024,
}

Let’s create a function that will handle incoming WebSocket connections:

func handleConnections(w http.ResponseWriter, r *http.Request) {
    // обновление соединения до WebSocket
    ws, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Fatal(err)
    }
    defer ws.Close()

    // цикл обработки сообщений
    for {
        messageType, message, err := ws.ReadMessage()
        if err != nil {
            log.Println(err)
            break
        }
        log.Printf("Received: %s", message)

        // эхо ансвер
        if err := ws.WriteMessage(messageType, message); err != nil {
            log.Println(err)
            break
        }
    }
}

Let’s register the function handleConnections as a route handler:

func main() {
    http.HandleFunc("/ws", handleConnections)
    log.Println("http server started on :8000")
    err := http.ListenAndServe(":8000", nil)
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

How to test?

We execute the command go run your_project_name.go to start the server and verify that the server starts without errors and is available on http://localhost:8000/ws.

You can also use a WebSocket client, such as a browser extension, to connect to your server and send messages.

Support

A WebSocket connection starts with an HTTP request, which is then “upgraded” to the WebSocket protocol. This process is called “Handshake”. The initial request must conform to WebSocket standards, including the correct headers (Upgrade: websocket and Connection: Upgrade).

On the server side, it is important to validate the field Origin in the HTTP request to prevent Cross-Site WebSocket Hijacking attacks.

Using gorilla in Go, you can configure connection parameters, including buffer sizes, timeouts, and compression mechanisms:

Let’s configure the Upgrader:

import (
    "net/http"
    "github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
    ReadBufferSize:  1024,  // Размер буфера чтения
    WriteBufferSize: 1024,  // Размер буфера записи
    // Позволяет определить, должен ли сервер сжимать сообщения
    EnableCompression: true,
}

func handleUpgrade(w http.ResponseWriter, r *http.Request) {
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        // обработка ошибки
        return
    }
    defer conn.Close()
    // дальнейшая обработка соединения
}

upgraderwhich is used to convert HTTP requests in a WebSocket connection

Timeouts:

func handleConnections(conn *websocket.Conn) {
    // Установка таймаута для чтения сообщения
    conn.SetReadDeadline(time.Now().Add(60 * time.Second))
    
    for {
        _, message, err := conn.ReadMessage()
        if err != nil {
            // обработка ошибки
            break
        }
        // обработка сообщения

        // Обновление таймаута после успешного чтения сообщения
        conn.SetReadDeadline(time.Now().Add(60 * time.Second))
    }
}

We set the time for reading operations on the WebSocket connection. SetReadDeadline is used to specify the time after which the connection will be closed if no new message is received.

WebSocket supports control framessuch as ping and pong.

Sending pings from the server to the client helps ensure that the client is still connected and active.

How it can be implemented:

On the client side, you need to set up a ping handler that will respond with pongs:

conn.SetPingHandler(func(appData string) error {
    return conn.WriteControl(websocket.PongMessage, []byte(appData), time.Now().Add(writeWait))
})

On the server, you can implement a goroutine that will periodically send pings to clients:

ticker := time.NewTicker(pingPeriod)
defer ticker.Stop()

for {
    select {
    case <-ticker.C:
        conn.SetWriteDeadline(time.Now().Add(writeWait))
        if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
            return // или обработать ошибку
        }
    }
}

Pongs are used in response to pings and help the server know that the client is still connected.

On the server, you can configure a pong handler to update the connection state:

conn.SetPongHandler(func(string) error { conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })

To protect the data transmitted by WebSocket, you should use WSS (the HTTPS equivalent of WebSocket), which provides data encryption. On the server, you should set limits on the number of simultaneously open connections, the size of received messages, and other parameters to protect against overloads and attacks.

About scaling

WebSocket supports persistent connections. However, when scaling, it is very important to manage these compounds. In Go, this is usually achieved by using goroutines for each connection:

package main

import (
    "net/http"
    "github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
    ReadBufferSize:  1024,
    WriteBufferSize: 1024,
}

func handler(w http.ResponseWriter, r *http.Request) {
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        return
    }
    defer conn.Close()

    go handleConnection(conn)
}

func handleConnection(conn *websocket.Conn) {
    for {
        messageType, p, err := conn.ReadMessage()
        if err != nil {
            return
        }
        // обработка сообщения...
    }
}

func main() {
    http.HandleFunc("/ws", handler)
    http.ListenAndServe(":8080", nil)
}

When scaling a WebSocket server in Go, it is necessary to ensure the processing of incoming and outgoing traffic. This may include the use of buffering, asynchronous data sending/receiving, and error handling:

func handleConnection(conn *websocket.Conn) {
    for {
        // Чтение месседжа
        _, message, err := conn.ReadMessage()
        if err != nil {
            break
        }

        // асинхрон отправка
        go func(msg []byte) {
            err = conn.WriteMessage(websocket.TextMessage, msg)
            if err != nil {
                return
            }
        }(message)
    }
}

Coroutines and channels are a nice asynchronous tool:

func handleConnection(conn *websocket.Conn) {
    msgChan := make(chan []byte)

    go func() {
        for {
            message, ok := <-msgChan
            if !ok {
                return
            }
            conn.WriteMessage(websocket.TextMessage, message)
        }
    }()

    for {
        _, message, err := conn.ReadMessage()
        if err != nil {
            close(msgChan)
            break
        }
        msgChan <- message
    }
}

Intermediate layers are often used to manage authentication, logging, speed limiting:

func loggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        log.Println("Получен запрос:", r.URL)
        next.ServeHTTP(w, r)
    }
}

func main() {
    http.HandleFunc("/ws", loggingMiddleware(handler))
    http.ListenAndServe(":8080", nil)


}

WebSocket in Go has many possibilities. This allows you to create interactive and responsive real-time programs. Go is a good choice for websockets, due to its performance, competitive features, and ease of integration.

Gorilla is not only a powerful animal, but also a good tool in your developer arsenal.

My colleagues from OTUS talk about other, no less useful tools in online courses. I also remind you that in the events calendar you can register for a number of useful free webinars.

Related posts