Server-Sent Events в C#: Реактивный веб без WebSocket

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

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

Начать курс

Почему ваш сайт до сих пор не "говорит" с пользователем в реальном времени?

Представьте: вы открываете биржевой терминал, и курс доллара обновляется сам собой. Или заходите в админку — и видите, как прямо на глазах растет количество зарегистрированных пользователей. Или чат, где сообщения приходят без постоянных «апдейтов» страницы.

Раньше для этого приходилось либо ставить WebSocket (сложно, дорого, не всегда нужно), либо использовать long polling (костыль, который убивает батарею телефона). Но есть третий путь — Server-Sent Events в C#.

Сегодня мы разберем технологию, которая позволяет серверу самостоятельно «стучаться» к клиенту и передавать данные. Без лишнего кода, без тонн JavaScript, без головной боли с прокси-серверами. Только C#, только хардкор.

Что такое Server-Sent Events (SSE) и почему это не «протокол для бедных»

Server-Sent Events (или SSE) — это стандарт HTML5, который позволяет серверу отправлять клиенту потоковые данные через обычное HTTP-соединение. Клиент подписывается на события, а сервер просто пишет в Response.Body. Всё.

Главное отличие от WebSocket: SSE — односторонний канал (сервер → клиент). Вам не нужно слать данные обратно? Отлично, SSE ваш выбор. При этом:

  • Работает поверх обычного HTTP/1.1 и HTTP/2
  • Автоматически переподключается при обрыве (встроено в браузерный EventSource)
  • Не требует дополнительных библиотек на клиенте (кроме стандартного JavaScript)
  • Проходит через любые прокси и балансировщики (в отличие от WebSocket)

На практике SSE C# используют для:

  • Ленты уведомлений (push-уведомления прямо в браузер)
  • Мониторинга серверов (дашборды с живыми метриками)
  • Трансляции логов (например, tail -f в браузере)
  • Стриминга AI-генераций (постепенная выдача ответа от ChatGPT-like моделей)

Как работает SSE: протокол одной строки

Всё гениальное просто. Сервер отправляет клиенту поток текста со специальным форматом:

data: Привет, мир!\n\n
data: {"message": "JSON тоже работает"}\n\n
event: customEvent\n
data: Событие с именем\n\n

Каждое сообщение начинается с data:, заканчивается двумя переводами строки. Можно указать event: для кастомного типа события, id: для идентификатора и retry: для таймаута переподключения.

Клиент на JavaScript подписывается так:

const source = new EventSource("/api/stream");
source.onmessage = (event) => {
    console.log("Получено:", event.data);
};
source.addEventListener("customEvent", (event) => {
    console.log("Кастомное событие:", event.data);
});

И это работает во всех современных браузерах. Никаких npm-пакетов, никаких WebSocket-полифиллов.

Server-Sent Events в C#: Реализация на ASP.NET Core

Давайте наконец напишем код. Самый простой способ организовать SSE в C# — это конечная точка (endpoint) в ASP.NET Core, которая не завершает ответ, а пишет в него частями.

Базовый эндпоинт SSE

using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api")]
public class StreamController : ControllerBase
{
    [HttpGet("stream")]
    public async Task Get(CancellationToken cancellationToken)
    {
        Response.Headers.Append("Content-Type", "text/event-stream");
        Response.Headers.Append("Cache-Control", "no-cache");
        Response.Headers.Append("Connection", "keep-alive");

        for (int i = 0; i < 10; i++)
        {
            if (cancellationToken.IsCancellationRequested)
                break;

            // Формат SSE: data: ... \n\n
            await Response.WriteAsync($"data: Сообщение #{i}\n\n", cancellationToken);
            await Response.Body.FlushAsync(cancellationToken);
            
            await Task.Delay(1000, cancellationToken);
        }

        await Response.WriteAsync("data: Поток завершен\n\n", cancellationToken);
        await Response.Body.FlushAsync(cancellationToken);
    }
}

Всё. Запускаете, открываете /api/stream в браузере — видите, как каждую секунду приходит новое сообщение. Server-Sent Events в C# оказывается просто циклом с WriteAsync.

Передача JSON и кастомных событий

Часто нужно слать структурированные данные. Используем сериализацию:

[HttpGet("orders")]
public async Task StreamOrders(CancellationToken ct)
{
    Response.Headers.Append("Content-Type", "text/event-stream");
    Response.Headers.Append("Cache-Control", "no-cache");

    while (!ct.IsCancellationRequested)
    {
        var order = new
        {
            Id = Guid.NewGuid(),
            Product = "Ноутбук",
            Price = 1200.50m,
            Timestamp = DateTime.UtcNow
        };

        var json = System.Text.Json.JsonSerializer.Serialize(order);
        
        await Response.WriteAsync($"event: newOrder\n", ct);
        await Response.WriteAsync($"data: {json}\n\n", ct);
        await Response.Body.FlushAsync(ct);

        await Task.Delay(3000, ct);
    }
}

Клиент ловит это через source.addEventListener("newOrder", ...). Удобно, правда?

Продвинутый SSE в C#: Фоновые сервисы и каналы

В реальном проекте вы не будете генерировать данные в том же методе. Данные поступают из фонового сервиса, очереди или базы данных. Тут на помощь приходит System.Threading.Channels — потокобезопасная очередь.

Фоновый генератор событий

public class EventBroadcastService : BackgroundService
{
    private readonly Channel<string> _channel = Channel.CreateUnbounded<string>();

    public ChannelReader<string> Reader => _channel.Reader;
    public ChannelWriter<string> Writer => _channel.Writer;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await Task.Delay(2000, stoppingToken);
            var message = $"Серверное время: {DateTime.Now:HH:mm:ss}";
            await Writer.WriteAsync(message, stoppingToken);
        }
    }
}

Контроллер, читающий из канала

[ApiController]
[Route("api")]
public class SseController : ControllerBase
{
    private readonly EventBroadcastService _broadcastService;

    public SseController(EventBroadcastService broadcastService)
    {
        _broadcastService = broadcastService;
    }

    [HttpGet("time-stream")]
    public async Task StreamTime(CancellationToken ct)
    {
        Response.Headers.Append("Content-Type", "text/event-stream");
        Response.Headers.Append("Cache-Control", "no-cache");

        var reader = _broadcastService.Reader;
        await foreach (var message in reader.ReadAllAsync(ct))
        {
            await Response.WriteAsync($"data: {message}\n\n", ct                

Блоги

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