WinForm中必须用ClientWebSocket.ConnectAsync异步连接、后台线程运行ReceiveAsync循环接收、状态校验防重复连接、UI更新用Invoke切换线程、关闭时调用CloseAsync并释放资源,否则会导致UI卡死、丢包、崩溃。

WinForm 用 ClientWebSocket 接 WebSocket 服务端,不是“能连上就行”,而是必须异步 + 线程安全 + 状态防护,否则 UI 卡死、接收丢包、重复连接崩溃是常态。
为什么不能直接用 Connect 同步方法
WinForm 是单线程 UI 模型,ClientWebSocket.Connect 是同步阻塞调用,一旦网络延迟或 DNS 解析慢,整个界面会卡住几秒甚至十几秒,用户点按钮无响应、窗口假死。.NET 原生只提供 ConnectAsync,没有同步入口——强行封装同步会丢失取消能力,且无法适配超时控制。
常见错误现象:
• 点“连接”按钮后窗体冻结,任务管理器显示“未响应”
• 连接成功但收不到任何 OnMessage 回调(因为没启动接收循环)
• 多次点击连接按钮触发多个并发 ConnectAsync,服务器端看到重复握手
- 必须用
await client.ConnectAsync(new Uri(url), token),配合CancellationTokenSource控制超时 - 连接成功后,立刻启动独立的接收循环(
Task.Run(() => ReceiveLoop())),不能在 UI 线程里await ReceiveAsync - 每次点击连接前,先检查
client.State:只允许从Created或Aborted状态发起新连接;若已是Open,应禁用按钮或提示“已连接”
ReceiveAsync 必须循环调用且不能 await 在 UI 线程
ReceiveAsync 不是“注册一次就自动触发”的事件,它是一次性读取操作:调用一次,最多读一个消息帧,然后返回。不手动循环调用,后续消息永远进不来。而如果在 Form_Load 或按钮事件里 await ReceiveAsync(...),UI 线程会被挂起等待数据,导致界面冻结。
使用场景:
• 实时监控类应用(如设备状态推送)需持续监听
• 消息可能间隔数秒到数分钟,不能靠定时轮询模拟
- 接收循环必须放在后台线程(
Task.Run或ThreadPool.QueueUserWorkItem),且内部用while (client.State == WebSocketState.Open)持续调用 - 每次
ReceiveAsync前,确保 buffer 重置(buffer.Array?.Clear()或新建ArraySegment<byte></byte>) - 收到数据后,反序列化和 UI 更新必须切回 UI 线程:
this.Invoke((MethodInvoker)delegate { txtLog.AppendText(msg); }) - 捕获
WebSocketException和ObjectDisposedException:前者表示连接异常断开,后者说明 client 已被释放
JSON 数据收发要注意编码与边界
WebSocket 原生只传 byte[],WinForm 客户端若直接 Encoding.UTF8.GetString(buffer),容易因分帧导致乱码:一个 JSON 对象可能被拆成两帧到达,第二帧开头不是完整 JSON 字符串。
参数差异:
• SendAsync 的第三个参数 endOfMessage 必须为 true(文本消息通常整包发送)
• ReceiveAsync 返回的 WebSocketReceiveResult.EndOfMessage 才表示一帧完整,但不等于一个逻辑消息
- 简单方案:约定每条消息以
\n结尾,接收端缓存未结束的片段,拼接后再解析 JSON - 推荐做法:服务端发送时加长度头(4 字节 int32),客户端先读长度,再读指定字节数,避免 JSON 解析失败
- 反序列化别用
JsonConvert.DeserializeObject<Dictionary<string, string>>直接吞原始字符串——要先Trim()去首尾空白,否则 JSON.NET 报Unexpected character - 发送前用
JsonConvert.SerializeObject(new { a = "val1", b = "val2" }),比手拼字符串更安全
连接关闭和异常清理最容易被跳过
很多示例代码写了 ConnectAsync 和 ReceiveAsync,但没写怎么关。不显式调用 CloseAsync,连接会滞留在 CloseReceived 状态,服务端无法及时释放资源,客户端重启后可能遇到端口占用或握手拒绝。
性能影响:
• 不释放 CancellationTokenSource → 内存泄漏
• 不 dispose ClientWebSocket → socket 句柄泄漏,Windows 下最多 65535 个,很快耗尽
- 关闭按钮逻辑必须包含:
await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "user close", token) - 在
FormClosing事件中调用关闭流程,并await完成,避免进程退出时 socket 强制中断 - 所有异步方法都应包裹
try/catch,尤其SendAsync:若连接已断还发数据,抛InvalidOperationException -
client实例不要复用:每次连接前client = new ClientWebSocket(),避免残留状态干扰
最常被忽略的是接收循环的退出条件和 UI 切换线程的时机——不是“收到就更新”,而是“收到→解码→判断是否完整→切 UI 线程→更新”,少一步就丢数据或崩界面。

















