← Назад к вопросам

Как используешь Server-Side Events?

2.0 Middle🔥 201 комментариев
#Браузер и сетевые технологии

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Server-Sent Events (SSE) в веб-приложениях

Что такое SSE

Server-Sent Events (SSE) — это веб-технология, которая позволяет серверу отправлять сообщения клиенту в реальном времени через обычное HTTP соединение. Это альтернатива WebSocket'ам для uni-directional коммуникации (только сервер -> клиент).

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

SSE лучше, чем WebSocket'ы когда:

  • Нужны только push-сообщения от сервера
  • Требуется автоматическое переподключение
  • Нужна поддержка старых браузеров
  • Простая структура сообщений

WebSocket'ы лучше, когда:

  • Нужна двусторонняя коммуникация
  • Требуется очень низкая задержка
  • Большой объем данных

1. Базовое использование SSE

// Клиент: подключение к SSE
const eventSource = new EventSource('/api/stream');

// Слушание сообщений
eventSource.addEventListener('message', (event) => {
  const data = JSON.parse(event.data);
  console.log('Получено сообщение:', data);
});

// Закрытие соединения
eventSource.close();

2. Серверная часть (Node.js/Express)

app.get('/api/stream', (req, res) => {
  // Устанавливаем правильные заголовки
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  
  // Отправляем сообщение сразу
  res.write('data: Вы подключились к stream\n\n');
  
  // Отправляем сообщение каждые 5 секунд
  const intervalId = setInterval(() => {
    const data = {
      timestamp: new Date(),
      message: 'Текущее сообщение'
    };
    res.write(`data: ${JSON.stringify(data)}\n\n`);
  }, 5000);
  
  // Очистка при отключении
  req.on('close', () => {
    clearInterval(intervalId);
    res.end();
  });
});

3. Использование именованных событий

// Сервер: отправка разных типов событий
app.get('/api/notifications', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  
  // Событие "online_users"
  res.write(`event: online_users\n`);
  res.write(`data: {"count": 5}\n\n`);
  
  // Событие "new_message"
  res.write(`event: new_message\n`);
  res.write(`data: {"from": "John", "text": "Hello"}\n\n`);
  
  // Событие "status_update" с id для восстановления
  res.write(`id: msg-123\n`);
  res.write(`event: status_update\n`);
  res.write(`data: {"status": "connected"}\n\n`);
});

// Клиент: слушание разных событий
const eventSource = new EventSource('/api/notifications');

eventSource.addEventListener('online_users', (event) => {
  const data = JSON.parse(event.data);
  console.log(`Онлайн пользователей: ${data.count}`);
  updateUserCount(data.count);
});

eventSource.addEventListener('new_message', (event) => {
  const data = JSON.parse(event.data);
  console.log(`Новое сообщение от ${data.from}: ${data.text}`);
  showNotification(data);
});

eventSource.addEventListener('status_update', (event) => {
  const data = JSON.parse(event.data);
  console.log(`Статус: ${data.status}`);
});

4. Обработка ошибок и переподключение

// Клиент
const eventSource = new EventSource('/api/stream');

// Обработка ошибок
eventSource.addEventListener('error', (event) => {
  if (eventSource.readyState === EventSource.CLOSED) {
    console.log('Соединение закрыто');
    // Не будет автоматического переподключения
  } else if (eventSource.readyState === EventSource.CONNECTING) {
    console.log('Попытка переподключиться...');
    // Автоматическое переподключение (через 5 сек по умолчанию)
  }
});

// Явное управление переподключением
let reconnectInterval = 1000;
let maxReconnectInterval = 30000;

function connect() {
  const eventSource = new EventSource('/api/stream');
  
  eventSource.addEventListener('open', () => {
    console.log('Соединение установлено');
    reconnectInterval = 1000; // Сброс интервала при успехе
  });
  
  eventSource.addEventListener('message', (event) => {
    console.log('Сообщение:', event.data);
  });
  
  eventSource.addEventListener('error', () => {
    eventSource.close();
    
    // Экспоненциальная задержка для переподключения
    setTimeout(() => {
      reconnectInterval = Math.min(reconnectInterval * 2, maxReconnectInterval);
      connect();
    }, reconnectInterval);
  });
}

connect();

5. SSE для live-обновлений данных

// Сервер: отправка обновлений цены акции
app.get('/api/stock-price/:ticker', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  
  const ticker = req.params.ticker;
  
  const intervalId = setInterval(() => {
    // Симуляция цены
    const price = (Math.random() * 200 + 50).toFixed(2);
    const data = {
      ticker: ticker,
      price: parseFloat(price),
      timestamp: new Date().toISOString()
    };
    
    res.write(`data: ${JSON.stringify(data)}\n\n`);
  }, 1000);
  
  req.on('close', () => {
    clearInterval(intervalId);
    res.end();
  });
});

// Клиент: отображение live-цены
function trackStockPrice(ticker) {
  const eventSource = new EventSource(`/api/stock-price/${ticker}`);
  
  eventSource.addEventListener('message', (event) => {
    const data = JSON.parse(event.data);
    document.getElementById('price').textContent = `$${data.price}`;
    document.getElementById('timestamp').textContent = new Date(data.timestamp).toLocaleTimeString();
  });
  
  return eventSource;
}

const aapl = trackStockPrice('AAPL');

6. SSE с параметрами запроса

// Клиент: отправка параметров в URL
const userId = 123;
const eventSource = new EventSource(`/api/user-notifications?userId=${userId}`);

eventSource.addEventListener('notification', (event) => {
  const notification = JSON.parse(event.data);
  showNotification(notification);
});

// Сервер
app.get('/api/user-notifications', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  
  const userId = req.query.userId;
  
  // Подписываем пользователя на уведомления
  notificationService.subscribe(userId, (notification) => {
    res.write(`event: notification\n`);
    res.write(`data: ${JSON.stringify(notification)}\n\n`);
  });
  
  req.on('close', () => {
    notificationService.unsubscribe(userId);
    res.end();
  });
});

7. SSE для прогресса длительной операции

// Сервер: отправка прогресса
app.get('/api/long-operation/:operationId', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  
  const operationId = req.params.operationId;
  
  // Симуляция длительной операции
  let progress = 0;
  const intervalId = setInterval(() => {
    progress += Math.random() * 20;
    
    if (progress >= 100) {
      res.write(`event: complete\n`);
      res.write(`data: {"progress": 100, "status": "done"}\n\n`);
      clearInterval(intervalId);
      res.end();
    } else {
      res.write(`event: progress\n`);
      res.write(`data: {"progress": ${Math.floor(progress)}, "status": "processing"}\n\n`);
    }
  }, 500);
  
  req.on('close', () => {
    clearInterval(intervalId);
    res.end();
  });
});

// Клиент: отображение прогресса
function trackProgress(operationId) {
  const eventSource = new EventSource(`/api/long-operation/${operationId}`);
  const progressBar = document.getElementById('progress');
  
  eventSource.addEventListener('progress', (event) => {
    const data = JSON.parse(event.data);
    progressBar.style.width = `${data.progress}%`;
    progressBar.textContent = `${data.progress}%`;
  });
  
  eventSource.addEventListener('complete', (event) => {
    const data = JSON.parse(event.data);
    progressBar.style.width = '100%';
    progressBar.textContent = 'Готово!';
    eventSource.close();
  });
  
  eventSource.addEventListener('error', () => {
    console.error('Ошибка при отслеживании прогресса');
    eventSource.close();
  });
}

trackProgress('op-456');

8. React компонент для SSE

import { useEffect, useState } from 'react';

function NotificationCenter() {
  const [notifications, setNotifications] = useState<any[]>([]);
  const [connected, setConnected] = useState(false);
  
  useEffect(() => {
    const eventSource = new EventSource('/api/notifications');
    
    eventSource.addEventListener('open', () => {
      setConnected(true);
    });
    
    eventSource.addEventListener('notification', (event) => {
      const notification = JSON.parse(event.data);
      setNotifications(prev => [notification, ...prev]);
    });
    
    eventSource.addEventListener('error', () => {
      setConnected(false);
      eventSource.close();
    });
    
    return () => {
      eventSource.close();
    };
  }, []);
  
  return (
    <div>
      <div className={`status ${connected ? 'online' : 'offline'}`}>
        {connected ? 'Подключено' : 'Отключено'}
      </div>
      <ul>
        {notifications.map((notif) => (
          <li key={notif.id}>{notif.message}</li>
        ))}
      </ul>
    </div>
  );
}

export default NotificationCenter;

Формат SSE сообщений

field: value\n
Поля:
- data: содержимое сообщения
- event: тип события
- id: идентификатор для восстановления (Last-Event-ID)
- retry: интервал переподключения в миллисекундах

Примеры:

Простое сообщение:
data: Hello world\n\n

Сообщение с типом события:
event: custom-event\ndata: Some data\n\n

Сообщение с ID для восстановления:
id: msg-1\ndata: Important message\n\n

Установка интервала переподключения (в сек):
retry: 10000\n\n

Итоги

SSE используется для:

  • Live-уведомлений
  • Обновления статуса операций
  • Live-данных (курсы, погода)
  • Chat-сообщений (в одном направлении)
  • Отслеживания прогресса

Преимущества:

  • Простой API
  • Автоматическое переподключение
  • Встроенная поддержка восстановления
  • Низкие накладные расходы

Ограничения:

  • Только uni-directional (сервер -> клиент)
  • Максимум соединений от браузера (6-8)
  • Не подходит для игр и real-time приложений
Как используешь Server-Side Events? | PrepBro