Введение в события, отправляемые сервером (SSE)

События, отправляемые сервером (SSE), представляют собой технологию серверного пуша, позволяющую клиентам автоматически получать обновления от сервера через HTTP-соединение. Они описывают, как сервер инициирует передачу данных клиенту после установки исходного клиентского соединения. Обычно они используются для отправки обновлений сообщений или непрерывных потоков данных клиентам браузера с целью улучшения локальных потоков через API JavaScript под названием EventSource. Клиенты могут использовать этот API для запроса определенного URL-адреса и получения потоков событий. В качестве части HTML5 API EventSource был стандартизирован WHATWG. Медиа-тип для SSE - text/event-stream.

Примечание: Самое большое различие между SSE и Websocket заключается в том, что SSE - это односторонний пуш сообщений от сервера к клиенту, в то время как Websocket - это двусторонний пуш сообщений. Иногда, когда бизнес-требования не такие сложные, достаточно одностороннего пуша сообщений SSE. Это аналогично используемой технологии в разговорах с искусственным интеллектом ChatGPT.

Пример SSE с Fiber

package main

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

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

// Симуляция клиента HTML, вот пример получения фронтендом сообщений обратной отправки от бэкенда в JS, в реальных бизнес-сценариях это реализует фронтенд.
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 = "Извините, ваш браузер не поддерживает события, отправляемые сервером (Server-Sent Events)...";
}
</script>

</body>
</html>
`)

func main() {
	// Создание экземпляра Fiber
	app := fiber.New()

	// Ограничения CORS, разрешение доступа с любого домена
	app.Use(cors.New(cors.Config{
		AllowOrigins:     "*",
		AllowHeaders:     "Cache-Control",
		AllowCredentials: true,
	}))

	// При доступе к пути / сначала возвращаем страницу фронтенда, позволяя фронтенду запросить у бэкенда начать отправку сообщений SSE
	app.Get("/", func(c *fiber.Ctx) error {
		c.Response().Header.SetContentType(fiber.MIMETextHTMLCharsetUTF8)

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

	// Адрес отправки сообщений SSE
	app.Get("/sse", func(c *fiber.Ctx) error {
		// Установка HTTP-заголовков SSE, обратите внимание на 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")

		// Начало отправки сообщений
		c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) {
			fmt.Println("WRITER")
			var i int
			// Симуляция непрерывной отправки сообщений клиенту
			for {
				i++
				msg := fmt.Sprintf("%d - время: %v", i, time.Now())
				// Использование функции writer и Fprintf для отправки сообщений клиенту, в реальных бизнес-сценариях может быть отправлен текст JSON для удобства обработки фронтендом
				fmt.Fprintf(w, "data: Сообщение: %s\n\n", msg)
				fmt.Println(msg)

				// Очистка выходных данных клиенту
				err := w.Flush()
				if err != nil {
					// Обновление страницы в веб-браузере установит новое соединение SSE, но будет активно только последнее, поэтому здесь нужно закрыть неактивные соединения.
					fmt.Printf("Ошибка при очистке: %v. Закрытие HTTP-соединения.\n", err)
					break
				}
				time.Sleep(2 * time.Second)
			}
		}))

		return nil
	})

	// Запуск сервера
	log.Fatal(app.Listen(":3000"))
}