应优先使用Microsoft.AspNetCore.SignalR而非System.Net.WebSockets原生API;SignalR内置路由、广播、重连、心跳及并发安全封装,支持WebSocket传输并自动降级,适用于95%实时业务场景。

别用 System.Net.WebSockets 原生 API 写业务服务端——它只管连接,不管路由、广播、重连、心跳、并发安全,硬刚等于重复造半个 SignalR。
该用 SignalR 还是裸 WebSocket?看这三点
如果你要实现的是“用户登录后实时收消息”“群聊广播”“断网自动重连”“服务端主动推通知”,直接上 Microsoft.AspNetCore.SignalR。它默认优先走 WebSocket 传输,同时内置 fallback(SSE / Long Polling),还自带连接管理、分组、用户标识、跨请求推送等能力。
裸 WebSocket(HttpContext.WebSockets.AcceptWebSocketAsync() 或 ClientWebSocket)只适合以下极少数场景:
- 你在写协议转换网关或嵌入式设备直连桥接
- 你明确禁用 SignalR(比如因安全策略不允许 Hub 自动注册路由)
- 你在调试底层帧交互,或做性能压测基准线
95% 的业务实时通信需求,SignalR 是唯一合理选择。
SignalR 服务端必须配的两个关键项
很多连接失败不是代码错,而是 Kestrel 和 WebSocket 协议层没对齐。以下两处漏一不可:
-
WebHost.CreateDefaultBuilder()中必须启用 WebSocket:在ConfigureKestrel里加opt.WebSocket = new WebSocketOptions { KeepAliveInterval = TimeSpan.FromSeconds(60) } - 服务端响应头必须含
Upgrade: websocket和Connection: Upgrade—— 如果前面有 Nginx,需显式透传:proxy_set_header Upgrade $http_upgrade;和proxy_set_header Connection "upgrade";
否则客户端会卡在 Failed to start the connection: Error: Unable to initialize any of the available transports,浏览器控制台只报错不给原因。
ClientWebSocket 连不上?先查三类异常类型
ClientWebSocket.ConnectAsync() 抛异常 ≠ 网络不通,不同错误对应不同根因:
- DNS 解析失败 →
SocketException(ErrorCode 11001) - SSL 证书不信任或域名不匹配 →
HttpRequestException套着WinHttpException(Message 含 “The certificate authority is invalid”) - 跨域被代理/网关拦截 →
HttpRequestException带403 Forbidden响应体,且无Sec-WebSocket-Accept头
.NET Framework 4.5–4.7.2 还有个隐藏坑:不支持向握手请求注入自定义 Header(如鉴权 token),必须升到 4.8+ 或换方案。
原生 WebSocket 接收循环里最常漏的退出条件
很多人写接收逻辑只判 webSocket.State == WebSocketState.Open,但这个状态可能长期卡在 CloseReceived 却没触发关闭流程,导致对方等超时、连接滞留。
真正可靠的退出信号是 WebSocketReceiveResult.CloseStatus:
- 每次
ReceiveAsync()返回后,必须检查result.CloseStatus.HasValue - 有值就立刻调
CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None) - 不要等
WebSocketException(比如 ErrorCode 1006)再处理——那时连接已不可用,再发SendAsync()会抛ObjectDisposedException
另外,ReceiveAsync() 是单次读取,文本帧需用 Encoding.UTF8.GetString(buffer, 0, result.Count),别直接 new string(...),否则乱码;二进制帧建议复用 ArrayPool<byte>.Shared</byte> 缓冲区,避免 GC 压力。
复杂点在于:WebSocket 没有内置心跳,Ping/Pong 帧在 .NET 5+ 才有公开 API,且服务端未必响应。业务级保活得自己发 {"type":"ping"} 并设超时等待 pong,失败后按退避策略重连(1s→2s→4s…),不然部署后一断网就雪崩。


















