280.54K
Категория: ПрограммированиеПрограммирование

SSE_Presentation_Slides (1)

1.

Server-Sent Events (SSE) в .NET 10

2.

Введение в Server-Sent Events (SSE)
Что такое SSE?
Microsoft подкинула нам интересную фичу в новой версии фреймворка
Server-Sent Events, или просто SSE - это технология, которая долгое
время оставалась в тени более раскрученного WebSocket. При этом,
если копнуть глубже, SSE решает довольно важную и распространенную
задачу: отправку данных от сервера клиенту в режиме реального
времени без лишних сложностей. Если объяснять простыми словами,
SSE - это механизм, который позволяет серверу "толкать" данные в
браузер, как только они становятся доступны, без необходимости со
стороны клиента постоянно спрашивать: "А есть что-нибудь новенькое?".
Технически это реализуется через обычное HTTP-соединение, которое
остается открытым длительное время, и по которому сервер может
отправлять данные, когда ему вздумается.
Ключевое отличие от WebSocket - однонаправленность. В то время как
WebSocket обеспечивает полнодуплексный канал связи (и клиент, и
сервер могут одновременно отправлять и получать данные), SSE
работает только в одном направлении: от сервера к клиенту. На первый
взгляд это кажется ограничением, но на практике для многих сценариев
нам и не нужно ничего другого.
Сервер ---> Клиент
// SSE: односторонняя связь
Сервер <--> Клиент
// WebSocket: двусторонняя связь

3.

А зачем нам SSE, когда есть WebSocket?
Не нужно стрелять из пушки по воробьям. WebSocket - это отдельный
протокол, который требует специальной настройки сервера, прокси и
иногда даже сетевой инфраструктуры. SSE же использует обычный
HTTP, который поддерживается везде и всюду. Вот краткий список
преимуществ SSE:
● Работает поверх стандартного HTTP.
● Автоматическое переподключение при разрыве соединения.
● Меньше накладных расходов на установку соединения.
● Не требует специальных настроек прокси и файерволов.
● Возможность назначать ID событиям для отслеживания
последнего полученного события.
● Поддержка различных типов событий в одном соединении.
Формат данных в SSE предельно прост. Сервер отправляет текстовые
сообщения, разделенные двойным переносом строки. Каждое
сообщение может иметь идентификатор, тип события и данные:
event: order
id: 12345
data: {"product": "Книга", "price": 500}
На стороне клиента (браузера) все события обрабатываются через
JavaScript API EventSource. Именно в простоте этого API и кроется еще
одно преимущество SSE - минимальный порог входа для фронтендразработчиков.
const eventSource = new EventSource('/orders');
eventSource.addEventListener('order', event => {
const orderData = JSON.parse(event.data);
console.log('Новый заказ:', orderData);
});
Появление нативной поддержки SSE в .NET 10 - это признак того, что
Microsoft наконец признала ценность этой технологии для определенных
сценариев. Раньше нам приходилось изобретать велосипеды или
использовать сторонние библиотеки, теперь же фреймворк предоставляет
готовое решение, оптимизированное с учетом всех особенностей
платформы.
Что касается внутреннего устройства SSE в .NET 10, то он построен на
основе асинхронных потоков (IAsyncEnumerable), что идеально вписывается
в асинхронную модель программирования современного .NET и позволяет
эффективно управлять ресурсами при большом количестве одновременных
соединений.

4.

Сравнение технологий реального
времени
Когда дело доходит до выбора технологии для получения данных в
реальном времени, разработчики часто стоят перед выбором между
несколькими подходами.
Polling - это когда клиент периодически запрашивает новые данные у
сервера, обычно с фиксированным интервалом (например, каждые 5
секунд). Long-polling - более продвинутая техника, при которой запрос
"подвешивается" на сервере до появления новых данных или
истечения таймаута. А SSE, как мы уже знаем, держит соединение
открытым и отправляет данные, когда они становятся доступны.
Сводная таблица производительности и характеристик:
Параметр
Polling
Long-polling
SSE
Сетевой трафик
Высокий
Средний
Низкий
Нагрузка на CPU
Высокая
Средняя
Низкая
Потребление RAM
Среднее
Высокое
Низкое
Задержка данных
Высокая
Низкая
Низкая
Восстановление связи
Плохое
Среднее
Хорошее
Потребление батареи
Высокое
Среднее
Низкое
На основе всех этих данных можно сделать вывод: SSE явно выигрывает
у традиционных подходов практически по всем параметрам.
Единственным ограничением остается однонаправленность связи - если
вам нужна двусторонняя коммуникация в реальном времени,
WebSocket или SignalR все еще будут более подходящим выбором.
Однако для большинства сценариев уведомлений и потоковой передачи
данных от сервера к клиенту SSE является оптимальным решением.

5.

Нативная поддержка в .NET 10
Что нового в .NET 10?
В .NET 10 Microsoft добавила нативную поддержку SSE через новый тип результата
ServerSentEventsResult в Minimal API. Это стало возможным благодаря введению
специального класса в пространстве имен Microsoft.AspNetCore.Http.HttpResults. Начнем с
базового примера.
Базовый пример подключения SSE:
app.MapGet("/events", (CancellationToken cancellationToken) =>
TypedResults.ServerSentEvents(
GetEventsAsync(cancellationToken),
eventType: "message"
)
);
async IAsyncEnumerable<string> GetEventsAsync(
[EnumeratorCancellation] CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
yield return $"Время на сервере: {DateTime.Now}";
await Task.Delay(1000, cancellationToken);
}
}
Здесь самое важное - использование IAsyncEnumerable<T> для создания
потока данных. Когда клиент подключается к этому эндпоинту, ASP.NET
Core автоматически настраивает все
необходимые заголовки и поддерживает соединение открытым, отправляя
каждый элемент последовательности как отдельное событие.
Метод ServerSentEvents принимает несколько параметров:
• source - асинхронная последовательность данных
(IAsyncEnumerable)
• eventType - тип события (необязательный параметр)
• eventId - функция для генерации ID события (необязательный
параметр)
Если вы опустите eventType, события будут отправляться без указания
типа, и на клиенте их можно будет обрабатывать через обычный
обработчик onmessage.

6.

Практический пример системы
уведомлений
Система уведомлений о новых заказах:
public class OrderService
{
private readonly Subject<Order> _orderSubject = new();
public void AddOrder(Order order)
{
_orderSubject.OnNext(order);
}
public IAsyncEnumerable<string> GetOrderUpdatesAsync(
CancellationToken cancellationToken)
{
return _orderSubject
.Select(order => JsonSerializer.Serialize(order))
.ToAsyncEnumerable()
.WithCancellation(cancellationToken);
}
}
Использование в Minimal
API:
app.MapGet("/orders/updates", (OrderService orderService, CancellationToken cancellationToken) =>
TypedResults.ServerSentEvents(
orderService.GetOrderUpdatesAsync(cancellationToken),
eventType: "order"
)
);
В этом примере используется System.Reactive для создания потока
событий через Subject<T>. Это позволяет легко интегрировать SSE с
существующей системой уведомлений или событий в приложении. Такой
подход особенно полезен в микросервисной архитектуре, где разные
сервисы могут генерировать события, которые нужно доставлять клиентам
в реальном времени.

7.

Автоматическая настройка заголовков
ASP.NET Core автоматически устанавливает
правильные заголовки:
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
Эти три заголовка сигнализируют браузеру, что он имеет дело с потоком
событий, и что кэшировать этот поток не стоит. Но иногда стандартных
настроек недостаточно, и нам нужно больше контроля. Если вы хотите
тонко настроить поведение SSE-соединений, можно реализовать
собственный метод расширения.
public static IResult
ServerSentEventsWithTimeout<T>(
Например,
добавление
таймаута для неактивных подключений:
this IResultExtensions extensions,
IAsyncEnumerable<T> source,
string? eventType = null,
Func<T, string?>? eventId = null,
TimeSpan timeout = default)
{
timeout = timeout == default ? TimeSpan.FromMinutes(2) : timeout;
return new CustomServerSentEventsResult<T>(
source,
eventType,
eventId,
timeout);
}
Такой подход дает вам гибкость в управлении соединениями, но требует
реализации
собственного класса CustomServerSentEventsResult. Это полезно в
production-окружениях, где нужно контролировать ресурсы сервера и
предотвращать накопление "зомби"-соединений.

8.

Организация сервисов и внедрение
зависимостей
Для организации работы с SSE в больших приложениях
рекомендуется регистрировать специальные сервисы в
контейнере зависимостей:
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddServerSentEvents(
this IServiceCollection services)
{
services.AddSingleton<IEventSourceService, EventSourceService>();
services.AddHostedService<EventBroadcastService>();
return services;
}
}
Здесь IEventSourceService отвечает за управление подписками и
генерацию событий, а EventBroadcastService - фоновый сервис, который
генерирует события для всех подключенных клиентов. Такое разделение
ответственности позволяет масштабировать систему и управлять
нагрузкой.

9.

Пример реализации EventSourceService
Реализация EventSourceService может выглядеть
так:
public class EventSourceService : IEventSourceService
{
private readonly ConcurrentDictionary<string, Channel<string>> _channels
= new();
public IAsyncEnumerable<string> Subscribe(string channelName,
CancellationToken cancellationToken)
{
var channel = _channels.GetOrAdd(channelName,
_ => Channel.CreateUnbounded<string>());
return channel.Reader.ReadAllAsync(cancellationToken);
}
public ValueTask PublishAsync(string channelName, string message)
{
if (_channels.TryGetValue(channelName, out var channel))
{
return channel.Writer.WriteAsync(message);
}
return ValueTask.CompletedTask;
}
}
Такая реализация позволяет организовать разные каналы событий, к
которым клиенты могут подписываться по отдельности. Это особенно
полезно в многопользовательских системах, где разные пользователи
должны получать разные наборы событий. ConcurrentDictionary
гарантирует потокобезопасность операций с каналами.

10.

Middleware для гибкой обработки SSE
Хотя Minimal API предоставляет удобный способ создания SSEэндпоинтов, иногда нужна более гибкая обработка. Вы можете создать
специальный middleware для SSE:
public class ServerSentEventsMiddleware
{
private readonly RequestDelegate _next;
public ServerSentEventsMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context,
IEventSourceService eventSource)
{
if (!context.Request.Path.StartsWithSegments("/sse"))
{
await _next(context);
return;
}
string channelName = context.Request.Query["channel"];
if (string.IsNullOrEmpty(channelName))
{
context.Response.StatusCode = 400;
return;
}
context.Response.Headers.Add("Content-Type", "text/event-stream");
context.Response.Headers.Add("Cache-Control", "no-cache");
context.Response.Headers.Add("Connection", "keep-alive");
var cancellationToken = context.RequestAborted;
await foreach (var message in eventSource
.Subscribe(channelName, cancellationToken)
.WithCancellation(cancellationToken))
{
await context.Response.WriteAsync($"data: {message}\r\n\r\n",
cancellationToken);
await context.Response.Body.FlushAsync(cancellationToken);
}
}
}
Регистрация middleware выполняется в методе.
app.UseMiddleware<ServerSentEventsMiddleware>();
Такой подход дает больше контроля над обработкой SSE-запросов, но
требует ручной настройки заголовков и формата событий. Это полезно,
когда нужно добавить дополнительную логику,
например, аутентификацию или авторизацию перед установкой SSEсоединения.

11.

Кэширование и буферизация для SSE
Один из важных аспектов настройки SSE - правильная буферизация
ответов. По умолчанию ASP.NET Core может буферизировать ответы,
что нежелательно для SSE. Убедитесь, что буферизация отключена:
app.Use(async (context, next) =>
{
context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
context.Features.Get<IHttpMaxRequestBodySizeFeature>()
?.MaxRequestBodySize = null;
context.Response.BufferOutput = false;
await next();
});
Также стоит настроить Kestrel для оптимальной работы с
долгоживущими соединениями:
builder.WebHost.ConfigureKestrel(options =>
{
options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2);
options.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(1);
options.Limits.MaxConcurrentConnections = 10000;
});
Эти настройки критически важны для стабильной работы SSE в
production-окружении, особенно при большом количестве
одновременных подключений.
Если вы работаете за прокси-сервером (например, NGINX), не забудьте
настроить его для правильной работы с SSE:
http {
proxy_buffering off;
proxy_read_timeout 3600s;
proxy_connect_timeout 3600s;
proxy_send_timeout 3600s;
server {
location /sse/ {
proxy_pass [url]http://backend;[/url]
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
}

12.

Структура приложения для крупных проектов
В крупных проектах рекомендуется следующая структура для работы с
SSE:
1. EventHub - центральный компонент для управления всеми
событиями
2. EventChannel - отдельный канал для конкретного типа событий
3. EventSourceController/Endpoint - API для подключения клиентов
4. EventPublisher - сервис для публикации событий из бизнес-логики
Для реализации такой структуры можно использовать следующий подход:
public class EventHub
{
private readonly ConcurrentDictionary<string, EventChannel> _channels = new();
public EventChannel GetOrCreateChannel(string name)
{
return _channels.GetOrAdd(name, _ => new EventChannel(name));
}
}
public class EventChannel
{
private readonly string _name;
private readonly Channel<object> _channel = Channel.CreateUnbounded<object>(
new UnboundedChannelOptions { SingleReader = false, SingleWriter = false });
public EventChannel(string name)
{
_name = name;
}
public async ValueTask PublishAsync(object data)
{
await _channel.Writer.WriteAsync(data);
}
public IAsyncEnumerable<string> Subscribe(CancellationToken cancellationToken)
{
return _channel.Reader
.ReadAllAsync(cancellationToken)
.Select(data => JsonSerializer.Serialize(data));
}
}
Такая архитектура обеспечивает масштабируемость и позволяет легко
добавлять новые типы событий без изменения существующего кода.
Каждый канал может обрабатываться независимо, что упрощает отладку
и мониторинг.

13.

Обработка ошибок в SSE
Одна из сложностей при работе с SSE - правильная обработка ошибок.
Когда клиент отключается, сервер должен корректно закрыть соединение и
освободить ресурсы. Вот как можно реализовать обработку исключений:
app.MapGet("/stream", async (HttpContext context,
IEventSourceService service,
CancellationToken cancellationToken) =>
{
try
{
return TypedResults.ServerSentEvents(
service.GetEvents(cancellationToken),
eventType: "update"
);
}
catch (OperationCanceledException)
{
// Клиент отключился, просто логируем
logger.LogInformation("Клиент отключился");
return Results.Empty;
}
catch (Exception ex)
{
logger.LogError(ex, "Ошибка при обработке SSE-соединения");
return Results.Problem("Внутренняя ошибка сервера");
}
});
Также важно обрабатывать ошибки на уровне соединения. Например,
при сетевых сбоях или проблемах с сериализацией данных. В
production-приложениях рекомендуется добавлять механизм повторной
отправки критически важных событий и ведения логов всех ошибок для
последующего анализа.

14.

Мониторинг SSE-соединений
Для крупных приложений критически важно отслеживать состояние SSEсоединений. Пример простой реализации мониторинга с использованием
механизма диагностики .NET:
public class SseConnectionMetrics
{
private readonly Counter<int> _activeConnections;
private readonly Counter<int> _totalConnections;
private readonly Counter<int> _messagesSent;
public SseConnectionMetrics(IMeterFactory meterFactory)
{
var meter = meterFactory.Create("SseMetrics");
_activeConnections = meter.CreateCounter<int>("active_connections");
_totalConnections = meter.CreateCounter<int>("total_connections");
_messagesSent = meter.CreateCounter<int>("messages_sent");
}
public void ConnectionOpened()
{
_activeConnections.Add(1);
_totalConnections.Add(1);
}
public void ConnectionClosed()
{
_activeConnections.Add(-1);
}
public void MessageSent()
{
_messagesSent.Add(1);
}
}
Такая система мониторинга позволяет отслеживать:
• Количество активных соединений
• Общее количество соединений за период
• Количество отправленных сообщений
• Среднее время жизни соединения
• Процент успешных/неуспешных соединений
Эти метрики помогают выявлять проблемы с производительностью и
планировать масштабирование системы.

15.

Интеграция с существующей
архитектурой
Часто бывает, что SSE нужно интегрировать в существующую
систему, например, с шаблоном CQRS и MediatR. Вот пример такой
интеграции:
public class EventNotifier : INotificationHandler<DomainEventNotification>
{
private readonly IEventHub _eventHub;
public EventNotifier(IEventHub eventHub)
{
_eventHub = eventHub;
}
public async Task Handle(DomainEventNotification notification,
CancellationToken cancellationToken)
{
var channelName = notification.Event.GetType().Name;
var channel = _eventHub.GetOrCreateChannel(channelName);
await channel.PublishAsync(notification.Event);
}
}
При такой реализации любое доменное событие автоматически
публикуется в соответствующий SSE-канал. Это позволяет создать
единую систему событий для всего приложения, где одни и те же события
могут обрабатываться как внутри серверной части, так и доставляться
клиентам в реальном времени. Такой подход упрощает поддержку кода и
обеспечивает консистентность данных между сервером и клиентами.

16.

Безопасность SSE-соединений
Важный аспект работы с SSE - безопасность. Не забывайте применять
авторизацию к SSE-эндпоинтам:
app.MapGet("/secure-stream",
[Authorize(Roles = "Premium")]
(CancellationToken token) =>
TypedResults.ServerSentEvents(GetPremiumEvents(token)))
.RequireAuthorization();
Также стоит ограничивать количество одновременных подключений для
предотвращения DoS- атак:
var connectionCounter = new SemaphoreSlim(1000); // макс. 1000 соединений
app.MapGet("/limited-stream", async (CancellationToken token) =>
{
if (!await connectionCounter.WaitAsync(0))
return Results.StatusCode(503); // Сервис перегружен
try
{
return TypedResults.ServerSentEvents(GetEvents(token));
}
finally
{
connectionCounter.Release();
}
});
Это базовые аспекты настройки SSE в ASP.NET Core, которые помогут
вам построить надежную систему с учетом промышленных требований к
производительности и надежности.

17.

Custom атрибуты для автоматизации
Когда количество SSE-эндпоинтов в приложении растет, начинает
проявляться повторяющийся код и тянучка с настройкой каждого
эндпоинта. В этой ситуации стоит задуматься о создании кастомных
атрибутов:
[AttributeUsage(AttributeTargets.Method)]
public class ServerSentEventAttribute : Attribute
{
public string? EventType { get; set; }
public int BufferSize { get; set; } = 1024;
public TimeSpan Timeout { get; set; } = TimeSpan.FromMinutes(2);
public bool IncludeEventId { get; set; } = true;
}
Теперь создадим метод расширения для IEndpointRouteBuilder,
который будет сканировать контроллеры и регистрировать помеченные
методы. Это значительно сокращает количество кода и упрощает
поддержку большого количества SSE-эндпоинтов.
public static class ServerSentEventsExtensions
{
public static IEndpointRouteBuilder MapServerSentEvents(
this IEndpointRouteBuilder endpoints,
Assembly assembly)
{
var methods = assembly.GetTypes()
.SelectMany(t => t.GetMethods())
.Where(m => m.GetCustomAttribute<ServerSentEventAttribute>() != null)
.ToList();
foreach (var method in methods)
{
var attribute = method.GetCustomAttribute<ServerSentEventAttribute>();
var path = method.GetCustomAttribute<RouteAttribute>()?.Template
?? $"/sse/{method.Name.ToLowerInvariant()}";
endpoints.MapGet(path, async context =>
{
// Настраиваем заголовки
context.Response.Headers.Add("Content-Type", "text/event-stream");
context.Response.Headers.Add("Cache-Control", "no-cache");
context.Response.Headers.Add("Connection", "keep-alive");
// Получаем сервис и вызываем метод
var serviceType = method.DeclaringType!;
var service = context.RequestServices.GetRequiredService(serviceType);
// Предполагаем, что метод возвращает IAsyncEnumerable<string>
var enumerable = (IAsyncEnumerable<string>)method.Invoke(
service, new object[] { context.RequestAborted })!;
var cancellationToken = context.RequestAborted;
// Отправляем события
await foreach (var item in enumerable.WithCancellation(cancellationToken))
{
var builder = new StringBuilder();
if (!string.IsNullOrEmpty(attribute!.EventType))
{
builder.AppendLine($"event: {attribute.EventType}");
}
if (attribute!.IncludeEventId)
{
builder.AppendLine($"id: {Guid.NewGuid()}");
}
builder.AppendLine($"data: {item}");
builder.AppendLine();
await context.Response.WriteAsync(
builder.ToString(), cancellationToken);
await context.Response.Body.FlushAsync(cancellationToken);
}
});
}
return endpoints;
}
}
17 / 20

18.

Custom атрибуты для автоматизации
Использовать это расширение можно в методе Configure:
app.MapServerSentEvents(typeof(Program).Assembly);
А в сервисах просто помечаем методы атрибутом:
public class StockService
{
[ServerSentEvent(EventType = "stock")]
[Route("/stocks/updates")]
public async IAsyncEnumerable<string> GetStockUpdates(
[EnumeratorCancellation] CancellationToken token)
{
while (!token.IsCancellationRequested)
{
yield return JsonSerializer.Serialize(
new { Symbol = "MSFT", Price = Random.Shared.Next(250, 350) });
await Task.Delay(1000, token);
}
}
}
Создадим еще один атрибут для определения параметров ретрая при
обрыве соединения:
[AttributeUsage(AttributeTargets.Method)]
public class SseRetryAttribute : Attribute
{
public int RetryMilliseconds { get; }
public SseRetryAttribute(int retryMilliseconds)
{
RetryMilliseconds = retryMilliseconds;
}
}
Важно отметить, что атрибуты сами по себе не делают ничего - они
просто хранят метаданные. Вся реальная работа происходит в методе
расширения, который я показал выше. Его можно расширить, добавив
обработку дополнительных атрибутов:
// В методе MapServerSentEvents добавляем:
var retryAttribute = method.GetCustomAttribute<SseRetryAttribute>();
if (retryAttribute != null)
{
builder.AppendLine($"retry: {retryAttribute.RetryMilliseconds}");
}

19.

Базовые классы для SSEконтроллеров
После создания атрибутов следующий логичный шаг - разработка
базовых классов для SSE-контроллеров. Это избавит нас от
дублирования кода и обеспечит единый подход к обработке событий
во всем приложении.
Определим базовый интерфейс:
public interface ISseController
{
IAsyncEnumerable<string> GetEventStreamAsync(CancellationToken cancellationToken);
string EventType { get; }
int RetryInterval { get; }
}
А затем создаю абстрактный класс, который его реализует:
public abstract class BaseSseController : ISseController
{
public abstract string EventType { get; }
public virtual int RetryInterval => 3000; // 3 секунды по умолчанию
protected readonly ILogger _logger;
protected BaseSseController(ILogger logger)
{
_logger = logger;
}
public abstract IAsyncEnumerable<string> GetEventStreamAsync(
CancellationToken cancellationToken);
protected ValueTask<string> SerializeEventDataAsync<T>(T data)
{
try
{
return ValueTask.FromResult(JsonSerializer.Serialize(data));
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка сериализации данных события");
throw;
}
}
}
Что дает нам базовый класс? Прежде всего - единообразие в обработке
событий, централизованную обработку ошибок и простую расширяемость.
Каждый специализированный контроллер может сосредоточиться только
на своей бизнес-логике, не заботясь о деталях реализации SSE.

20.

Предварительная проверки соединений и
валидации Origin
Безопасность - это то, чем никогда нельзя пренебрегать, особенно когда
речь идет о событийных потоках. В реальных проектах не раз
сталкивались с ситуациями, когда SSE-эндпоинты оставались полностью
открытыми.
Особое внимание стоит уделить валидации заголовка Origin. Этот
заголовок указывает, с какого домена был инициирован запрос, и его
проверка помогает предотвратить атаки типа CSRF:
app.MapGet("/secure-stream", async (HttpContext context, CancellationToken token) =>
{
// Проверка наличия необходимых заголовков
if (!context.Request.Headers.TryGetValue("X-Client-Id", out var clientId))
{
return Results.BadRequest("Отсутствует идентификатор клиента");
}
// Проверка лимитов соединений для конкретного клиента
if (!await _connectionLimiter.TryAcquireAsync(clientId))
{
return Results.StatusCode(429); // Too Many Requests
}
// Если все проверки пройдены, запускаем SSE
return TypedResults.ServerSentEvents(GetSecureEvents(clientId, token));
});
public class OriginValidationMiddleware
{
private readonly RequestDelegate _next;
private readonly HashSet<string> _allowedOrigins;
public OriginValidationMiddleware(RequestDelegate next, IConfiguration config)
{
_next = next;
_allowedOrigins = new HashSet<string>(
config.GetSection("AllowedOrigins").Get<string[]>() ?? Array.Empty<string>());
}
public async Task InvokeAsync(HttpContext context)
{
// Проверяем только SSE-запросы
if (context.Request.Path.StartsWithSegments("/sse"))
{
var origin = context.Request.Headers.Origin.ToString();
if (string.IsNullOrEmpty(origin) || !_allowedOrigins.Contains(origin))
{
context.Response.StatusCode = 403;
return;
}
}
await _next(context);
}
}
Особое внимание стоит уделить валидации заголовка Origin. Этот
заголовок указывает, с какого домена был инициирован запрос, и его
проверка помогает предотвратить атаки типа CSRF:
Такая реализация гарантирует, что только доверенные домены могут
подключаться к SSE- эндпоинтам, что критически важно для защиты от
межсайтовых атак.

21.

Буферизация и управление очередью
Буферизация данных и управление размером очереди событий критически важные аспекты при работе с SSE. В интернете много
примеров, как неправильно настроенная буферизация превращала
шустрое приложение в неповоротливого монстра, пожирающего память.
Для решения этой проблемы обычно используются каналы с
ограниченной ёмкостью:
public class BoundedEventChannel<T>
{
private readonly Channel<T> _channel;
private readonly ChannelWriter<T> _writer;
private readonly ChannelReader<T> _reader;
public BoundedEventChannel(int capacity,
BoundedChannelFullMode fullMode = BoundedChannelFullMode.Wait)
{
_channel = Channel.CreateBounded<T>(new BoundedChannelOptions(capacity)
{
FullMode = fullMode,
SingleWriter = false,
SingleReader = false
});
_writer = _channel.Writer;
_reader = _channel.Reader;
}
public ValueTask WriteAsync(T item, CancellationToken ct = default)
{
return _writer.WriteAsync(item, ct);
}
public IAsyncEnumerable<T> ReadAllAsync(CancellationToken ct = default)
{
return _reader.ReadAllAsync(ct);
}}
Параметр fullMode позволяет гибко настраивать поведение при переполнении:
Wait - блокирует запись до освобождения места
DropWrite - отбрасывает новые события при переполнении
DropOldest - вытесняет старые события
Не забывайте про буферизацию на уровне HTTP. По умолчанию ASP.NET Core
буферизирует ответы, что критично для производительности SSE. Для отключения
буферизации добавьте:
app.Use(async (context, next) =>
{
context.Response.BufferOutput = false;
await next();
});
Размер буфера сетевого потока тоже можно настроить через Kestrel:
builder.WebHost.ConfigureKestrel(options =>
{
options.Limits.MaxResponseBufferSize = 64 * 1024; // 64 KB
});
Такие тонкие настройки сильно влияют на производительность, особенно при
большом количестве медленных клиентов или при передаче объемных данных.

22.

Middleware для логирования и аудита
Логирование и аудит - те вещи, без которых не существует серьезных
промышленных систем. Особенно когда речь идет о долгоживущих
соединениях вроде SSE.
Можно создавать специальный middleware для логирования SSEактивности:
public class SseAuditMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<SseAuditMiddleware> _logger;
public SseAuditMiddleware(RequestDelegate next, ILogger<SseAuditMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
if (context.Request.Path.StartsWithSegments("/sse"))
{
var clientIp = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
var userAgent = context.Request.Headers.UserAgent.ToString();
var userId = context.User.Identity?.IsAuthenticated == true
? context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value
: "anonymous";
using var loggingScope = _logger.BeginScope(new Dictionary<string, object>
{
["ClientIp"] = clientIp,
["UserAgent"] = userAgent,
["UserId"] = userId,
});
_logger.LogInformation("SSE connection established");
["ConnectionId"] = context.Connection.Id
var originalBodyStream = context.Response.Body;
using var responseBody = new MemoryStream();
context.Response.Body = responseBody;
var startTime = DateTime.UtcNow;
var eventCounter = 0;
context.Response.OnStarting(() =>
{
context.Response.Headers["X-SSE-Audit-Id"] = Guid.NewGuid().ToString();
return Task.CompletedTask;
});
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in SSE connection");
throw;
}
finally
{
var duration = DateTime.UtcNow - startTime;
_logger.LogInformation(
"SSE connection closed after {Duration}. Events sent: {EventCount}",
duration, eventCounter);
}
}
else
{
await _next(context);
}
}
}
Такая система дает полную картину активности SSE в приложении. Ее можно дополнить
визуализацией в Grafana для удобного отслеживания трендов и аномалий.

23.

Заключение и рекомендации
Итоги и ключевые выводы:
1. Производительность: SSE значительно эффективнее традиционных
подходов по всем ключевым метрикам
2. Простота внедрения: .NET 10 предоставляет нативную поддержку
через ServerSentEventsResult
3. Масштабируемость: Правильно настроенная система SSE может
обслуживать тысячи одновременных подключений
4. Надежность: Автоматическое восстановление соединений и
устойчивость к сетевым сбоям
5. Безопасность: Возможность интеграции со всеми стандартными
механизмами аутентификации и авторизации ASP.NET Core
Рекомендации для production-использования:
1. Всегда настраивайте ограничения на количество подключений
2. Реализуйте систему мониторинга и логирования
3. Используйте bounded channels для контроля использования
памяти
4. Настройте правильные таймауты и keep-alive
5. Тестируйте под нагрузкой с реалистичными сценариями
SSE - мощная технология, которая при правильном использовании может
существенно улучшить пользовательский опыт и снизить нагрузку на
инфраструктуру. В .NET 10 она получила первоклассную поддержку, что
делает ее отличным выбором для реализации систем уведомлений и
потоковой передачи данных в реальном времени.
English     Русский Правила