在 .NET 应用程序中,你可能需要实现从后端到前端的实时更新功能。你有几种选择可以实现这一点:
每几秒钟轮询端点可能会使服务器过载并浪费带宽,而对于简单的单向更新来说,全双工的 WebSockets 可能又过于复杂。
服务器发送事件 (SSE) 为 ASP.NET Core 应用程序提供了一种轻量级、可靠的方法来推送连续的数据流,而无需双向协议的复杂性。
今天,我想向你展示如何在 .NET 10 中使用服务器发送事件:
Last-Event-ID 标头处理重连让我们开始吧!
在我的网站上,我分享 .NET 和架构的最佳实践。 订阅我的新闻通讯以提高你的 .NET 技能。 免费下载本次新闻通讯的源代码
服务器发送事件 (SSE) 是一种 Web 标准,使服务器能够通过单个 HTTP 连接将实时数据推送到 Web 客户端。与客户端必须重复轮询服务器以获取更新的传统请求-响应模式不同,SSE 允许服务器在有新信息可用时主动发起通信并发送数据。
SSE 的主要特性:
text/event-stream MIME 类型。不需要特殊的 WebSocket 握手。SSE 支持情况?
因为 SSE 通过纯 HTTP 工作,所有主流浏览器都支持它。你也可以在 IDE 中使用 HTTP 请求文件以及像 curl、Postman、Apidog 这样的工具来测试 SSE。
常见用例:
当你需要从服务器向客户端推送更新,但不需要双向通信时,SSE 是完美的选择。它比 WebSockets 更简单,并且可以与现有的 HTTP 基础设施无缝协作。
从 .NET 10 预览版 4 开始,ASP.NET Core 增加了对服务器发送事件的支持。在底层,它将 Content-Type 设置为 text/event-stream,处理刷新,并与取消令牌集成。
你需要下载 .NET 10 SDK 预览版才能开始使用 SSE
让我们创建一个生成股票价格更新异步流的 StockService:
public record StockPriceEvent(string Id, string Symbol, decimal Price, DateTime Timestamp);
public class StockService
{
public async IAsyncEnumerable<StockPriceEvent> GenerateStockPrices(
[EnumeratorCancellation] CancellationToken cancellationToken)
{
var symbols = new[] { "MSFT", "AAPL", "GOOG", "AMZN" };
while (!cancellationToken.IsCancellationRequested)
{
// 随机选择一个符号和价格
var symbol = symbols[Random.Shared.Next(symbols.Length)];
var price = Math.Round((decimal)(100 + Random.Shared.NextDouble() * 50), 2);
var id = DateTime.UtcNow.ToString("o");
yield return new StockPriceEvent(id, symbol, price, DateTime.UtcNow);
// 发送下一个更新前等待 2 秒
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
}
}
}
此方法以固定间隔生成一个无限的 IAsyncEnumerable 流,包含 StockPriceEvent 项。
我们可以使用 TypedResults.ServerSentEvents 结果来发送服务器发送事件。
让我们创建一个发送股票价格更新 SSE 的最小 API 端点:
builder.Services.AddSingleton<StockService>();
app.MapGet("/stocks", (StockService stockService, CancellationToken ct) =>
{
return TypedResults.ServerSentEvents(
stockService.GenerateStockPrices(ct),
eventType: "stockUpdate"
);
});
SSE 最强大的功能之一是自动重连。当连接断开时,浏览器会自动尝试重新连接,并可以使用 Last-Event-ID 标头从断开的地方恢复。
如果连接丢失,浏览器将重新打开流并包含 Last-Event-ID:
Last-Event-ID: 20250616T150430Z
在后端,我们可以检查 HttpRequest.Headers["Last-Event-ID"] 来确定从哪里恢复。你可以跳过旧项、重放错过的条目或记录重连事件。
以下是实现此类逻辑的方法:
app.MapGet("/stocks2", (
StockService stockService,
HttpRequest httpRequest,
CancellationToken ct) =>
{
// 1. 读取 Last-Event-ID (如果有)
var lastEventId = httpRequest.Headers.TryGetValue("Last-Event-ID", out var id)
? id.ToString()
: null;
// 2. 可选择记录或处理恢复逻辑
if (!string.IsNullOrEmpty(lastEventId))
{
app.Logger.LogInformation("Reconnected, client last saw ID {LastId}", lastEventId);
}
// 3. 使用 lastEventId 和 retry 流式传输 SSE
var stream = stockService.GenerateStockPricesSince(lastEventId, ct)
.Select(evt =>
{
var sseItem = new SseItem<StockPriceEvent>(evt, "stockUpdate")
{
EventId = evt.Id
};
return sseItem;
});
return TypedResults.ServerSentEvents(
stream,
eventType: "stockUpdate"
);
});
在这里,我们创建 SseItem 并指定事件标识符;当重连发生时,此标识符将由客户端在 Last-Event-ID 标头中发送。
几乎每个 IDE (Visual Studio, Visual Studio Code, JetBrains Rider) 都支持 HTTP 请求文件,你可以用它来测试你的 API 端点。并且它们支持服务器发送事件。
@ServerSentEvents_HostAddress = http://localhost:5000
### Test SSE stream from .NET 10 Minimal API
GET {{ServerSentEvents_HostAddress}}/stocks
Accept: text/event-stream
让我们运行应用程序并发送请求。你将每 2 秒收到一个 JSON 格式的新事件:
Response code: 200 (OK); Time: 410ms (410 ms)
event: stockUpdate
data: {"id":"2025-06-16T05:31:10.5426180Z","symbol":"AMZN","price":122.67,"timestamp":"2025-06-16T05:31:10.5445659Z"}
event: stockUpdate
data: {"id":"2025-06-16T05:31:12.5838704Z","symbol":"AAPL","price":118.88,"timestamp":"2025-06-16T05:31:12.5838771Z"}
event: stockUpdate
data: {"id":"2025-06-16T05:31:14.5937683Z","symbol":"AAPL","price":104.01,"timestamp":"2025-06-16T05:31:14.593772Z"}
我们的 SSE 端点按预期工作。
现在,让我们构建一个简单的前端应用程序来使用 SSE。
你可以使用原生的 EventSource API 在前端使用服务器发送事件。
让我们创建一个简单的 HTML 页面,使用一些 Tailwind CSS 样式显示股票价格更新:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Live Stock Ticker</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="styles.css" rel="stylesheet">
</head>
<body class="bg-gray-50 min-h-screen p-8">
<div class="max-w-4xl mx-auto">
<h1 class="text-3xl font-bold text-gray-800 mb-6 flex items-center">
📈<span class="ml-2">Live Stock Market Updates</span>
</h1>
<div class="bg-white rounded-lg shadow-md p-6">
<ul id="updates" class="divide-y divide-gray-200"></ul>
</div>
</div>
<script src="scripts.js"></script>
</body>
</html>
我们可以使用 EventSource 在 JavaScript 中订阅 "stockUpdate" 事件:
// 1. 连接到 SSE 端点
const source = new EventSource('http://localhost:5000/stocks');
// 2. 监听我们命名的 "stockUpdate" 事件
source.addEventListener('stockUpdate', e => {
// 解析 JSON 负载
const { symbol, price, timestamp } = JSON.parse(e.data);
// 创建并前置一个带有 Tailwind 类的新列表项
const li = document.createElement('li');
li.classList.add('new', 'flex', 'justify-between', 'items-center');
// 创建时间元素
const timeSpan = document.createElement('span');
timeSpan.classList.add('text-gray-500', 'text-sm');
timeSpan.textContent = new Date(timestamp).toLocaleTimeString();
// 创建符号元素
const symbolSpan = document.createElement('span');
symbolSpan.classList.add('font-medium', 'text-gray-800');
symbolSpan.textContent = symbol;
// 创建价格元素
const priceSpan = document.createElement('span');
priceSpan.classList.add('font-bold', 'text-green-600');
priceSpan.textContent = `$${price}`;
// 将所有元素附加到列表项
li.appendChild(timeSpan);
li.appendChild(symbolSpan);
li.appendChild(priceSpan);
const list = document.getElementById('updates');
list.prepend(li);
// 片刻后移除高亮
setTimeout(() => li.classList.remove('new'), 2000);
});
// 3. 处理错误和自动重连
source.onerror = err => {
console.error('SSE connection error:', err);
};
// 4. (可选) 检查最后收到的事件 ID
source.onmessage = e => {
console.log('Last Event ID now:', source.lastEventId);
};
工作原理:
new EventSource(url) 打开一个到 /stocks 端点的持久 HTTP 连接,带有 Accept: text/event-stream 标头。addEventListener('stockUpdate', …) 监听 stockUpdate 事件。source.lastEventId — 表示最后一个事件的 id: 值,可用于调试或自定义逻辑。Last-Event-ID。为了能在本地测试,我们需要在 Program.cs 的开发模式下允许 CORS 策略:
if (builder.Environment.IsDevelopment())
{
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowFrontend", policy =>
{
policy.WithOrigins("*")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
}
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseCors("AllowFrontend");
}
这是我们每 2 秒接收股票更新页面的样子:
(此处原为图片占位符 Press enter or click to view image in full size)
这是在 Web 浏览器 DevTools 的 Network 选项卡中的样子:
(此处原为图片占位符 Press enter or click to view image in full size)
SSE 通过纯 HTTP/1 工作,并且 Web 浏览器原生支持它;调试和测试这些事件非常容易。如果你曾经尝试查看或调试 WebSocket 事件,那并不简单,需要第三方软件工具。
虽然服务器发送事件 (SSE) 和 SignalR 都支持在 ASP.NET Core 中进行实时消息传递,但它们针对不同的场景,并在复杂性、功能和资源使用方面有不同的权衡。
以下是它们的区别:
| 特性 | 服务器发送事件 (SSE) | SignalR |
| :--- | :--- | :--- |
| 协议 | HTTP/1.1 流 (text/event-stream) | WebSocket (带有 HTTP 回退传输) |
| 通信方向 | 单向 (仅 服务器 → 客户端) | 全双工 (双向) |
| 浏览器支持 | 大多数现代浏览器原生支持 | 原生 WebSocket + 通过长轮询回退 |
| 连接开销 | 单个 HTTP 请求,最小帧开销 | WebSocket 握手 + 帧管理 |
| 自动重连 | 内置可配置重试 | 内置可配置重试 |
| 消息类型 | 仅文本 | 二进制或文本 |
| 服务器 API | 最小化: TypedResults.ServerSentEvents | 丰富: Hubs, 强类型方法, 组 |
| 可扩展性 | 像任何 HTTP 端点一样扩展 | 使用背板扩展 (Redis/Azure SignalR) |
| 用例 | 单向更新: 通知、警报、股票行情、日志 | 交互式: 聊天应用、协作工具、带用户输入的实时仪表板 |
当你只需要从服务器向客户端推送更新时,服务器发送事件 (SSE) 是 SignalR 的一个易于集成的替代方案。
何时选择 SSE:
何时选择 SignalR: