Simplifying Server-Side Push Notifications: A SSE Implementation in Golang

If you’re building a web application that requires real-time data updates, you may have considered using WebSockets or long-polling techniques to achieve this functionality. However, another option may be more suitable for certain use cases: Server-Sent Events (SSE).

What are Server-Sent Events?

Server-Sent Events (SSE) is a technology that enables a server to send real-time updates to a client over a single, long-lived HTTP connection. With SSE, the server pushes updates to the client as soon as they become available, eliminating the need for the client to poll the server for updates continuously.

SSE has been around for over a decade, but it is not as widely known as WebSockets or traditional polling techniques. One of the main reasons for this is that SSE has some limitations in terms of the types of data that can be transmitted and the level of control that clients have over the connection.

However, SSE has several advantages over traditional polling techniques, such as reduced network traffic, lower server load, and faster response times. SSE is also simpler to implement than WebSockets, using standard HTTP requests and responses.

How SSE Differs from Traditional Requests and WebSockets

To understand the benefits of SSE, it’s helpful to compare it to traditional request-response techniques and WebSockets.

  • In a traditional request-response model, the client sends a request to the server and waits for a response. If the client needs to receive updates from the server, it must continuously send requests at regular intervals. This approach can lead to high network traffic and increased server load, as well as delays in receiving updates due to the polling interval.
  • WebSockets, on the other hand, establish a persistent bidirectional connection between the client and server, allowing for real-time communication in both directions. However, WebSockets can be more complex to implement than SSE, as they require a separate protocol and may not be supported by all web browsers.
  • SSE falls somewhere in between these two approaches. Like traditional requests, SSE uses a unidirectional connection (from server to client), but the connection remains open for as long as the client needs to receive updates. This approach allows for real-time updates without the overhead of continuous polling or the complexity of bidirectional communication.

SSE vs. WebSockets: When to Use Which?

Both SSE and WebSockets have their strengths and weaknesses, and the choice between them depends on the specific use case of your application.

Use SSE when:

  • You need real-time updates from the server to the client.
  • You want a simple implementation without the overhead of bidirectional communication.
  • You’re looking for browser compatibility, as SSE is supported by most modern browsers.

Use WebSockets when:

  • You require bidirectional communication between the client and server.
  • You need to transmit complex data structures efficiently.
  • Your application can handle the additional complexity and potential browser compatibility issues.

Implementing SSE in Golang

Now that you understand the basics of SSE and how it differs from traditional requests and WebSockets, you may be wondering how to implement SSE in Golang. The good news is that Golang has built-in support for SSE through its net/http package.

Below is a basic example of an SSE handler in Golang:

package main

import (
   "fmt"
   "math/rand"
   "net/http"
   "time"
)

func sseHandler(w http.ResponseWriter, r *http.Request) {
   w.Header().Set("Content-Type", "text/event-stream")
   w.Header().Set("Cache-Control", "no-cache")
   w.Header().Set("Connection", "keep-alive")

   // Simulate sending SSE data
   for {
      randomNumber := rand.Intn(100)
      fmt.Fprintf(w, "Number: %d\n", randomNumber)
      w.(http.Flusher).Flush() // Flush the response to the client
      // Simulate a delay between each event
      time.Sleep(time.Second)
   }
}

func main() {
   http.HandleFunc("/sse", sseHandler)
   http.ListenAndServe(":8080", nil)
}

In this example, the sseHandler function sets the necessary HTTP headers for SSE and sends a series of SSE data events to the client. In this code, the flusher.Flush() call is used to send the buffered data to the client immediately. This ensures that the client receives the data in real-time as it’s written, which is important for SSE.

Here’s how you can test the SSE endpoint using curl:

ShellScript
curl -H "Accept: text/event-stream" http://localhost:8080/sse

After running the curl command, you should see a stream of SSE data being printed in your terminal as the server sends random numbers. Please note that the behavior of the curl output might vary depending on your terminal and curl version. You may need to press Ctrl+C to stop the command and exit.

When a client connects to an SSE endpoint, it receives a continuous stream of data from the server. However, what makes this even more captivating is that every connected client gets its own distinct set of random numbers. This is due to the server generating fresh random numbers and sending them individually to each connected client. As a result, each client experiences a unique flow of random data. This example showcases the power of SSE in delivering personalized content to multiple clients simultaneously, adding a touch of randomness and uniqueness to the real-time experience.

Enhancing Server-Sent Events with Personalized Streams

In our basic example, we delved into streaming random numbers, creating distinct experiences for each connected client. Now, we’ll take it a step further by implementing a POST request system coupled with secret-key-based authentication.

Imagine an HTML form with fields for a message and a secret key. Upon submission, this form triggers an SSE stream explicitly targeted to clients who provide the corresponding secret key as a GET parameter. This enhancement strengthens security and enables the server to send personalized updates.

Our updated Go SSE example now handles multiple clients, each with its own secret keys, showcasing the potential of SSE to deliver customized content securely and efficiently. With this setup, real-time data transmission becomes a finely tuned symphony of personalized streams, redefining the way we engage with dynamic content.

To enhance our SSE implementation, a slight modification to the SSE handler is in order. This adjustment involves tracking and managing all newly connected clients within the SSE stream. As clients establish connections, they will be carefully recorded in our server’s records. This registry of connected clients ensures that messages and updates are consistently dispatched to the right recipients. Additionally, a mechanism to automatically remove clients upon disconnection will be implemented. This dynamic management of client connections ensures that resources are efficiently utilized and that no lingering connections remain. By making these refinements, we’re optimizing our SSE solution to be both efficient and robust, delivering a seamless and responsive real-time experience for all connected users.

var clients = make(map[string]http.ResponseWriter)

func sseHandler(w http.ResponseWriter, r *http.Request) {
   w.Header().Set("Content-Type", "text/event-stream")
   w.Header().Set("Cache-Control", "no-cache")
   w.Header().Set("Connection", "keep-alive")
   w.Header().Set("Access-Control-Allow-Origin", "*")

   secretKey := r.URL.Query().Get("secretKey")

   // Store the client's response writer for sending messages
   clients[secretKey] = w

   for {
      select {
      case <-r.Context().Done():
         delete(clients, secretKey)
         fmt.Println("Client closed connection.")
         return
      }
   }
}

After we incorporating essential configurations, the code snippet configures proper response headers to ensure seamless and uninterrupted communication for Server-Sent Events (SSE), while also efficiently extracting authentication credentials from URL query parameters.

The code initializes a map named “clients” to store client information, and then assigns the client’s response writer to the map using their secret key as the key for efficient management of connected clients.

In our forthcoming development, we’re set to create a dynamic real-time interaction using a pair of HTML files designed to manage form submissions and establish client connections to our SSE stream.

With a client-side scenario in mind, the process unfolds as follows: as the appropriate secret key is utilized to establish the SSE connection, and a fresh message is submitted via a form, the client will be primed to instantly witness these new messages in their personalized stream.

To realize this vision, we will introduce three new handleFunc entries into our Go application.

The submitHandler will deftly manage POST requests, orchestrating the smooth flow of data. Meanwhile, the serveClient and serveForm functions will be responsible for serving up the HTML files that power the client’s side of the interaction. This trio of functionalities will converge to create a captivating real-time experience that redefines engagement with dynamic content.

In the example above, we employed the secret key “john” alongside the message “Hello John from SSE!” for demonstration purposes. Upon submitting the message, any client authenticated with the corresponding secret key gains immediate visibility of the transmitted content.

For a comprehensive view of the entire code implementation, you can access and explore the project on my GitHub repository.

As we conclude this journey through enhancing real-time communication with Server-Sent Events, let’s take a final glimpse at our polished main.go file, a testament to the power of personalized streaming and secure connections.

package main

import (
   "fmt"
   "net/http"
)

var clients = make(map[string]http.ResponseWriter)

func main() {
   http.HandleFunc("/sse", sseHandler)
   http.HandleFunc("/submit", submitHandler)
   http.HandleFunc("/client", serveClient)
   http.HandleFunc("/", serveForm)
   http.ListenAndServe(":8080", nil)
}

func sseHandler(w http.ResponseWriter, r *http.Request) {
   w.Header().Set("Content-Type", "text/event-stream")
   w.Header().Set("Cache-Control", "no-cache")
   w.Header().Set("Connection", "keep-alive")
   w.Header().Set("Access-Control-Allow-Origin", "*")

   secretKey := r.URL.Query().Get("secretKey")

   // Store the client's response writer for sending messages
   clients[secretKey] = w

   for {
      select {
      case <-r.Context().Done():
         delete(clients, secretKey)
         fmt.Println("Client closed connection.")
         return
      }
   }
}

func submitHandler(w http.ResponseWriter, r *http.Request) {
   if r.Method == http.MethodPost {
      message := r.FormValue("message")
      secretKey := r.FormValue("secretKey")

      if client, ok := clients[secretKey]; ok {
         fmt.Fprintf(client, "data: %s\n\n", message)
         flusher, ok := client.(http.Flusher)
         if !ok {
            http.Error(w, "Here not okey", http.StatusInternalServerError)
            return
         }
         flusher.Flush()
      } else {
         fmt.Fprintf(w, "Check your secret key.\n")
      }
   }
}

func serveForm(w http.ResponseWriter, r *http.Request) {
   http.ServeFile(w, r, "form.html")
}

func serveClient(w http.ResponseWriter, r *http.Request) {
   http.ServeFile(w, r, "client.html")
}

9 comments On Simplifying Server-Side Push Notifications: A SSE Implementation in Golang

Leave a reply:

Your email address will not be published.

Site Footer