System.Net.WebSockets 是 .NET 原生唯一可靠 WebSocket 路径;ClientWebSocket 需手动设 Origin 和 SubProtocol,接收需处理分片与 MessageType,关闭前必须调 CloseAsync 并判断状态。

System.Net.WebSockets 是 .NET 原生支持 WebSocket 的唯一可靠路径,别碰第三方封装(除非你明确知道它底层没绕过这个类);WebSocket 类本身不处理重连、心跳、消息分片或协议升级细节,这些都得你自己补。
用 ClientWebSocket 连接服务端时,必须手动设置 Origin 和 SubProtocol
很多服务端(如 Nginx 反向代理后、Spring WebFlux、某些 Node.js 实现)会校验 Origin 头,不设就直接 403;SubProtocol 则是协商通信语义的关键——比如你传 "json",服务端却只认 "v1.chat",连接会静默关闭。
- 设置方式:在
await client.ConnectAsync(uri, cancellationToken)前,调用client.Options.SetRequestHeader("Origin", "https://example.com") -
SubProtocol必须在构造时传入:new ClientWebSocket { Options = { SubProtocols = { "v1.chat" } } } - 注意:
SetRequestHeader对User-Agent等标准头无效,.NET 会覆盖它们
ReceiveAsync 返回 WebSocketReceiveResult,别忽略 MessageType 和 EndOfMessage
WebSocket 消息可能被自动分片(尤其大消息),ReceiveAsync 每次只收一帧。如果只看 buffer 内容,会把半条 JSON 当成完整消息解析,然后报 JsonException: Invalid token。
-
result.MessageType == WebSocketMessageType.Text才能当字符串处理,二进制帧不能直接Encoding.UTF8.GetString() -
result.EndOfMessage == false表示还有后续帧,必须循环ReceiveAsync拼接,直到为true - 缓冲区大小建议 ≥ 8192 字节,太小会导致频繁分片和额外 GC
关闭连接前必须调用 CloseAsync,且要等它完成
直接 Dispose() 或让 GC 回收,会触发 TCP RST,服务端收不到 close frame,可能长时间保留连接状态,甚至拒绝新连接。
- 正确顺序:
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "bye", cancellationToken)→await socket.DisposeAsync() -
CloseAsync超时默认是 30 秒,生产环境建议显式传TimeSpan.FromSeconds(5) - 如果服务端已断开,
CloseAsync会抛WebSocketException,需捕获并忽略(只要不是ObjectDisposedException)
最常被跳过的其实是异常后的资源清理:ReceiveAsync 抛异常时,socket 状态可能是 Aborted 或 CloseReceived,此时再调 CloseAsync 会 throw,得先判断 socket.State == WebSocketState.Open 才执行。










