Skip to main content

Gin WebSockets

Introduction

WebSockets provide a powerful way to establish persistent, bidirectional communication channels between clients and servers. Unlike traditional HTTP requests that follow a request-response pattern, WebSockets allow data to flow in both directions simultaneously, making them ideal for real-time applications like chat systems, live dashboards, and multiplayer games.

In this tutorial, we'll explore how to integrate WebSockets with the Gin web framework in Go. You'll learn how to establish WebSocket connections, handle messages, and build a simple real-time application.

Prerequisites

Before we begin, make sure you have:

  1. Basic knowledge of Go programming
  2. Familiarity with the Gin framework
  3. Go installed on your system
  4. The Gin framework installed: go get -u github.com/gin-gonic/gin
  5. The Gorilla WebSocket library: go get -u github.com/gorilla/websocket

Understanding WebSockets

WebSockets are an advanced technology that enables interactive communication between a client (typically a web browser) and a server. Key characteristics include:

  • Persistent Connection: Once established, the connection remains open until explicitly closed
  • Full-Duplex Communication: Data can flow in both directions simultaneously
  • Low Latency: Minimal overhead compared to repeated HTTP requests
  • Real-Time Updates: Ideal for applications requiring instant data updates

Setting Up WebSockets with Gin

Let's start by creating a basic WebSocket server using Gin:

go
package main

import (
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"net/http"
"log"
)

var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
// Allow all origins for now (not recommended for production)
CheckOrigin: func(r *http.Request) bool {
return true
},
}

func handleWebSocket(c *gin.Context) {
// Upgrade HTTP connection to WebSocket
ws, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Printf("Failed to upgrade connection: %v", err)
return
}
defer ws.Close()

// WebSocket event loop
for {
messageType, message, err := ws.ReadMessage()
if err != nil {
log.Printf("Error reading message: %v", err)
break
}

log.Printf("Received message: %s", message)

// Echo the message back to the client
if err := ws.WriteMessage(messageType, message); err != nil {
log.Printf("Error writing message: %v", err)
break
}
}
}

func main() {
r := gin.Default()

// Serve the HTML file for WebSocket client
r.GET("/", func(c *gin.Context) {
c.File("./index.html")
})

// WebSocket endpoint
r.GET("/ws", handleWebSocket)

r.Run(":8080")
}

Now, let's create a simple HTML file (index.html) to test our WebSocket server:

html
<!DOCTYPE html>
<html>
<head>
<title>Gin WebSocket Example</title>
<style>
body {
max-width: 800px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
#messages {
border: 1px solid #ccc;
height: 300px;
margin-bottom: 10px;
overflow-y: scroll;
padding: 10px;
}
input, button {
padding: 8px;
margin-right: 5px;
}
</style>
</head>
<body>
<h1>Gin WebSocket Example</h1>
<div id="messages"></div>
<input type="text" id="messageInput" placeholder="Type a message...">
<button onclick="sendMessage()">Send</button>

<script>
let ws;
const messagesDiv = document.getElementById('messages');
const messageInput = document.getElementById('messageInput');

function connect() {
// Create WebSocket connection
ws = new WebSocket('ws://' + window.location.host + '/ws');

// Connection opened
ws.onopen = function(e) {
addMessage('System', 'Connection established');
};

// Listen for messages
ws.onmessage = function(e) {
addMessage('Server', e.data);
};

// Connection closed
ws.onclose = function(e) {
addMessage('System', 'Connection closed');
// Try to reconnect after 1 second
setTimeout(connect, 1000);
};

// Connection error
ws.onerror = function(e) {
addMessage('System', 'Error occurred');
};
}

function sendMessage() {
if (ws && messageInput.value) {
ws.send(messageInput.value);
addMessage('You', messageInput.value);
messageInput.value = '';
}
}

function addMessage(sender, message) {
const messageElement = document.createElement('div');
messageElement.innerHTML = `<strong>${sender}:</strong> ${message}`;
messagesDiv.appendChild(messageElement);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}

// Connect when page loads
window.onload = connect;

// Allow sending message with Enter key
messageInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
sendMessage();
}
});
</script>
</body>
</html>

How It Works

Let's break down what happens in our WebSocket implementation:

  1. Upgrader Setup: We configure a WebSocket upgrader with appropriate buffer sizes and origin checking.

  2. Connection Upgrade: When a client connects to the /ws endpoint, we upgrade the HTTP connection to a WebSocket connection.

  3. Event Loop: We enter a continuous loop to:

    • Read incoming messages from the client
    • Process the messages (in this example, we simply log them)
    • Send responses back to the client
  4. Client-Side: The HTML file establishes a WebSocket connection, sends messages, and displays responses.

Enhanced WebSocket Example: Real-time Chat Application

Let's build a more practical example: a simple chat application where multiple users can communicate in real-time.

go
package main

import (
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"net/http"
"log"
"sync"
)

var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}

// Client represents a connected user
type Client struct {
ID string
Conn *websocket.Conn
Username string
}

// ChatServer manages active clients and broadcasts messages
type ChatServer struct {
clients map[*Client]bool
register chan *Client
unregister chan *Client
broadcast chan []byte
mutex sync.Mutex
}

// NewChatServer creates a new chat server
func NewChatServer() *ChatServer {
return &ChatServer{
clients: make(map[*Client]bool),
register: make(chan *Client),
unregister: make(chan *Client),
broadcast: make(chan []byte),
}
}

// Start begins the chat server's main loop
func (s *ChatServer) Start() {
for {
select {
case client := <-s.register:
s.mutex.Lock()
s.clients[client] = true
s.mutex.Unlock()
log.Printf("Client connected: %s", client.Username)

// Notify all clients about new user
message := []byte("User " + client.Username + " joined the chat")
s.broadcast <- message

case client := <-s.unregister:
s.mutex.Lock()
if _, ok := s.clients[client]; ok {
delete(s.clients, client)
close := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")
client.Conn.WriteMessage(websocket.CloseMessage, close)
client.Conn.Close()
}
s.mutex.Unlock()
log.Printf("Client disconnected: %s", client.Username)

// Notify all clients about user leaving
message := []byte("User " + client.Username + " left the chat")
s.broadcast <- message

case message := <-s.broadcast:
s.mutex.Lock()
for client := range s.clients {
err := client.Conn.WriteMessage(websocket.TextMessage, message)
if err != nil {
log.Printf("Error broadcasting to client: %v", err)
client.Conn.Close()
delete(s.clients, client)
}
}
s.mutex.Unlock()
}
}
}

func main() {
chatServer := NewChatServer()
go chatServer.Start()

r := gin.Default()

r.GET("/", func(c *gin.Context) {
c.File("./chat.html")
})

r.GET("/ws", func(c *gin.Context) {
username := c.Query("username")
if username == "" {
username = "Anonymous"
}

handleChatConnection(c, chatServer, username)
})

r.Run(":8080")
}

func handleChatConnection(c *gin.Context, server *ChatServer, username string) {
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Printf("Failed to set websocket upgrade: %+v", err)
return
}

client := &Client{
ID: c.Request.RemoteAddr,
Conn: conn,
Username: username,
}

server.register <- client

// Read messages from client
go func() {
defer func() {
server.unregister <- client
}()

for {
_, message, err := conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
log.Printf("error: %v", err)
}
break
}

// Format message with username
formattedMsg := []byte(username + ": " + string(message))
server.broadcast <- formattedMsg
}
}()
}

Now, let's create a more advanced HTML client for our chat application (chat.html):

html
<!DOCTYPE html>
<html>
<head>
<title>Gin WebSocket Chat</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}

#chatContainer {
display: none;
}

#loginContainer {
text-align: center;
margin-top: 100px;
}

#messages {
height: 400px;
border: 1px solid #ccc;
padding: 10px;
overflow-y: scroll;
margin-bottom: 10px;
background: #f9f9f9;
}

.message {
margin-bottom: 8px;
padding: 5px;
}

.system-message {
color: #666;
font-style: italic;
}

.input-container {
display: flex;
}

#messageInput {
flex-grow: 1;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px 0 0 4px;
}

button {
padding: 10px 15px;
background: #4CAF50;
color: white;
border: none;
cursor: pointer;
border-radius: 0 4px 4px 0;
}

#usernameInput {
padding: 10px;
margin-right: 5px;
}
</style>
</head>
<body>
<div id="loginContainer">
<h1>Welcome to Chat</h1>
<input type="text" id="usernameInput" placeholder="Enter your username">
<button onclick="login()">Join Chat</button>
</div>

<div id="chatContainer">
<h1>Real-time Chat</h1>
<div id="messages"></div>
<div class="input-container">
<input type="text" id="messageInput" placeholder="Type your message...">
<button onclick="sendMessage()">Send</button>
</div>
</div>

<script>
let ws;
let username = '';
const messagesDiv = document.getElementById('messages');
const messageInput = document.getElementById('messageInput');
const loginContainer = document.getElementById('loginContainer');
const chatContainer = document.getElementById('chatContainer');

function login() {
username = document.getElementById('usernameInput').value.trim();
if (username === '') {
username = 'Anonymous';
}

loginContainer.style.display = 'none';
chatContainer.style.display = 'block';

connect();
}

function connect() {
const wsUrl = 'ws://' + window.location.host + '/ws?username=' + encodeURIComponent(username);
ws = new WebSocket(wsUrl);

ws.onopen = function(e) {
addSystemMessage('Connected to chat server');
};

ws.onmessage = function(e) {
addMessage(e.data);
};

ws.onclose = function(e) {
addSystemMessage('Disconnected from server. Trying to reconnect...');
setTimeout(connect, 1000);
};

ws.onerror = function(e) {
addSystemMessage('Error occurred');
console.error('WebSocket error:', e);
};
}

function sendMessage() {
if (ws && messageInput.value) {
ws.send(messageInput.value);
messageInput.value = '';
}
}

function addMessage(message) {
const messageElement = document.createElement('div');
messageElement.className = 'message';
messageElement.textContent = message;
messagesDiv.appendChild(messageElement);
scrollToBottom();
}

function addSystemMessage(message) {
const messageElement = document.createElement('div');
messageElement.className = 'message system-message';
messageElement.textContent = message;
messagesDiv.appendChild(messageElement);
scrollToBottom();
}

function scrollToBottom() {
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}

messageInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
sendMessage();
}
});
</script>
</body>
</html>

How the Chat Application Works

  1. ChatServer: Manages all client connections and handles broadcasting messages

    • clients: A map of all connected clients
    • register: Channel for new clients
    • unregister: Channel for disconnecting clients
    • broadcast: Channel for messages to be sent to all clients
  2. Client Management:

    • When a user connects, they're registered and other users are notified
    • When a user disconnects, they're unregistered and other users are notified
  3. Message Broadcasting:

    • Messages from any client are sent to all connected clients
    • Each message is prefixed with the sender's username
  4. User Interface:

    • Login screen to enter a username
    • Chat interface showing all messages
    • Real-time updates as users join, leave, and send messages

WebSocket Best Practices

When working with WebSockets in a production environment, consider these best practices:

  1. Implement Proper Origin Checking:
go
upgrader.CheckOrigin = func(r *http.Request) bool {
origin := r.Header.Get("Origin")
return origin == "https://yourdomain.com"
}
  1. Handle Connection Timeouts:
go
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
conn.SetPongHandler(func(string) error {
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
return nil
})
  1. Implement Ping-Pong for Connection Health:
go
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()

go func() {
for {
select {
case <-ticker.C:
if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}()
  1. Handle Graceful Shutdowns:
go
// In your main server function
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
<-c
// Notify all clients of server shutdown
// Close all connections
os.Exit(0)
}()
  1. Consider Message Size Limits:
go
upgrader.ReadBufferSize = 1024
upgrader.WriteBufferSize = 1024
  1. Add Authentication:
go
r.GET("/ws", func(c *gin.Context) {
token := c.Query("token")
// Validate token here
if !isValidToken(token) {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
handleWebSocket(c)
})

Summary

In this tutorial, we've learned how to integrate WebSockets with the Gin web framework to enable real-time bidirectional communication. We've covered:

  1. The basics of WebSocket technology and its advantages
  2. Setting up a simple echo WebSocket server with Gin
  3. Building a more complex real-time chat application
  4. Best practices for WebSocket implementation in production

WebSockets open up possibilities for building engaging, interactive applications that respond instantly to user actions and server-side events. They're particularly valuable for applications like:

  • Chat systems
  • Live notifications
  • Collaborative editing tools
  • Real-time dashboards
  • Multiplayer games

Additional Resources

To deepen your understanding of WebSockets with Gin, explore these resources:

  1. Gorilla WebSocket Documentation
  2. Gin Framework Documentation
  3. WebSockets API (MDN Web Docs)
  4. RFC 6455 - The WebSocket Protocol

Exercises

To practice your WebSocket skills:

  1. Enhance the Chat Application:

    • Add private messaging functionality
    • Implement chat rooms
    • Add user status indicators (online/offline)
  2. Create a Real-time Dashboard:

    • Build a server that sends periodic updates about system metrics
    • Create a client that displays these metrics in real time
  3. Build a Collaborative Drawing Application:

    • Implement a shared canvas where multiple users can draw together
    • Synchronize drawing actions using WebSockets
  4. WebSocket Authentication:

    • Add token-based authentication to your WebSocket connection
    • Implement roles and permissions for different user types


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)