Mastering Gorilla WebSockets
We're about to dive headfirst into the exciting world of Gorilla WebSockets.
Table of contents
- The WebSocket Protocol :
- well...If that doesn't sound convincing to you
- Gorilla WebSocket
- 1) Upgrade
- How does the Upgrade take place?
- to Visualize the Upgrade Method:
- 2) Dialer:
- Visualize it this way
- Ping & Pong (not the game:p)
- Difference between Upgrade and Dial
- Reading & Writing Methods for Websockets
- Reading:
- conn.ReadMessage()
- conn.NextReader()
- Writing:
- conn.WriteMessage()
- conn.NextWriter()
- conn.WriteControl()
- Closing a WebSocket connection
- Methods for closing a ws Connection:
- close() && closeHandler
- close():
- closeHandler:
- Conclusion:
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:
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.
The wire protocol has a high overhead, with each client-to-server message having an HTTP header.
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
It creates an HTTP request for the WebSocket URL
The request may be rewritten if a Proxy is used
It then makes a TCP connection to the server
Then attempts to upgrade this to a WebSocket connection
If the handshake fails, an error is returned
If it succeeds, a WebSocket connection is returned
Read and write listeners are started on the connection
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:
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.
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:
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.
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.
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 framecloseHandler
- 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......