Building a Conversational WebSocket Server with Gin Gonic in Go

Building a Conversational WebSocket Server with Gin Gonic in Go

WebSockets have become essential to modern web programming, allowing real-time communication between clients and servers. In this lesson, we’ll look at using the well-liked Gin Gonic framework to add WebSockets to a Go application. It would help if you had a firm grasp of how to build a WebSocket server and include it in your Go web application by the time you finish this tutorial.

Prerequisites:

Before diving into the tutorial, make sure you have the following prerequisites installed:

  1. Go: Ensure that you have Go installed on your system. You can download it from the official Go website (go.dev/doc/install).

  2. Gin Gonic: Install the Gin Gonic framework using the following command:
    go get -u github.com/gin-gonic/gin

  3. Gorilla WebSocket: We'll use the Gorilla WebSocket library for handling WebSocket connections. Install it with:
    go get -ugithub.com/gorilla/websocket

Now, let's start building our WebSocket-enabled Go application!

Step 1: Setting Up the Project

Create a new directory for your project and initialize a Go module:

mkdir go-conversation-server
cd go-conversation-server
go mod init conversation-server

Step 2: Creating a Foundation - Your Project Structure

This step involves organizing your folder structure to provide the foundation for your project. Consider it the basic structure that will hold up your application. Throughout the development process, you may guarantee clarity and maintainability by following the recommended layout exactly. This phase lays the groundwork for the other steps so you can build on a strong foundation with ease.

├── LICENSE
├── README.md
├── api
│   └── routes
│       └── chat.go
├── go.mod
├── go.sum
├── main.go
└── pkg
    └── chat
        ├── client.go
        └── hub.go

Step 3: Setting Up main.go file

The main.go file provided is the entry point for a Go web application using the Gin Gonic framework. Let's break down its functionality into individual steps:

  • Step 1: Define package and import required packages:

      package main
      import (
          "conversationserver/api/routes"
          "os"
    
          "time"
    
          "github.com/gin-contrib/cors"
          "github.com/gin-gonic/gin"
          "github.com/joho/godotenv"
      )
    
  • Step 2: Initialize main function with gin server and handler cors:

      //complete main.go code
    
      package main
    
      import (
          "conversationserver/api/routes"
          "os"
    
          "time"
    
          "github.com/gin-contrib/cors"
          "github.com/gin-gonic/gin"
          "github.com/joho/godotenv"
      )
    
      func main() {
          // `app := gin.Default()` is creating a new instance of the Gin framework's default router.
          app := gin.Default()
    
          // The code `app.Use(cors.New(cors.Config{...}))` is configuring Cross-Origin Resource Sharing (CORS)
          // middleware for the Gin framework.
          app.Use(
              cors.New(
                  cors.Config{
                      AllowOrigins:     []string{"*"},
                      AllowHeaders:     []string{"Accept, Content-Type, Content-Length, Token"},
                      AllowMethods:     []string{"POST, GET, OPTIONS, PUT, DELETE"},
                      AllowCredentials: true,
                      MaxAge:           12 * time.Hour,
                  }))
    
          // `godotenv.Load()` It is used to load environment variables from a `.env` file into the current environment.
          godotenv.Load()
    
          routes.CreateChatRoomRoute(app)
    
          app.Run("localhost:" + os.Getenv("PORT"))
      }
    
  • Step 3: Create a Route for the chat room
    Create a function called WebSocketRoomConnectionHandler to handle WebSocket connections for a specific chat room.

      package routes
    
      import (
          "conversationserver/pkg/chat"
          "github.com/gin-gonic/gin"
      )
    
      // WebSocketRoomConnectionHandler handles WebSocket connections for a specific chat room.
      func WebSocketRoomConnectionHandler() gin.HandlerFunc {
          return func(c *gin.Context) {
              roomId := c.Param("roomId")
              chat.ServeWs(c.Writer, c.Request, roomId)
          }
      }
    
      // CreateChatRoomRoute sets up the WebSocket route for a specific chat room.
      func CreateChatRoomRoute(app *gin.Engine) {
          app.GET("/ws/:roomId", WebSocketRoomConnectionHandler())
      }
    

Step 4: WebSocket Configuration:

The code begins by setting up a WebSocket upgrader with specific buffer sizes and a CheckOrigin function that allows connections from any origin.

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

Subscription and Connection Structs
Two important structs, subscription and connection, are defined to manage WebSocket connections and their subscriptions. Each connection has its send channel for outgoing messages.

type connection struct {
    send chan []byte
    ws   *websocket.Conn
}

type subscription struct {
    conn *connection
    room string
}

ReadPump Function and WritePump Function:
The ReadPump function, associated with a subscription, reads messages from the WebSocket connection and broadcasts them to all other subscribers in the same room.

The WritePump function, also associated with a subscription, writes messages to the WebSocket connection. It includes a periodic ping to ensure the connection remains open.

package chat

import (
    "log"
    "net/http"
    "time"

    "github.com/gorilla/websocket"
)

const (
    writeWait      = 10 * time.Second
    pongWait       = 60 * time.Second
    pingPeriod     = (pongWait * 9) / 10
    maxMessageSize = 512
)

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

// The `ReadPump` function is responsible for reading messages from the websocket connection and
// broadcasting them to all other subscribers in the same room.
func (s subscription) ReadPump() {

    c := s.conn
    defer func() {
        H.unregister <- s
        c.ws.Close()
    }()
    c.ws.SetReadLimit(maxMessageSize)
    c.ws.SetReadDeadline(time.Now().Add(pongWait))
    c.ws.SetPongHandler(func(string) error { c.ws.SetReadDeadline(time.Now().Add(pongWait)); return nil })
    for {
        _, msg, err := c.ws.ReadMessage()
        if err != nil {
            if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
                log.Printf("error: %v\n", err)
            }
            break
        }
        m := message{msg, s.room}
        H.broadcast <- m

    }
}

// This function is writing the messages to the websocket connection.
func (c *connection) Write(mt int, payload []byte) error {
    c.ws.SetWriteDeadline(time.Now().Add(writeWait))
    return c.ws.WriteMessage(mt, payload)
}

// This function is writing the messages to the websocket connection.
func (s *subscription) WritePump() {
    c := s.conn
    ticker := time.NewTicker(pingPeriod)
    defer func() {
        ticker.Stop()
        c.ws.Close()
    }()
    for {
        select {
        case message, ok := <-c.send:
            if !ok {
                c.Write(websocket.CloseMessage, []byte{})
                return
            }
            if err := c.Write(websocket.TextMessage, message); err != nil {

                return
            }
        case <-ticker.C:
            if err := c.Write(websocket.PingMessage, []byte{}); err != nil {
                return
            }
        }
    }
}

// It takes a request, upgrades it to a websocket connection, creates a new connection object, creates
// a new subscription object, and then registers the subscription with the hub
func ServeWs(w http.ResponseWriter, r *http.Request, roomId string) {
    ws, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Println(err.Error())
        return
    }
    c := &connection{send: make(chan []byte, 256), ws: ws}
    s := subscription{c, roomId}
    H.register <- s
    go s.WritePump()
    go s.ReadPump()
}

Step 5: Hub function to handle Register, Unregister, and Broadcasting:
Hub Struct manages connections across different chat rooms, broadcasting messages to subscribers, and handling the registration and unregistration of WebSocket connections.

type Hub struct {
    rooms      map[string]map[*connection]bool
    broadcast  chan message
    register   chan subscription
    unregister chan subscription
}

The code initializes a singleton instance of the Hub named H with channels and room tracking.

var H = Hub{
    rooms:      make(map[string]map[*connection]bool),
    broadcast:  make(chan message),
    register:   make(chan subscription),
    unregister: make(chan subscription),
}

Handing Registration, unregistration, and broadcasting.

func (h *Hub) Hub() {
    for {
        select {
        case s := <-h.register:
            connections := h.rooms[s.room]
            if connections == nil {
                connections = make(map[*connection]bool)
                h.rooms[s.room] = connections
            }
        case s := <-h.unregister:
            connections := h.rooms[s.room]
            if connections != nil {
                if _, ok := connections[s.conn]; ok {
                    delete(connections, s.conn)
                    close(s.conn.send)
                    if len(connections) == 0 {
                        delete(h.rooms, s.room)
                    }
                }
            }
        case m := <-h.broadcast:
            connections := h.rooms[m.room]
            for c := range connections {
                select {
                case c.send <- m.data:
                default:
                    close(c.send)
                    delete(connections, c)
                    if len(connections) == 0 {
                        delete(h.rooms, m.room)
                    }
                }
            }
        }
    }
}
  • Step 4: Running the Application:

Save your changes and run the application using.
go run main.go

Visit ws://localhost:8080/ws/1 to test the socket connection and also share the repo for the code.

Conclusion:

Well done! You've successfully used Gin Gonic in Go to set up a conversational WebSocket server. You can add on this base to create more complex real-time communication functionalities for your web apps. Please feel free to investigate other features provided by Gin Gonic and improve your WebSocket implementation according to the needs of your application. Have fun with coding!

Github Repo Link:

https://github.com/HarshKumarraghav/go-conversation-server