Server-Sent Events в Go: Реактивные веб-приложения без WebSocket

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Server-Sent Events в Go: Когда WebSocket — это перебор

Представьте: вы пишете дашборд для мониторинга серверов. Данные обновляются каждые 5 секунд. Клиент должен получать уведомления о падении нагрузки. Что вы выберете? WebSocket? Long Polling? А что, если я скажу, что есть технология, которая работает поверх обычного HTTP, не требует дополнительных библиотек на клиенте и идеально подходит для однонаправленной передачи данных?

Знакомьтесь — Server-Sent Events (SSE). Это протокол, встроенный в HTML5, который позволяет серверу отправлять данные клиенту в реальном времени через одно HTTP-соединение. В отличие от WebSocket, SSE работает только в одну сторону — от сервера к клиенту. Но именно это делает его идеальным для уведомлений, логов, котировок акций и любых задач, где сервер — источник данных, а клиент — пассивный слушатель.

В этой статье мы разберем, как реализовать Server-Sent Events в Go. Вы узнаете, как создать простой стриминг, как передавать кастомные события и как обрабатывать отключения клиентов. Поехали!

Почему Go + SSE — это мощно?

Go — язык, созданный для сетевых приложений. Его горутины и каналы идеально ложатся на модель SSE: каждое соединение — отдельная горутина, а каналы обеспечивают безопасную передачу данных. Плюс, Go — это компилируемый язык с отличной производительностью. Вы можете держать десятки тысяч открытых SSE-соединений без особых затрат памяти.

Ключевое преимущество SSE перед WebSocket — простота. На клиенте используется обычный EventSource API, который работает во всех современных браузерах. Никаких дополнительных библиотек, никаких фреймворков. Серверная часть — это обычный HTTP-хендлер с правильными заголовками.

Когда стоит использовать SSE?

  • Однонаправленная передача — сервер отправляет данные, клиент только получает.
  • Уведомления и алерты — система мониторинга, push-уведомления.
  • Стриминг данных — логи, котировки, твиты, новости.
  • Автоматическое переподключение — браузер сам восстанавливает соединение при обрыве.

Основы протокола Server-Sent Events

Прежде чем писать код, давайте разберемся, как работает SSE на уровне HTTP. Когда клиент открывает соединение, сервер отправляет специальные заголовки:

Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

После этого сервер отправляет данные в формате:

data: Сообщение\n\n

Каждое сообщение разделяется двумя переводами строки. Можно отправлять несколько полей:

event: userlogin
data: {"username": "alice"}\n\n

Поле event определяет тип события, а data — его содержимое (обычно JSON). Также есть поля id (идентификатор последнего события) и retry (время переподключения в миллисекундах).

Пишем SSE-сервер на Go

Начнем с самого простого — сервера, который отправляет текущее время каждую секунду. Создайте файл main.go:

package main

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

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

func sseHandler(w http.ResponseWriter, r *http.Request) {
    // Устанавливаем заголовки для SSE
    w.Header().Set("Content-Type", "text/event-stream")
    w.Header().Set("Cache-Control", "no-cache")
    w.Header().Set("Connection", "keep-alive")

    // Создаем flusher для немедленной отправки данных
    flusher, ok := w.(http.Flusher)
    if !ok {
        http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
        return
    }

    // Бесконечный цикл отправки событий
    for {
        select {
        case <-r.Context().Done():
            // Клиент отключился
            fmt.Println("Client disconnected")
            return
        default:
            // Отправляем событие
            fmt.Fprintf(w, "data: %s\n\n", time.Now().Format(time.RFC3339))
            flusher.Flush()
            time.Sleep(1 * time.Second)
        }
    }
}

Этот код делает следующее:

  • Устанавливает правильные заголовки для SSE.
  • Проверяет, поддерживает ли ResponseWriter интерфейс Flusher (необходим для немедленной отправки).
  • В бесконечном цикле отправляет текущее время каждую секунду.
  • Следит за контекстом запроса — если клиент закрыл соединение, горутина завершается.

Клиентская часть

Создайте файл index.html:

<!DOCTYPE html>
<html>
<head>
    <title>SSE Demo</title>
</head>
<body>
    <h1>Server Time</h1>
    <div id="time"></div>
    <script>
        const eventSource = new EventSource('/events');
        eventSource.onmessage = function(event) {
            document.getElementById('time').innerText = event.data;
        };
        eventSource.onerror = function() {
            console.log('Connection error');
        };
    </script>
</body>
</html>

Запустите сервер go run main.go и откройте index.html в браузере. Вы увидите, как время обновляется каждую секунду. Магия!

Кастомные события и JSON

В реальных приложениях редко передают просто строку. Обычно это JSON с разными типами событий. Давайте модернизируем сервер, чтобы он отправлял события message и alert:

type Event struct {
    Type string `json:"type"`
    Data string `json:"data"`
}

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")

    flusher, ok := w.(http.Flusher)
    if !ok {
        http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
        return
    }

    // Канал для отправки событий
    events := make(chan Event)
    defer close(events)

    // Запускаем горутину-генератор событий
    go func() {
        for i := 0; ; i++ {
            if i%5 == 0 {
                events <- Event{Type: "alert", Data: "Важное уведомление!"}
            } else {
                events <- Event{Type: "message", Data: fmt.Sprintf("Сообщение #%d", i)}
            }
            time.Sleep(2 * time.Second)
        }
    }()

    for {
        select {
        case <-r.Context().Done():
            return
        case event := <-events:
            jsonData, _ := json.Marshal(event)                

Блоги

Рекомендаций по книгам