Einführung in servergesendete Ereignisse (SSE)

Servergesendete Ereignisse (SSE) sind eine Server-Push-Technologie, die es Clients ermöglicht, automatisch Updates vom Server über eine HTTP-Verbindung zu erhalten. Es beschreibt, wie der Server den Datentransfer an den Client initiiert, nachdem die initiale Client-Verbindung hergestellt wurde. Sie werden häufig verwendet, um Nachrichtenaktualisierungen oder kontinuierliche Datenströme an Browser-Clients zu senden, mit dem Ziel, lokale, plattformübergreifende Streams über das JavaScript-API namens EventSource zu verbessern. Clients können dieses API verwenden, um eine spezifische URL zur Empfang von Ereignisströmen anzufordern. Als Teil von HTML5 wurde das EventSource-API vom WHATWG standardisiert. Der Medientyp für SSE ist text/event-stream.

Hinweis: Der größte Unterschied zwischen SSE und Websocket besteht darin, dass SSE einen Einweg-Server-zu-Client-Naheichts-Push ist, während Websocket einen beidseitigen Naheichts-Push ist. Manchmal, wenn die Geschäftsanforderungen nicht so komplex sind, reicht der einseitige Naheichts-Push von SSE aus. Dies ähnelt der Technologie, die in ChatGPT AI-Gesprächen verwendet wird.

Fiber SSE-Beispiel

package main

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

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

// Simuliere einen HTML-Client, hier ist ein Beispiel für den Frontend-Empfang von Backend-Push-Nachrichten in JS, in tatsächlichen Geschäftsszenarien wird dies vom Frontend implementiert.
var index = []byte(`<!DOCTYPE html>
<html>
<body>

<h1>SSE-Nachrichten</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 = "Entschuldigung, Ihr Browser unterstützt keine servergesendeten Ereignisse...";
}
</script>

</body>
</html>
`)

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

	// CORS-Beschränkungen, Zugriff von jeder Domain zulassen
	app.Use(cors.New(cors.Config{
		AllowOrigins:     "*",
		AllowHeaders:     "Cache-Control",
		AllowCredentials: true,
	}))

	// Zugriff auf den /-Pfad, wir liefern zuerst die Frontend-Seite, die es dem Frontend ermöglicht, vom Backend das Starten des SSE-Nachrichten-Push anzufordern
	app.Get("/", func(c *fiber.Ctx) error {
		c.Response().Header.SetContentType(fiber.MIMETextHTMLCharsetUTF8)

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

	// SSE-Nachrichten-Push-Adresse
	app.Get("/sse", func(c *fiber.Ctx) error {
		// Setze SSE-HTTP-Header, achte auf 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")

		// Starte das Pushen von Nachrichten
		c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) {
			fmt.Println("WRITER")
			var i int
			// Simuliere das kontinuierliche Pushen von Nachrichten an den Client
			for {
				i++
				msg := fmt.Sprintf("%d - die Zeit ist %v", i, time.Now())
				// Verwende den Writer und die Fprintf-Funktion, um Nachrichten an den Client zu pushen. In tatsächlichen Geschäftsszenarien kann JSON-Text für die Verarbeitung durch das Frontend gesendet werden.
				fmt.Fprintf(w, "data: Nachricht: %s\n\n", msg)
				fmt.Println(msg)

				// Spüle die Ausgabedaten an den Client
				err := w.Flush()
				if err != nil {
					// Das Aktualisieren der Seite im Webbrowser wird eine neue SSE-Verbindung herstellen, aber nur die letzte bleibt aktiv, so dass tote Verbindungen hier geschlossen werden müssen.
					fmt.Printf("Fehler beim Spülen: %v. Schließe die HTTP-Verbindung.\n", err)
					break
				}
				time.Sleep(2 * time.Second)
			}
		}))

		return nil
	})

	// Starte den Server
	log.Fatal(app.Listen(":3000"))
}