ASP.NET Core WebSocket生产部署必须严格遵循中间件顺序(UseWebSockets()在UseRouting()后、UseEndpoints()前),显式校验IsWebSocketRequest,正确处理分片、UTF-8解码、CloseStatus响应及心跳保活,否则极易因握手失败、乱码、假活或资源耗尽导致连接异常中断。

别用 HttpListener 手动升级 WebSocket,99% 的生产问题都出在这一步——握手漏头、大小写错、版本没校验,错误日志只显示 “connection closed before complete handshake”,根本没法定位。
ASP.NET Core 服务端必须配对启用和注册
光调 AddWebSockets() 不够,UseWebSockets() 必须出现在 UseRouting() 之后、UseEndpoints() 之前,否则中间件链断掉,请求压根进不到 WebSocket 处理逻辑里。
-
app.UseWebSockets()要放在路由之后,否则context.WebSockets.IsWebSocketRequest永远为false - 终结点注册必须显式判断
IsWebSocketRequest,不检查就直接AcceptWebSocketAsync()会抛InvalidOperationException - 每个连接拿到的
WebSocket实例是独占的,不能复用HttpContext,后续所有读写只能走它自己的ReceiveAsync/SendAsync
ClientWebSocket 连接后必须手动处理分片和 CloseStatus
很多人写一次 ReceiveAsync 就解析消息,结果 JSON 解析失败、中文乱码、消息截断——其实只是没等完分片。WebSocket 协议允许单条消息拆成多个帧发送,result.EndOfMessage == false 就说明还有后续帧。
- 用
List缓存所有分片,直到result.EndOfMessage == true才拼接解码 - 文本帧必须用
Encoding.UTF8.GetString(buffer, 0, result.Count),不能直接new string(...),否则乱码 - 每次
ReceiveAsync后必须检查result.CloseStatus.HasValue,有值就得立刻调CloseAsync,不能只靠WebSocketState.Open判断存活 -
WebSocketState可能卡在CloseReceived状态,不调CloseAsync会导致对方等超时
心跳和重连不是可选项,是弱网下的生存线
ClientWebSocket 没有内置心跳,Ping() 方法在 .NET 5+ 才有,且服务端未必响应。真实场景下,NAT 超时、代理静默断连、客户端切后台,都会让连接“假活”——State 还是 Open,但发不出也收不到任何数据。
- 业务层必须实现带超时的 ping/pong:每 30 秒发
{"type":"ping"},启动CancellationTokenSource等 pong,超时(建议 ≤10s)即判定失联 - 重连要用指数退避,别上来就 100ms 重试,连续失败后应逐步拉长间隔
-
ConnectAsync抛异常不能吞掉:SocketException(DNS 或网络不通)、HttpRequestException(证书错、403、426)要分类处理 - 发送前必须检查
clientWebSocket.State == WebSocketState.Open,别依赖try/catch捕获发送失败
最易被忽略的是缓冲区复用和大消息分片:接收用 64KB 固定缓冲区比每次 new 更稳;发送超过 128KB 的消息必须手动分片,否则服务端可能直接断连。这些细节不写进业务逻辑,上线后扛不住并发或弱网就立刻暴露。











