使用system.net.websockets需严格遵循asp.net core中间件顺序(userouting→usewebsockets→useendpoints),服务端须检查closestatus而非state退出循环,客户端须手动处理分片、utf-8解码及心跳重连。

用 System.Net.WebSockets 做 C# WebSocket 实时通信,服务端必须走 ASP.NET Core 中间件链,客户端必须手动处理分片和 CloseStatus——跳过任一环节,连接大概率在弱网、NAT 或页面刷新后静默失效。
ASP.NET Core 服务端注册顺序不能错
光调 AddWebSockets() 不够,UseWebSockets() 必须紧接在 UseRouting() 之后、UseEndpoints() 之前。顺序错了,context.WebSockets.IsWebSocketRequest 永远是 false,后续 AcceptWebSocketAsync() 直接抛 InvalidOperationException。
实操建议:
- 在
Program.cs里按顺序写:app.UseRouting(); app.UseWebSockets(); app.UseEndpoints(...) - 终结点内必须先判断:
if (context.WebSockets.IsWebSocketRequest),再调await context.WebSockets.AcceptWebSocketAsync() - 拿到的
WebSocket实例是独占的,不能复用context,所有读写只能走它自己的ReceiveAsync()和SendAsync()
ReceiveAsync 必须检查 CloseStatus,不是 State
只靠 while (ws.State == WebSocketState.Open) 循环接收,会卡死或假活:网络中断后 State 可能卡在 CloseReceived,但 ReceiveAsync() 不返回也不报错;客户端关页时,服务端收不到关闭帧,就一直等下去。
真正可靠的退出信号是 WebSocketReceiveResult.CloseStatus:
- 每次
ReceiveAsync()返回后,立刻检查result.CloseStatus.HasValue - 有值就立刻调
await ws.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None) - 别等超时、别依赖
State判断存活,它不反映真实连接状态
ClientWebSocket 连接后必须处理分片和 UTF-8 解码
ClientWebSocket 不自动拼合分片,也不帮你转编码。一次 ReceiveAsync() 只读一帧,result.EndOfMessage == false 就说明还有后续帧;直接用 Encoding.UTF8.GetString(buffer, 0, result.Count),别用 new string(),否则中文全乱码。
常见错误现象:
- JSON 解析失败——其实只是消息被拆成两帧,第二帧没读
- 收到半个中文字符——用了错误的字符串构造方式
- 大消息发一半就断——服务端未按 WebSocket 协议要求分片发送
发送大消息时,服务端必须手动分片(单帧 ≤ 64KB),客户端接收缓冲区建议固定为 64KB 并复用 MemoryPool<byte>.Shared</byte>。
心跳和重连不是可选项,是弱网下的生存线
ClientWebSocket 没内置心跳,Ping() 方法在 .NET 5+ 才有,且服务端未必响应。NAT 超时、代理静默断连、手机切后台,都会让 State 保持 Open 却无法通信。
业务层必须自己实现:
- 每 30 秒发一个
{"type":"ping"}文本帧 - 启动
CancellationTokenSource等待pong响应,超时(≤10s)即判定失联 - 重连用指数退避:第一次 1s,第二次 2s,第三次 4s……避免雪崩式重试
-
ConnectAsync()抛异常不能吞掉:SocketException(DNS/网络不通)、HttpRequestException(证书错、403、426)要分类捕获并响应
最易被忽略的是:服务端主动断连时,必须调 CloseAsync(),不能直接 Dispose()——否则客户端收不到标准关闭帧,触发的是 error 事件而非 close。











