必须用ASP.NET Core的AcceptWebSocketAsync()或成熟封装,禁用手动HttpListener升级;需先校验IsWebSocketRequest再处理,接收后须检查CloseStatus并主动CloseAsync,缓冲区应复用MemoryPool,心跳间隔≤45秒且代理需透传Upgrade头。

别用 HttpListener 手动升级 WebSocket 握手——90% 的连接失败、400/426 错误和“connection closed before complete handshake”都源于此。稳的路径只有一条:ASP.NET Core 的 HttpContext.WebSockets.AcceptWebSocketAsync() 或第三方成熟封装(如 websocket-sharp),二者选其一,别混搭。
ASP.NET Core 服务端:注册终结点前必须检查 IsWebSocketRequest
直接在 app.Map("/ws", async context => { ... }) 里调 AcceptWebSocketAsync() 会炸——如果请求根本不是 WebSocket 升级请求(比如浏览器误刷了 /ws 地址),就会抛 InvalidOperationException。
- 务必先判断:
if (context.WebSockets.IsWebSocketRequest),否则跳过或返回 400 - 握手成功后拿到的
WebSocket实例是独占的,context不可再读写,后续所有收发必须走它自己的ReceiveAsync()和SendAsync() - 不要在循环里只靠
webSocket.State == WebSocketState.Open判断是否继续接收——状态可能卡在CloseReceived,但没调CloseAsync(),导致客户端等超时 - 每次
ReceiveAsync()返回后,必须检查result.CloseStatus.HasValue;有值就立刻调CloseAsync(result.CloseStatus.Value, ...),这是唯一可靠的断连信号
ClientWebSocket 客户端:ConnectAsync 失败 ≠ 网络不通
ClientWebSocket.ConnectAsync() 抛异常的原因极分散,不能笼统 catch Exception 就重试。
- DNS 解析失败 →
SocketException(ErrorCode = 11001) - SSL 证书不信任 →
HttpRequestException套着WinHttpException(.NET on Windows) - Nginx 代理未透传 Upgrade 头 → 服务端返回 403 或 426,客户端收到
HttpRequestException带 “Forbidden” 或 “Upgrade Required” 响应体 - .NET Framework 4.5–4.7.2 不支持往握手请求加自定义头(如鉴权
Authorization),必须升到 4.8+ 或换方案
收发消息:CloseStatus 和缓冲区复用是关键
很多人卡在“能连上但收不到消息”或“发几次就崩”,问题往往出在消息生命周期管理上。
- 接收端:用
MemoryPool<byte>.Shared.Rent(64 * 1024)</byte>分配缓冲区,别用 new byte[64 * 1024],避免 GC 压力;接收完记得buffer.Return() - 发送二进制帧时,
WebSocketMessageType.Binary要自己处理MemoryStream或ArrayPool<byte>.Shared</byte>,文本帧(Text)必须用Encoding.UTF8.GetString()解码,直接Encoding.UTF8.GetChars()或强制转string会乱码 - 服务端主动断连,必须调
CloseAsync()再 dispose;只 dispose 会导致客户端触发error事件而非close,前端逻辑错乱 - 客户端收到
CloseStatus或WebSocketException(如 ErrorCode = 1006)后,SendAsync()会立即抛ObjectDisposedException,必须清空待发队列并停止发送任务
心跳与重连:没有内置机制,必须业务层实现
ClientWebSocket 没有自动重连,Ping() 方法仅在 .NET 5+ 可用且服务端未必响应——靠它保活等于裸奔。
- 真实做法:每 30 秒发一次业务 ping(如
{"type":"ping","ts":123456789}),同时启动CancellationTokenSource设 10 秒超时等待 pong - 服务端未回 pong 时,不能无限等——弱网下可能阻塞,超时即判定失联,触发重连逻辑
- 重连要退避:首次 1s,失败后 2s、4s、8s… 最大不超过 30s,避免雪崩式重连打垮服务端
- Nginx 等反向代理默认 60 秒断空闲连接,心跳间隔必须小于该值(建议 ≤45s),且代理配置必须含:
proxy_http_version 1.1;、proxy_set_header Upgrade $http_upgrade;、proxy_set_header Connection "upgrade";
最常被忽略的是:WebSocket 连接不是“连上了就万事大吉”,而是从握手、收发、心跳、断连清理到重连,每个环节都有明确的状态跃迁和资源责任归属。漏掉任意一环,都会在高并发或弱网下暴露为偶发性崩溃或静默失联。


















