Mastering Gorilla WebSockets

We're about to dive headfirst into the exciting world of Gorilla WebSockets.

Mastering Gorilla WebSockets

If you've ever wondered how to supercharge your web applications with real-time interactivity, you're in the right place, but Before we do that let us Understand What WebSockets

The WebSocket Protocol :

The WebSocket Protocol enables two-way communication between a client running code and a remote host that has agreed to communicate through that code.

How does the connection Begin?

The protocol Starts with an opening handshake which is followed by basic message framing, layered over TCP.

Why Should I Use Websocket?

The goal of Websockets is to provide a mechanism for Web-based applications that need two-way communication with servers that do not rely on opening multiple HTTP connections.

well...If that doesn't sound convincing to you

Let's take a practical example

Historically, creating web applications that need bidirectional communication between a client and a server

Say for example: instant messaging, Online Games, stock tickers and multiuser applications with simultaneous editing have required an abuse of HTTP to request the server for updates while sending upstream notifications as separate HTTP calls

This results in a variety of problems:

  1. The server is forced to use several different underlying TCP connections for each client: one for sending information to the client and a new one for each incoming message.

  2. The wire protocol has a high overhead, with each client-to-server message having an HTTP header.

  3. The client-side script is forced to maintain a mapping from the outgoing connections to the incoming connection to track replies.

A simpler solution would be to use a single TCP connection for traffic in both directions. This is what the WebSocket Protocol provides. Combined with the WebSocket API, it provides an alternative to HTTP polling for two-way communication from a web page to a remote server.

Gorilla WebSocket

There are 2 ways in which a Websocket connection can be established

1) Upgrade

Upgrade upgrades the HTTP server connection to the WebSocket protocol.

func WsHandler(w http.ResponseWriter, r *http.Request) {
    WsConnection, err := upgrader.Upgrade(w, r, nil)
//for sake of simplicity i'll refer to WsConnection as conn
    if err != nil {
        log.Println(err) //standard error handler
        return
    } 
//From here we can use the WsConnection to read or write

}

ReadBufferSize and WriteBufferSize specify [Input/Output] buffer sizes in bytes. If a buffer size is zero, then buffers allocated by the HTTP server are used. An interesting thing to note is that the I/O buffer sizes do not limit the size of the messages that can be sent or received.The buffer sizes just control How much data is buffered before a read/write system call and How much data is buffered before being flushed to the network.

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

How does the Upgrade take place?

For the WebSocket connection to be firmly established, there are a number of internal function calls that happen. If I filter out Some pretty important error handlers and come back to the main exoskeleton, these functions are the backbone of upgrade

func (u *Upgrader) Upgrade(w http.ResponseWriter,
 r *http.Request, responseHeader http.Header)
 (*Conn, error) {

h, ok := w.(http.Hijacker) //First step

var brw *bufio.ReadWriter
    netConn, brw, err := h.Hijack() //Second step

Hijack lets the caller take over the connection(This is the crucial part). After a call to Hijack the HTTP server library will not do anything else with the connection.

It becomes the caller's responsibility to manage and close the connection. The returned net.Conn may have read or write deadlines already set, depending on the configuration of the Server. It is the caller's responsibility to set or clear those deadlines as needed.

var br *bufio.Reader
    if u.ReadBufferSize == 0 && bufioReaderSize(netConn, brw.Reader) > 256 {

        br = brw.Reader
    }

    buf := bufioWriterBuffer(netConn, brw.Writer)

var br *bufio.Reader : Declares a Reader variable.

if u.ReadBufferSize == 0 && bufioReaderSize(netConn, brw.Reader) > 256 {...xyz} : If no read buffer size is configured and the hijacked Reader is large enough, it reuses that Reader instead of allocating a new one.

buf := bufioWriterBuffer(netConn, brw.Writer) : Gets the buffer from the hijacked Writer.

var writeBuf []byte
if u.WriteBufferPool == nil && u.WriteBufferSize == 0 && len(buf) >= maxFrameHeaderSize+256 {
writeBuf = buf
}

var writeBuf []byte: Declares a byte slice for the write buffer.

if u.WriteBufferPool == nil ... { ... }: If no write buffer pool is configured and the hijacked write buffer is large enough, it reuses that buffer instead of allocating a new one.

c := newConn(netConn, //Final step
 true, u.ReadBufferSize,
 u.WriteBufferSize,
 u.WriteBufferPool,
 br, writeBuf)
func newConn{//not including the args cuz they're too long
mu := make(chan struct{}, 1)
    mu <- struct{}{}
    c := &Conn{
        isServer:               isServer,
        br:                     br,
        conn:                   conn,
        mu:                     mu,
        readFinal:              true,
        writeBuf:               writeBuf,
        writePool:              writeBufferPool,
        writeBufSize:           writeBufferSize,
        enableWriteCompression: true,
        compressionLevel:       defaultCompressionLevel,
    }
return c
}

to Visualize the Upgrade Method:

2) Dialer:

Here is how the Dialer works :

The Dialer is used to establish a WebSocket connection to a server. It is defined in the gorilla/websocket package as:

  type Dialer struct {
     NetDial     func(net, addr string) (net.Conn, error)  
     Proxy       func(req *http.Request) (*url.URL, error)
     HandshakeTimeout time.Duration
     TLSClientConfig   *tls.Config   
     Subprotocols     []string  
     ReadBufferSize   int
     WriteBufferSize  int  
     WriteTimeout     time.Duration  
     HandleHTTP       bool
  }

The NetDial field specifies the function used to connect to the WebSocket server. By default it uses net.Dial.

The Proxy field specifies an HTTP proxy function to use when connecting.

The HandshakeTimeout field specifies the duration to wait for a WebSocket handshake to complete.

The TLSClientConfig field specifies the TLS configuration to use for secure connections.

The Subprotocols field specifies list of supported subprotocols.

The BufferSize fields specify the read and write buffer sizes.

The WriteTimeout specifies the duration to wait for a write operation to the server to complete.

The HandleHTTP field specifies if the Dialer should handle regular HTTP connections, false is the default value.

To connect to a WebSocket server, you call the Dial() method on the Dialer, passing the URL of the WebSocket server:

  dialer := websocket.Dialer{}
  conn, _, err := dialer.Dial("ws://localhost:8080", nil)
  if err != nil {
     // handle error
  }
  defer conn.Close()

Dial() uses the Dialer configuration

  1. It creates an HTTP request for the WebSocket URL

  2. The request may be rewritten if a Proxy is used

  3. It then makes a TCP connection to the server

  4. Then attempts to upgrade this to a WebSocket connection

  5. If the handshake fails, an error is returned

  6. If it succeeds, a WebSocket connection is returned

  7. Read and write listeners are started on the connection

  8. Client can now send/receive messages over the socket

So Dial() handles the connection setup, while the returned WebSocketConnection enables communication.

Visualize it this way

Ping & Pong (not the game:p)

The Ping and Pong messages are used to keep the WebSocket connection alive.

The client ( or the browser) will send a Ping message to the server periodically. The server will then respond with a Pong message. This has two purposes:

  1. It checks that the connection is still open and active. If the server does not receive any Ping messages for some time, it understands that the connection has been closed or lost.

  2. It keeps the connection alive. TCP connections have an idle timeout, and if no data is sent for a while the connection will be closed. Sending Ping/Pong messages regularly prevents the connection from timing out.

The implementation for ping|pong is as follows:

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

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

    // Set ping handler
    upgrader.SetPingHandler(func(appData string) error {
        log.Printf("Received ping from client!")
        return nil
    })

    // Set pong handler
    upgrader.SetPongHandler(func(appData string) error {
        log.Printf("Received pong from client!")
        return nil
    })

    // Send ping every 10 seconds
    go func() {
        for {
            conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(time.Second*10))
        }
    }()
}

Difference between Upgrade and Dial

  • Use the dialer when you want to establish a completely new WebSocket connection to a URL.

  • Use the upgrader when you have an already existing HTTP connection, for example from an HTTP request to your server, and you want to upgrade that connection to WebSocket.

Reading & Writing Methods for Websockets

Reading:

conn.ReadMessage()

  • Used to read a complete message from the connection.

  • It handles details like fragmentation and masking transparently.

  • Returns the message payload as bytes and the message type (text or binary).

  • Blocks until the full message is received or error happens.

conn.NextReader()

  • Returns an io.Reader to read the next message payload incrementally.

  • The reader handles fragmentation and masking.

  • Allows reading the message in chunks/streams instead of all at once.

  • Returns io.EOF error when message payload is fully read.

Writing:

conn.WriteMessage()

  • Writes a message to the connection.

  • Handles fragmentation if the message exceeds max frame size.

  • Accepts the message payload as bytes and the message type.

  • Blocks until the message is fully written.

conn.NextWriter()

  • Returns an io.Writer to write the next message incrementally.

  • The writer handles fragmentation automatically.

  • Allows writing the message payload in chunks/streams.

  • Returns nil error after message is fully written.

conn.WriteControl()

  • Used to write control frames like ping, pong, close.

  • Accepts the control frame type and payload.

  • Write is completed immediately.

Additionally, there are some convenience methods like ReadJSON() and WriteJSON() for structured data.

refer: https://pkg.go.dev/github.com/gorilla/websocket#Conn.ReadJSON

and : https://pkg.go.dev/github.com/gorilla/websocket#Conn.WriteJSON

Closing a WebSocket connection

You might be wondering "why should I bother closing a WebSocket connection ?"

Well let me give you 3 points on why it is necessary to close a WebSocket connection:

  1. Releases resources - WebSocket connections hold onto resources like TCP connections and memory. By closing the connection, these resources are released and made available again for other uses.

  2. Following standard protocol - The WebSocket protocol specifies that connections should be closed gracefully using proper close frames. Closing the connection ensures you follow the protocol.

  3. Allows for clean reconnect - If your application needs to reconnect to the WebSocket, a graceful close allows it to smoothly reconnect without any issues. However, an abrupt disconnection can cause problems reconnecting.

Methods for closing a ws Connection:

close() && closeHandler

These are two functions used for closing a WebSocket connection:

close():

  • This is the method to gracefully close a WebSocket connection. It sends a close frame to the server, indicating a successful close-down of the connection. it's syntax is:

      conn.Close()
    

    closeHandler:

  • This is a handler function that is called when the server closes the WebSocket connection. We can register a close handler using:

      conn.SetCloseHandler(func(code int, text string) error {
         // Handle close and perform logging and cleanup
      }
    

The close handler receives the close code and text sent by the server, and this is the place where we can perform any cleanup or logging.

An example of close handler:

  conn.SetCloseHandler(func(code int, text string) error {
     log.Println("Connection closed with code:", code, "and reason:", text)
     return nil
  })

So in summary:

  • close() - Gracefully closes the connection by sending a close frame

  • closeHandler - Handles the closure of the connection initiated by the server

Note: Calling close() will also trigger the execution of the closeHandler if one is registered.

Conclusion:

In a nutshell, Gorilla WebSockets is like a trusty sidekick for web developers. It's an easy to use and dependable library that takes the trouble out of adding WebSocket functionality to real-time web applications. With its excellent performance and wide compatibility. Whether you're building a chat app, diving into game development, or solving any real-time project, Gorilla WebSockets is your go-to companion in the go-ecosystem.

until next time......

Happy coding with Go!