使用 ClientWebSocket 前需确认 .NET 版本(4.5+/2.1+)及服务端 WebSocket 兼容性,Nginx 等代理须透传 Upgrade 头;收发消息需手动处理 CloseStatus 和异常;无自动重连与心跳,需业务层实现带超时的 ping/pong 及退避重连;多线程发送可并发但接收须串行;关闭前必须先 CloseAsync 再 Dispose。

用 WebSocket 类建立连接前,先确认 .NET 版本和服务器兼容性
如果你用的是 .NET Framework 4.5+ 或 .NET Core 2.1+,System.Net.WebSockets.ClientWebSocket 是首选——它原生、轻量、不依赖第三方包。但别急着写 ConnectAsync,先看服务端是否支持标准 WebSocket 协议(比如是否返回 101 Switching Protocols)。常见错误是本地测试时连上了 ws://localhost:3000,但部署后因反向代理(如 Nginx)没透传升级头(Upgrade、Connection),直接返回 426 Upgrade Required 或静默断连。
- 检查服务端响应头是否含
Upgrade: websocket和Connection: Upgrade - Nginx 配置里必须显式添加:
proxy_http_version 1.1;、proxy_set_header Upgrade $http_upgrade;、proxy_set_header Connection "upgrade"; - .NET Framework 4.5–4.7.2 不支持自定义 HTTP 头注入,无法带鉴权 token;得升到 4.8 或换
System.Net.Http.HttpClient手动发握手请求(不推荐)
发送/接收消息必须手动处理 CloseStatus 和异常中断
ClientWebSocket 不会自动重连,也不区分“网络闪断”和“服务端优雅关闭”。一旦 ReceiveAsync 返回 CloseStatus 非 null,或抛出 WebSocketException(比如 ErrorCode = 1006),连接就已不可用——此时再调 SendAsync 会触发 ObjectDisposedException。
- 每次
ReceiveAsync后必须检查result.CloseStatus != null,然后主动调CloseAsync清理 - 不要在
try/catch里只捕获OperationCanceledException,漏掉WebSocketException会导致后台任务卡死 - 二进制帧(
WebSocketMessageType.Binary)需自己处理MemoryStream或ArrayPool缓冲区,避免频繁 GC;文本帧(.Shared Text)记得用Encoding.UTF8.GetString解码,别直接转string(会乱码)
心跳保活不能只靠 Ping,得结合业务层超时检测
ClientWebSocket 没有内置心跳机制,WebSocket.Ping 方法仅在 .NET 5+ 可用,且服务端未必响应。更现实的做法是:每 30 秒发一次业务级 ping(比如 {"type":"ping"}),同时启动一个 CancellationTokenSource,等待对应 pong 响应;超时即判定失联。
- 别依赖 TCP keep-alive:它默认 2 小时才触发,对即时通讯毫无意义
- 服务端若未返回 pong,
SendAsync可能阻塞(尤其在弱网下),务必设cancellationToken并控制超时时间(建议 ≤10s) - 重连逻辑要退避:首次失败后等 1s,第二次 2s,第三次 4s……避免雪崩式重连请求
多线程读写必须加锁,但别锁整个 ClientWebSocket
ClientWebSocket 的 SendAsync 和 ReceiveAsync 不是线程安全的,但你不需要对整个对象加 lock。实际只需确保「同一时刻只有一个任务在调 ReceiveAsync」,因为它是独占式读取循环;而多个线程并发 SendAsync 是允许的——只要每个调用都用独立的 ArraySegment 或 ReadOnlyMemory。
- 典型写法:用
ConcurrentQueue缓存待发消息,由单个后台任务轮询并SendAsync - 避免在
ReceiveAsync回调里直接反序列化大 JSON(如聊天记录列表),会阻塞接收线程;应把Memory丢进Task.Run处理 - 关闭连接前,务必先
await _webSocket.CloseAsync(...),再Dispose();否则可能触发ObjectDisposedException在ReceiveAsync中
最常被跳过的细节是:没在 AppDomain.CurrentDomain.ProcessExit 或 IHostApplicationLifetime.ApplicationStopping 里触发优雅关闭。进程直接杀掉时,WebSocket 连接不会发 Close 帧,服务端得靠超时清理,这会拖慢用户状态同步。











