最稳路径是asp.net core的mapwebsocketmanager或httpcontext.websockets.acceptwebsocketasync();手动用httplistener升级易漏握手细节,调试困难;接收需检查closestatus而非仅state;发送大消息须分片,接收宜用64kb缓冲区并复用memorypool。

用 System.Net.WebSockets 写服务端,别碰 HttpListener 手动升级
原生 WebSocket 服务端在 C# 中最稳的路径是走 ASP.NET Core 的 MapWebSocketManager 或直接用 HttpContext.WebSockets.AcceptWebSocketAsync()。自己用 HttpListener 拦请求、手动解析 Upgrade 头、做 Sec-WebSocket-Accept 计算——容易漏掉握手细节,比如大小写敏感的 Connection: Upgrade、缺少 Sec-WebSocket-Version: 13 就直接 400,调试时只看到“connection closed before complete handshake”这种模糊错误。
实操建议:
- ASP.NET Core 项目中,在
Startup.Configure或Program.cs里注册一个终结点,例如app.Map("/ws", async context => { ... }) - 检查
context.WebSockets.IsWebSocketRequest再调用AcceptWebSocketAsync(),否则会抛InvalidOperationException - 握手成功后拿到的
WebSocket实例是独占的,不能复用HttpContext,后续读写必须用它自己的ReceiveAsync/SendAsync
WebSocket.ReceiveAsync 必须配合 WebSocketCloseStatus 处理断连
很多人只写 while (webSocket.State == WebSocketState.Open) 循环接收,但网络抖动、客户端关页、NAT 超时都会让连接静默中断,ReceiveAsync 可能永远不返回,或突然抛 WebSocketException 带着 InvalidState。真正的退出信号其实是 CloseStatus —— 客户端发来关闭帧时,ReceiveAsync 会返回 CloseStatus 非空的 WebSocketReceiveResult。
实操建议:
- 每次
ReceiveAsync后检查result.CloseStatus.HasValue,有值就调webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None) - 不要依赖
WebSocketState判断是否存活,它可能卡在CloseReceived却没调CloseAsync,导致对方等超时 - 服务端主动断连时,务必用
CloseAsync而非直接Dispose,否则客户端收不到标准关闭帧,会触发 error 事件而非 close
客户端用 ClientWebSocket 时,ConnectAsync 失败不等于网络不通
ClientWebSocket.ConnectAsync 抛异常常见但原因分散:DNS 失败是 SocketException,SSL 证书错是 HttpRequestException 套着 WinHttpException,跨域被拦是 HttpRequestException 带 “403 Forbidden” 响应体。更隐蔽的是,某些代理或防火墙会伪造 200 响应但不转发 WebSocket 升级头,结果 ConnectAsync 成功返回,一发消息就立刻断开。
实操建议:
- 捕获
WebSocketException、HttpRequestException、OperationCanceledException三类,分别处理重试逻辑 - 连接后立刻发一条 ping(用
SendAsync发WebSocketMessageType.Ping)并等待响应,确认通道真正可用 - 设置
ClientWebSocket.Options.KeepAliveInterval = TimeSpan.FromSeconds(30),避免中间设备单向断连却无感知
消息分片和缓冲区大小直接影响吞吐和内存
WebSocket 协议本身支持消息分片(fragmentation),但 .NET 的 SendAsync 默认把整个 ArraySegment<byte></byte> 当作一帧发出去。如果传入 10MB 的字节数组,它不会自动切片,而是尝试一次性分配大缓冲区,可能触发 OutOfMemoryException;反过来,接收端若用固定 4KB 缓冲区去收 8MB 消息,就得循环 ReceiveAsync 几千次,CPU 和 GC 压力陡增。
实操建议:
- 发送前手动分片:对大于 64KB 的数据,用
WebSocketMessageType.Binary+endOfMessage: false发前 N-1 帧,最后一帧设endOfMessage: true - 接收时别硬写死缓冲区大小,用
new byte[64 * 1024]而非new byte[1024],减少系统调用次数 - 服务端广播场景下,用
MemoryPool<byte>.Shared.Rent()</byte>复用缓冲区,避免高频分配释放
WebSocket 在 C# 里不是“连上就能发”,每个环节都卡在协议细节和资源管理上。最常被跳过的其实是关闭流程的完整性验证和缓冲区生命周期控制——这两处不出问题则已,一出就是偶发、难复现、日志里还找不到线索。











