在构建需要服务端主动向客户端发送数据的应用时,.NET 开发者通常会面对多种实时通信技术的选择。不同的方案在协议层级、客户端兼容性和开发复杂度上差异明显,理解它们的底层机制和适用场景,能帮助我们避免后期重构。

WebSocket 原生通信
WebSocket 是 HTML5 定义的全双工通信协议,它在一次 HTTP 握手后,将连接升级为长连接,之后客户端与服务端可以双向自由发送数据帧。在 .NET 中,可以通过 HttpListener 或 ASP.NET Core 的 WebSocket 中间件来接受原始连接。
使用原生 WebSocket 意味着你要自己处理消息分片、心跳保活、连接状态机和断线重连。虽然控制力最强,但开发量也最大。下面的代码展示了在 ASP.NET Core 中接受 WebSocket 并回显消息的最小示例:
public class WebSocketHandler
{
public async Task HandleAsync(HttpContext context)
{
if (!context.WebSockets.IsWebSocketRequest)
{
context.Response.StatusCode = 400;
return;
}
using var ws = await context.WebSockets.AcceptWebSocketAsync();
var buffer = new byte[1024];
while (ws.State == WebSocketState.Open)
{
var result = await ws.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Close)
{
await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "bye", CancellationToken.None);
}
else
{
await ws.SendAsync(new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType, true, CancellationToken.None);
}
}
}
}
原生 WebSocket 的优势在于不依赖任何高级框架,跨语言互通性极好。缺点也很突出:没有内置的路由、组播和序列化机制,在大型项目中容易写出难以维护的通信层。
SignalR 高级封装
SignalR 是微软为 .NET 打造的实时通信库,它在 WebSocket 之上提供了集线器(Hub)抽象、自动协议协商和传输降级。如果客户端不支持 WebSocket,SignalR 可以回退到 Server-Sent Events 或长轮询,对开发者完全透明。
通过 Hub,我们可以用强类型方法直接调用客户端函数,而不必手动解析消息。下面的例子定义了一个简单的通知 Hub,并向前端推送消息:
public class NotifyHub : Hub
{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
// 在 Program.cs 中映射
app.MapHub<NotifyHub>("/notifyHub");
SignalR 还内置了连接分组(Groups)和用户信息映射,适合聊天室、实时仪表盘和协同编辑等场景。它的不足是包体相对较大,且在极低延迟要求的物联网场景中,抽象层可能引入微小开销。
gRPC 双向流
gRPC 基于 HTTP/2,使用 Protocol Buffers 作为接口定义和序列化格式。在 .NET 中,gRPC 支持客户端流、服务端流和双向流,非常适合服务间高性能实时调用。
与 WebSocket 相比,gRPC 的契约先行的设计能在编译期发现接口错误。下面的 proto 定义描述了一个双向流服务:
syntax = "proto3";
service Realtime {
rpc Chat (stream Message) returns (stream Message);
}
message Message {
string user = 1;
string content = 2;
}
在 .NET 客户端中,我们可以通过异步流读取和写入消息:
using var channel = GrpcChannel.ForAddress("https://ipipp.com");
var client = new Realtime.RealtimeClient(channel);
using var call = client.Chat();
_ = Task.Run(async () =>
{
await foreach (var msg in call.ResponseStream.ReadAllAsync())
{
Console.WriteLine($"{msg.User}: {msg.Content}");
}
});
await call.RequestStream.WriteAsync(new Message { User = "a", Content = "hi" });
gRPC 的劣势在于浏览器原生不支持,需要 gRPC-Web 代理;且对调试人员的要求高于文本协议。它更适合内部微服务或移动端到服务端的实时链路。
Server-Sent Events
Server-Sent Events(SSE)是基于纯 HTTP 的单向推送技术,服务端以 text/event-stream 格式持续输出数据,浏览器通过 EventSource 对象接收。在 .NET 中可手动实现,也可借助 Minimal API 返回流。
SSE 不需要特殊握手,穿透代理和负载均衡更友好,但只能服务端推客户端。简单示例:
app.MapGet("/stream", async (HttpContext context) =>
{
context.Response.Headers.Add("Content-Type", "text/event-stream");
for (int i = 0; i < 10; i++)
{
await context.Response.WriteAsync($"data: {i}nn");
await context.Response.Body.FlushAsync();
await Task.Delay(1000);
}
});
当业务只需要行情播报、日志推送等单向场景时,SSE 比 WebSocket 更轻量。但注意旧版 IE 不支持 EventSource,且连接数在 HTTP/1.1 下受限。
技术选型对照
为了更直观地比较,我们把四种选项的核心特征整理成表:
| 技术 | 方向 | 协议基础 | 典型场景 |
|---|---|---|---|
| WebSocket | 双向 | 独立TCP升级 | 自定义通信协议 |
| SignalR | 双向 | WebSocket等 | 应用级实时功能 |
| gRPC流 | 双向 | HTTP/2 | 服务间高性能调用 |
| SSE | 单向 | HTTP | 服务端广播 |
实际项目中,不少团队会用 SignalR 处理 Web 前端实时性,用 gRPC 流打通后端服务,两者互补。明确消息方向、客户端类型和运维能力,是做出合理决策的前提。