Lesson 3
Enabling Real-time Notifications with WebSockets
Introduction to Real-time Communication

Welcome to our next lesson in this course on building a ToDo application with Go and Gin. Today, we will dive into real-time communication, a highly sought-after feature in today's interactive applications. Real-time communication allows applications to push updates and information to users as they happen, creating a seamless and engaging experience.

In our journey so far, we've established a solid foundation with database interactions and SQL queries. Now, let’s explore how we can enhance user engagement by enabling real-time notifications in our ToDo app using WebSockets.

Let's first understand the theoretical aspects of WebSockets before diving into implementation.

Understanding WebSockets

WebSockets represent a shift from the traditional HTTP protocol by allowing full-duplex (bidirectional and simultaneous) communication between a client and a server. In HTTP, communication begins with a request from the client, followed by a response from the server, after which the connection is typically closed; for new data exchanges, a new request must be initiated by the client, leading to another round of request/response communication. This model works well for many applications but isn't ideal for real-time updates.

With WebSockets:

  • A connection is initiated by the client, similar to an HTTP request.
  • Once established, the connection remains open, enabling continuous data exchange without repeated requests.
  • Both client and server can send data independently, significantly improving efficiency.

This difference allows us to send real-time updates to users without polling the server or waiting for user actions. In our ToDo app, this means we can notify users immediately when tasks are updated or completed.

Pros and Cons of Using WebSockets

Before implementing WebSockets, it’s essential to understand the benefits and challenges they introduce.

Pros:

  • Real-time Data: WebSockets provide immediate data transfer, which is ideal for time-sensitive applications.
  • Efficiency: After the initial handshake, messages can be exchanged with minimal overhead, unlike HTTP, which carries headers with every request.
  • Bi-directional Communication: Both the client and server can send messages independently of each other.

Cons:

  • Resource Consumption: Persistent connections can consume significant server resources, especially with many clients.
  • Complexity: Implementing WebSockets may add complexity to your app's architecture.
  • Network Reliability: WebSocket connections can be disrupted by fluctuating network conditions.

Keep these points in mind as you decide where and how to implement WebSockets in your projects.

WebSocket Business Logic

Let’s add WebSocket functionality to our ToDo app. First, create a service in todoapp/services/websocket_service.go to handle WebSocket communication logic:

Go
1package services 2 3import ( 4 "fmt" 5 6 "github.com/gorilla/websocket" 7) 8 9func WebSocketLoop(conn *websocket.Conn) { 10 for { 11 _, msg, err := conn.ReadMessage() 12 if err != nil { 13 fmt.Println("[SERVER LOG] Error reading message:", err) 14 break 15 } 16 fmt.Println("[SERVER LOG] Message received:", string(msg)) 17 18 if err := conn.WriteMessage(websocket.TextMessage, []byte("This is the message received by the server: "+string(msg))); err != nil { 19 fmt.Println("[SERVER LOG] Error writing message:", err) 20 break 21 } 22 } 23}
  • github.com/gorilla/websocket package: This is a third-party Go package that provides support for WebSockets. This package handles the complexities of WebSocket connections and supports features like upgrading HTTP connections, reading and writing messages, handling binary and text data, and more.
  • The WebSocketLoop function handles the ongoing message exchange between the server and client by reading messages sent by the client and echoing them back to the client.
  • conn.ReadMessage() and conn.WriteMessage(websocket.TextMessage, []byte("message")) are used respectively to read and to write messages from and to the WebSocket connection.
WebSocket Handler

Next, in todoapp/controllers/websocket_controller.go, define a handler to manage WebSocket connections:

Go
1package controllers 2 3import ( 4 "net/http" 5 6 "github.com/gin-gonic/gin" 7 "github.com/gorilla/websocket" 8 9 "codesignal.com/todoapp/services" 10) 11 12var upgrader = websocket.Upgrader{ 13 CheckOrigin: func(r *http.Request) bool { 14 // Allow connections only from localhost 15 return r.Host == "localhost:3000" || r.Host == "127.0.0.1:3000" 16 }, 17} 18 19func WebSocketHandler(c *gin.Context) { 20 conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) 21 if err != nil { 22 http.NotFound(c.Writer, c.Request) 23 return 24 } 25 defer conn.Close() // Ensure connection is closed when handler exits 26 services.WebSocketLoop(conn) 27}

Here's what's happening in the handler:

  • websocket.Upgrader: This is responsible for upgrading a regular HTTP connection to a WebSocket connection. The websocket.Upgrader struct is part of the gorilla/websocket module and manages the handshake process, ensuring it meets the WebSocket protocol requirements.
  • CheckOrigin: This field in websocket.Upgrader is a function used to validate the origin of incoming requests to prevent unwanted cross-origin requests.
  • Connection Upgrade: When a client requests /ws, the HTTP connection is upgraded to a WebSocket using upgrader.Upgrade.
  • Message Handling: With the connection established, the server continuously reads client messages and swaps back responses via services.WebSocketLoop.
  • Connection Cleanup: A defer conn.Close() statement ensures the WebSocket connection is properly closed upon handling completion.

The controller function is then attached to the endpoint /ws.

Testing the WebSocket

To see this in action, use a WebSocket client tool to connect to ws://localhost:3000/ws. As cURL does not natively support WebSocket connections, we employ websocat, which allows connecting to WebSockets from the command line. Please note that websocat usually needs to be installed manually; refer to the GitHub repository of the project for installation instructions. In our CodeSignal IDE, websocat is already installed and ready to run.

Once you have installed websocat, simply run the following command to open the connection:

Bash
1websocat ws://localhost:3000/ws

Once the connection is open, you can type messages in the terminal, pressing enter to send them to the server. In the current setup, any message you type into the WebSocket client will be echoed back by the server, accompanied by logs in the server console indicating that the message was received. For example:

  • Client tab:

    1Hello, WebSocket server! 2This is the message received by the server: Hello, WebSocket server! 3This is a test message. 4This is the message received by the server: This is a test message.
  • Server tab (in the console running the Gin server):

    1[SERVER LOG] Message received: Hello, WebSocket server! 2[SERVER LOG] Message received: This is a test message.
Summary and Practice Preparation

In this lesson, we explored WebSockets and integrated real-time notification capabilities into our ToDo app. We discussed the benefits and challenges of using WebSockets and implemented a basic WebSocket connection with message handling in Gin.

Now that you have a solid understanding of real-time communication with WebSockets, get ready to dive into our practice tasks, where you will test your understanding of WebSocket connections. Keep up the great work!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.