DEV Community

tom-takeru
tom-takeru

Posted on

Real-Time Web Application demo with WebSocket - Backend

Introduction

In this article, I will explore the backend implementation of my real-time WebSocket application. Built using Gin and Go, the backend efficiently manages WebSocket connections, stores messages, and broadcasts updates to all connected clients.


Project Structure

https://github.com/tom-takeru/web-socket-demo

My backend project is organized to ensure modularity and reusability. Below is the updated directory structure:

./backend
├── go.mod
├── go.sum
├── main.go
└── stores
    └── messages.go
Enter fullscreen mode Exit fullscreen mode

Key Directories and Files

  • go.mod: Defines module dependencies and versions.
  • main.go: Entry point of the application that initializes the WebSocket server and routes.
  • stores/messages.go: Manages message storage with thread-safe operations.

Core Component: main.go

main.go is the main entry point for my WebSocket server application. It sets up the Gin router, defines the WebSocket route, and handles the WebSocket lifecycle.

Code Walkthrough

package main

import (
    "encoding/json"
    "net/http"
    "sync"
    "time"

    "github.com/gin-gonic/gin"
    "github.com/gorilla/websocket"

    "github.com/tom-takeru/web-socket-demo/backend/stores"
)

var (
    upgrader = websocket.Upgrader{
        CheckOrigin: func(r *http.Request) bool {
            origin := r.Header.Get("Origin")
            // NOTE: This project is for local development only.
            return origin == "http://localhost:3000"
        },
    }
    messageStore = stores.NewMessageStore()
    clients      = make(map[*websocket.Conn]bool)
    clientsMu    sync.Mutex
)

func handleWebSocket(c *gin.Context) {
    conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to upgrade to WebSocket"})
        return
    }
    defer conn.Close()

    clientsMu.Lock()
    clients[conn] = true
    clientsMu.Unlock()

    // Send existing messages to the new connection
    for _, msg := range messageStore.MarshalMessages() {
        conn.WriteMessage(websocket.TextMessage, msg)
    }

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

        var msgData map[string]string
        if err := json.Unmarshal(message, &msgData); err != nil {
            break
        }

        timestamp := time.Now().Format(time.RFC3339)
        msgData["timestamp"] = timestamp

        messageStore.AddMessage(msgData)

        modifiedMessage, err := json.Marshal(msgData)
        if err != nil {
            break
        }

        clientsMu.Lock()
        for client := range clients {
            if err := client.WriteMessage(websocket.TextMessage, modifiedMessage); err != nil {
                client.Close()
                delete(clients, client)
            }
        }
        clientsMu.Unlock()
    }

    clientsMu.Lock()
    delete(clients, conn)
    clientsMu.Unlock()
}

func main() {
    r := gin.Default()
    r.GET("/ws", handleWebSocket)
    r.Run("localhost:8080")
}
Enter fullscreen mode Exit fullscreen mode

Key Functionalities

  1. State Management: Tracks connected clients and ensures thread-safe access using a mutex.
  2. WebSocket Lifecycle: Handles connection setup, message broadcasting, and cleanup on disconnection.

Conclusion

The backend implementation of my WebSocket demo application demonstrates how to manage real-time communication effectively using Gin and Go. By leveraging WebSocket for persistent connections and a thread-safe message store, this application serves as a robust foundation for building real-time web applications.

In the next article, I will discuss deployment strategies and optimizing WebSocket performance.


Links to the Series

Top comments (0)