引言
在实时Web应用开发中,Server-Sent Events(SSE)作为一种轻量级的服务器推送技术,正成为提升用户体验的重要方案。SSE基于HTTP协议,允许服务器通过持久连接主动向客户端推送数据流,特别适合新闻更新、实时监控等场景。.NET 8通过性能优化和服务器端增强,为SSE提供了更强大的支持,使其成为构建高效实时应用的理想选择。
基本原理
SSE是一种基于HTTP/1.1的单向通信技术,核心是通过text/event-stream MIME类型建立持久连接。客户端使用EventSource接口监听服务器推送的事件流,服务器则以分块编码形式持续发送数据。与轮询相比,SSE显著降低网络开销;与WebSocket相比,SSE实现更简单,特别适合服务器到客户端的单向数据推送。
后端代码实现
1、Controller层代码实现
C#
/// <summary>
/// 建立 SSE 流连接的端点
/// 客户端通过此端点建立持久连接以接收服务器推送的事件
/// </summary>
/// <param name="clientId">客户端唯一标识符(可选)</param>
/// <returns>异步任务</returns>
[HttpGet("stream")]
public async Task StreamEvents([FromQuery] string clientId)
{
// 如果客户端未提供ID,则生成一个唯一的ID
if (string.IsNullOrEmpty(clientId))
{
clientId = Guid.NewGuid().ToString();
}
// 设置响应头以支持 SSE 协议
Response.Headers.Append("Content-Type", "text/event-stream"); // 指定内容类型为 SSE
Response.Headers.Append("Cache-Control", "no-cache"); // 禁用缓存
Response.Headers.Append("Connection", "keep-alive"); // 保持连接
Response.Headers.Append("Access-Control-Allow-Origin", "*"); // 允许跨域访问
// 将当前客户端添加到 SSE 服务管理器中
await _sseService.AddClientAsync(clientId, Response);
// 保持连接打开,直到客户端断开或请求被取消
try
{
// 循环检查请求是否已被取消
while (!HttpContext.RequestAborted.IsCancellationRequested)
{
// 每隔1秒检查一次连接状态
await Task.Delay(1000, HttpContext.RequestAborted);
}
}
catch (OperationCanceledException)
{
_logger.LogInformation($"Client {clientId} disconnected (canceled)");
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error in SSE stream for client: {clientId}");
}
finally
{
await _sseService.RemoveClientAsync(clientId);
_logger.LogInformation($"SSE connection cleaned up for client: {clientId}");
}
}
2、Service代码实现
C#
/// <summary>
/// Server-Sent Events (SSE) 服务接口
/// 提供向客户端推送实时事件的功能
/// </summary>
public interface ISseService
{
/// <summary>
/// 向指定客户端发送事件数据
/// </summary>
/// <param name="clientId">客户端唯一标识符</param>
/// <param name="data">要发送的数据</param>
/// <returns>异步任务</returns>
Task SendEventAsync(string clientId, string data);
/// <summary>
/// 向指定客户端发送指定类型的事件数据
/// </summary>
/// <param name="clientId">客户端唯一标识符</param>
/// <param name="eventType">事件类型</param>
/// <param name="data">要发送的数据</param>
/// <returns>异步任务</returns>
Task SendEventAsync(string clientId, string eventType, string data);
/// <summary>
/// 添加新的 SSE 客户端连接
/// </summary>
/// <param name="clientId">客户端唯一标识符</param>
/// <param name="response">HTTP 响应对象</param>
/// <returns>异步任务</returns>
Task AddClientAsync(string clientId, HttpResponse response);
/// <summary>
/// 移除指定的客户端连接
/// </summary>
/// <param name="clientId">客户端唯一标识符</param>
/// <returns>异步任务</returns>
Task RemoveClientAsync(string clientId);
Task BroadcastAsync(string data);
Task BroadcastAsync(string eventType, string data);
}
/// <summary>
/// SSE 服务实现类
/// 管理所有客户端连接并向其推送实时数据
/// </summary>
public class SseService : ISseService
{
private ILogger<SseService> _logger;
public SseService(ILogger<SseService> logger)
{
_logger = logger;
}
/// <summary>
/// 存储所有活动的客户端连接
/// 使用线程安全的字典存储客户端ID和对应的HttpResponse
/// </summary>
private readonly ConcurrentDictionary<string, HttpResponse> _clients = new();
/// <summary>
/// 添加新的 SSE 客户端连接到管理器
/// </summary>
/// <param name="clientId">客户端唯一标识符</param>
/// <param name="response">HTTP 响应对象</param>
/// <returns>异步任务</returns>
public async Task AddClientAsync(string clientId, HttpResponse response)
{
// 将客户端添加到连接字典中
_clients.TryAdd(clientId, response);
// 发送连接确认事件给客户端
await SendEventAsync(clientId, "connection", $"Connected with ID: {clientId}");
}
/// <summary>
/// 从管理器中移除指定的客户端连接
/// </summary>
/// <param name="clientId">要移除的客户端ID</param>
/// <returns>异步任务</returns>
public async Task RemoveClientAsync(string clientId)
{
// 尝试从字典中移除客户端并获取其响应对象
if (_clients.TryRemove(clientId, out var response))
{
// 完成 HTTP 响应
//await response.CompleteAsync();
// 清空缓冲区并关闭流。这实际上等同于完成了响应。但更常用的是不需要手动调用此方法。
await response.Body.FlushAsync();
}
}
/// <summary>
/// 向指定客户端发送默认类型的事件消息
/// </summary>
/// <param name="clientId">目标客户端ID</param>
/// <param name="data">要发送的数据</param>
/// <returns>异步任务</returns>
public async Task SendEventAsync(string clientId, string data)
{
// 调用重载方法,使用默认的消息类型
await SendEventAsync(clientId, "message", data);
}
/// <summary>
/// 向指定客户端发送指定类型的事件消息
/// </summary>
/// <param name="clientId">目标客户端ID</param>
/// <param name="eventType">事件类型(如:message, notification, update等)</param>
/// <param name="data">要发送的数据内容</param>
/// <returns>异步任务</returns>
public async Task SendEventAsync(string clientId, string eventType, string data)
{
// 检查客户端是否仍然连接
if (_clients.TryGetValue(clientId, out var response))
{
try
{
// 按照 SSE 格式写入事件类型
await response.WriteAsync($"event: {eventType}\n");
// 按照 SSE 格式写入数据内容
await response.WriteAsync($"data: {data}\n\n");
// 刷新响应流,确保数据立即发送到客户端
await response.Body.FlushAsync();
}
catch
{
// 如果发生异常(如客户端断开连接),则移除该客户端
await RemoveClientAsync(clientId);
}
}
}
/// <summary>
/// 向所有连接的客户端广播消息
/// </summary>
/// <param name="data">要广播的数据</param>
/// <returns>异步任务</returns>
public async Task BroadcastAsync(string data)
{
// 为每个客户端创建发送任务并等待全部完成
var tasks = _clients.Keys.Select(clientId => SendEventAsync(clientId, data));
await Task.WhenAll(tasks);
}
/// <summary>
/// 向所有连接的客户端广播指定类型的事件
/// </summary>
/// <param name="eventType">事件类型</param>
/// <param name="data">要广播的数据</param>
/// <returns>异步任务</returns>
public async Task BroadcastAsync(string eventType, string data)
{
// 为每个客户端创建发送任务并等待全部完成
var tasks = _clients.Keys.Select(clientId => SendEventAsync(clientId, eventType, data));
await Task.WhenAll(tasks);
}
}
模拟发送数据
C#
这里只是为了模拟数据,具体可根据自己的业务做出调整
/// <summary>
/// SSE 后台服务
/// 定期向所有连接的客户端推送数据更新
/// </summary>
public class SseBackgroundService : BackgroundService
{
private readonly ISseService _sseService;
/// <summary>
/// 日志记录器,用于记录服务运行状态和错误信息
/// </summary>
private readonly ILogger<SseBackgroundService> _logger;
/// <summary>
/// 构造函数,通过依赖注入获取所需的服务
/// </summary>
/// <param name="scopeFactory">服务作用域工厂</param>
/// <param name="logger">日志记录器</param>
public SseBackgroundService(ISseService sseService, ILogger<SseBackgroundService> logger)
{
_sseService = sseService;
_logger = logger;
}
/// <summary>
/// 后台服务执行方法
/// 定期执行任务直到服务停止
/// </summary>
/// <param name="stoppingToken">服务停止通知令牌</param>
/// <returns>异步任务</returns>
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// 循环执行直到收到停止信号
while (!stoppingToken.IsCancellationRequested)
{
try
{
// 准备要广播的事件数据
var eventData = new
{
timestamp = DateTime.UtcNow, // 当前 UTC 时间戳
message = "Server time update", // 消息内容
temperature = Random.Shared.Next(20, 30) // 模拟温度数据
};
// 将事件数据广播给所有连接的客户端
await _sseService.BroadcastAsync("time-update", JsonSerializer.Serialize(eventData));
// 等待 5 秒后继续下一次循环,支持取消操作
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
catch (Exception ex)
{
// 记录执行过程中发生的异常
_logger.LogError(ex, "Error in SSE background service");
}
}
}
}
其他配置
C#
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
ConfigureAutofac(builder);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
}
/// <summary>
/// 配置 Autofac
/// </summary>
/// <param name="builder"></param>
private static void ConfigureAutofac(WebApplicationBuilder builder)
{
// 配置 Autofac 作为 DI 容器
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
// 配置容器注册
builder.Host.ConfigureContainer<ContainerBuilder>(containerBuilder =>
{
// 重点内容,这里必须要注册成单例模式
containerBuilder.RegisterType<SseService>().As<ISseService>().SingleInstance();
// Autofac注册
containerBuilder.RegisterModule(new AutofacModuleRegister());
// 注册后台服务,项目启动时就会执行
containerBuilder.RegisterType<SseBackgroundService>().As<IHostedService>();
});
}
}
前端代码
JS
<script>
function sendMessage() {
// 连接到 SSE 流
const clientId = 'client-' + Math.random().toString(36).substr(2, 9);
const eventSource = new EventSource(`替换成你的域名/api/sse/stream?clientId=${clientId}`);
eventSource.onopen = function(event) {
console.log('SSE connection opened');
};
eventSource.onmessage = function(event) {
console.log('Message received:', event.data);
};
eventSource.addEventListener('connection', function(event) {
console.log('Connection event:', event.data);
});
eventSource.addEventListener('time-update', function(event) {
const data = JSON.parse(event.data);
console.log('Time update:', data);
document.getElementById('server-time').innerText = data.timestamp;
});
eventSource.onerror = function(event) {
console.error('SSE error occurred');
};
}
</script>
<div class="text-center">
<div>
<input type="button" value="Send message" onclick="sendMessage()" />
</div>
</div>
最后效果
