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)