Introduction to Server-Sent Events (SSE)

Server-Sent Events (SSE) is a server push technology that allows clients to automatically receive updates from the server via an HTTP connection. It describes how the server initiates data transfer to the client after establishing the initial client connection. They are commonly used to send message updates or continuous data streams to browser clients, with the aim of enhancing local cross-browser streams through the JavaScript API called EventSource. Clients can use this API to request a specific URL to receive event streams. As part of HTML5, the EventSource API has been standardized by WHATWG. The media type for SSE is text/event-stream.

Note: The biggest difference between SSE and Websocket is that SSE is a one-way server-to-client message push, while Websocket is a two-way message push. Sometimes, when the business requirements are not as complex, SSE’s one-way message push is sufficient. This is similar to the technology used in ChatGPT AI conversations.

Fiber SSE Example

package main

import (
    "bufio"
    "fmt"
    "log"
    "time"

    "github.com/gofiber/fiber/v2"
    "github.com/gofiber/fiber/v2/middleware/cors"
    "github.com/valyala/fasthttp"
)

// Simulate HTML client, here is an example of the frontend receiving backend push messages in JS, in actual business scenarios, the frontend will implement this.
var index = []byte(`<!DOCTYPE html>
<html>
<body>

<h1>SSE Messages</h1>
<div id="result"></div>

<script>
if(typeof(EventSource) !== "undefined") {
  var source = new EventSource("http://127.0.0.1:3000/sse");
  source.onmessage = function(event) {
    document.getElementById("result").innerHTML += event.data + "<br>";
  };
} else {
  document.getElementById("result").innerHTML = "Sorry, your browser does not support server-sent events...";
}
</script>

</body>
</html>
`)

func main() {
    // Fiber instance
    app := fiber.New()

    // CORS restrictions, allowing access from any domain
    app.Use(cors.New(cors.Config{
        AllowOrigins:     "*",
        AllowHeaders:     "Cache-Control",
        AllowCredentials: true,
    }))

    // Access the / path, we first return the frontend page, allowing the frontend to request the backend to start SSE message push
    app.Get("/", func(c *fiber.Ctx) error {
        c.Response().Header.SetContentType(fiber.MIMETextHTMLCharsetUTF8)

        return c.Status(fiber.StatusOK).Send(index)
    })

    // sse message push address
    app.Get("/sse", func(c *fiber.Ctx) error {
        // Set sse http header, pay attention to Content-Type
        c.Set("Content-Type", "text/event-stream")
        c.Set("Cache-Control", "no-cache")
        c.Set("Connection", "keep-alive")
        c.Set("Transfer-Encoding", "chunked")

        // Start pushing messages
        c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) {
            fmt.Println("WRITER")
            var i int
            // Simulate continuously pushing messages to the client
            for {
                i++
                msg := fmt.Sprintf("%d - the time is %v", i, time.Now())
                // Use writer and Fprintf function to push messages to the client, in actual business scenarios, JSON text can be sent for frontend processing convenience
                fmt.Fprintf(w, "data: Message: %s\n\n", msg)
                fmt.Println(msg)

                // Flush the output data to the client
                err := w.Flush()
                if err != nil {
                    // Refreshing the page in the web browser will establish a new SSE connection, but only the last one is alive, so dead connections must be closed here.
                    fmt.Printf("Error while flushing: %v. Closing http connection.\n", err)
                    break
                }
                time.Sleep(2 * time.Second)
            }
        }))

        return nil
    })

    // Start server
    log.Fatal(app.Listen(":3000"))
}