恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
C# WebSocketSharp 详解:从协议原理到服务端客户端实战
首页
资讯中心
/
C# WebSocketSharp 详解:从协议原理到服务端客户端实战
C# WebSocketSharp 详解:从协议原理到服务端客户端实战
发布时间:2026/8/30 5:16:01
简介本资源是一份面向C#开发者的基础实战型WebSocket开发指南聚焦于WebSocketSharp框架在实时通信场景如在线聊天、股票行情推送、多人协作应用中的客户端与服务端完整实现。资源包含94个文件以17个核心C#源码文件含客户端/服务器主逻辑、窗体交互、协议行为封装、8个DLL依赖库、6个配置文件及多组编译产物exe/pdb/xml等为主体总大小1.91MB结构清晰分为WebSocketSharpClient与WebSocketSharpServer两大工程模块便于分步学习与对比调试。已有909人学习下载配套示例代码覆盖连接建立、消息收发、事件监听OnOpen/OnMessage/OnClose、自定义行为类继承、SSL配置等关键环节并提供可直接运行的WinForm可视化项目帮助开发者快速掌握全双工通信的C#落地实践路径。 WebSocket 这个东西做 C# 开发的应该都不陌生。早些年我做上位机相关的项目设备端和服务器之间要实时推送状态轮询太浪费带宽又想要真正的服务端主动推送WebSocket 几乎是唯一的选择。但 .NET 自带的 ClientWebSocket 用起来特别繁琐要自己处理握手、缓冲、分段接收这一大堆底层细节写出来的代码又长又难维护。后来朋友推荐了 WebSocketSharp用了一周就彻底爱上了。这个框架最打动我的地方就是干净利落把连接、事件、消息收发都封装成了很自然的 API写起来跟写普通事件处理程序一样顺手。这篇文章就围绕 WebSocketSharp 的实际用法把我这些年在项目里踩过的坑、琢磨出来的套路一次性说清楚适合刚入门的 C# 开发者也适合已经用官方 WebSocket 写得很痛苦、想换一个更顺手的库的老手。1. WebSocketSharp 框架核心认知与选型思路1.1 先搞清楚 WebSocket 协议解决了什么问题传统 HTTP 协议是请求-响应模式服务端没办法主动往客户端塞数据。早期做聊天室、实时监控这类功能只能靠客户端轮询比如每隔几秒发一个 HTTP 请求问服务端“有没有新消息”。这种方式在连接数少的时候还行连接一多网络开销和服务器压力都会成倍增长而且消息的实时性也受限于轮询间隔。WebSocket 协议则是在 TCP 之上建立一条长连接客户端和服务端都可以随时往这条连接里写数据。握手阶段用 HTTP Upgrade 头完成协议升级之后就完全脱离了 HTTP 的语义变成全双工通信。打个比方HTTP 像是你给邮局写信寄一封信等一封回信WebSocket 则是直接拉了一条专线电话双方随时能说话。WebSocketSharp 就是把这条“专线电话”从建立、维护到挂断的整个过程封装成了简单好用的 C# 类库。1.2 为什么我最终选了 WebSocketSharp当年选型的时候我实际对比过三套方案.NET 自带的 ClientWebSocket、SignalR、还有 WebSocketSharp。SignalR 功能确实强自动重连、多种传输协议、分组管理这些能力都很完善但它太重了我那时候只是做一个设备状态上报的模块引 SignalR 感觉像杀鸡用牛刀。官方 ClientWebSocket 倒是轻量可它设计得过于底层用起来得自己维护 ArraySegment、处理分片消息稍不留神就出内存问题。WebSocketSharp 恰好卡在中间它足够轻整个库就一个 dll没有任何外部依赖它又足够完整客户端、服务端、SSL、代理这些全都支持。最让我欣赏的是它的 API 设计事件处理程序用起来跟 WinForm 里的 Button.Click 一样自然。不需要手动管理接收缓冲区的循环OnMessage 事件直接把解析好的消息文本或二进制数据给你。1.3 框架结构概览客户端与服务端的统一抽象WebSocketSharp 整个框架可以分成两个核心类。WebSocket 类负责客户端逻辑WebSocketServer 类负责服务端逻辑。两个类在事件模型上保持了高度一致都有 OnOpen、OnMessage、OnError、OnClose 这四个关键生命周期事件。这意味着你只需掌握一套事件机制客户端和服务端就都能很快上手。命名空间是 WebSocketSharp客户端的 WebSocket 类和 .NET 自带的那个同名引用的时候容易混淆建议在代码文件顶部用别名区分这个细节我后面实操部分会专门提到。另外这个库同时支持 .NET Framework 和 .NET Core/.NET 5我最近在新项目里用 .NET 8 也跑得很稳NuGet 包一直在维护这点不需要担心。2. 环境准备与连接搭建2.1 安装方式及版本选择WebSocketSharp 在 NuGet 上的包名是 WebSocketSharp安装方式有两种一是用 Visual Studio 的 NuGet 包管理器界面搜索安装二是用包管理器控制台执行下面的命令。Install-Package WebSocketSharp如果用的是 .NET Core 或 .NET 5 的命令行工具也可以这样安装dotnet add package WebSocketSharp版本选择上我有两点建议。第一官方原版 WebSocketSharp 的最后一个版本停留在了 1.0.3-rc11这个版本稳定可用但出现了一些兼容性 issue。推荐使用社区维护的 WebSocketSharp-netstandard 这个 fork它由 sta 维护合并了很多修复支持 .NET Standard 2.0包名是 WebSocketSharp-netstandard。第二安装后留意一下编译是否出现警告。如果你用的是 .NET Framework 4.6.1 以下版本某些 API 可能不可用建议至少锁定在 4.7.2 以上。2.2 客户端连接建立与状态管理客户端的使用可以概括成四步创建连接、注册事件、调用 Connect、发送数据。下面是一段最基础的连接代码。using WebSocketSharp; // 注意如果项目里同时引用了 System.Net.WebSockets建议加别名 // using WsClient WebSocketSharp.WebSocket; var ws new WebSocket(ws://127.0.0.1:8080/device); ws.OnOpen (sender, e) { Console.WriteLine(连接已建立); }; ws.OnMessage (sender, e) { Console.WriteLine($收到消息: {e.Data}); }; ws.OnError (sender, e) { Console.WriteLine($发生异常: {e.Message}); }; ws.OnClose (sender, e) { Console.WriteLine($连接关闭: {e.Reason}); }; ws.Connect();这段代码做完之后连接就是建立状态了。注意 Connect 方法会阻塞当前线程直到连接成功或失败所以一般建议放在后台线程或者 async 包装里调用避免卡住 UI 线程。在 WinForms 或 WPF 上位机程序里我习惯这样写Task.Run(() ws.Connect());连接状态的判断可以用 ws.IsAlive 属性它在底层维护了连接状态和心跳逻辑不需要自己维护标志位。但实测中 IsAlive 在断网情况下不一定能立刻变成 false因为它依赖心跳超时机制心跳间隔默认时间比较长所以我通常在业务层再加一层定时检测逻辑。2.3 服务端监听与客户端接入客户端的 WebSocket 类用于主动连接远程服务端WebSocketServer 类则负责在本地开启一个监听端口。下面是一个最简单的服务端示例。using WebSocketSharp.Server; var server new WebSocketServer(ws://0.0.0.0:8080); server.AddWebSocketServiceDeviceBehavior(/device); server.Start(); Console.WriteLine(服务端已启动监听端口 8080); Console.ReadKey(); server.Stop();这里 AddWebSocketService 的泛型参数 DeviceBehavior 需要继承 WebSocketBehavior它是服务端处理单个连接的核心类。每个客户端连接都会对应一个 DeviceBehavior 实例生命周期从客户端接入开始到连接断开结束。public class DeviceBehavior : WebSocketBehavior { protected override void OnOpen() { Console.WriteLine($设备接入: {ID}); } protected override void OnMessage(MessageEventArgs e) { Console.WriteLine($收到设备数据: {e.Data}); // 处理完业务后可以给这个客户端回一条消息 Send($服务端已收到: {e.Data}); } protected override void OnClose(CloseEventArgs e) { Console.WriteLine($设备断开: {ID}); } }这个设计让服务端编程变得非常直观每个 Socket 连接就是一个 Behavior 实例你只需要在这个类里写业务逻辑。ID 属性是 WebSocketSharp 自动为每个连接生成的唯一标识可以用来定位具体是哪个客户端发来的消息。3. 核心功能详解与实战用法3.1 消息发送与接收的完整姿势WebSocketSharp 支持两种消息格式文本和二进制。服务端发送文本消息可以直接用 Send(string)发送二进制数据则用 Send(byte[])。客户端接收方通过 MessageEventArgs 的 IsText 和 IsBinary 属性判断消息类型e.Data 拿文本内容e.RawData 拿二进制内容。// 客户端发送二进制数据 var buffer new byte[] { 0x01, 0x02, 0x03, 0x04 }; ws.Send(buffer); // 服务端发送文本 Send(hello device); // 接收端判断消息类型 ws.OnMessage (sender, e) { if (e.IsText) { Console.WriteLine($文本消息: {e.Data}); } else if (e.IsBinary) { Console.WriteLine($二进制消息, 长度: {e.RawData.Length}); } };实际项目里文本消息适合传 JSON 协议二进制适合传图片、语音、文件片段。我做设备上位机时经常遇到一种特殊的消息形式就是分片消息一个大的文件被拆成多个 frame 发送。WebSocketSharp 的 OnMessage 事件默认会把分片消息合并完整后才触发这一点省了很多事。官方 ClientWebSocket 需要自己循环读取直到 EndOfMessage对比之下 WebSocketSharp 这种设计对使用者非常友好。3.2 事件驱动的编程模型委托与事件的实践在 C# 里事件基于委托实现WebSocketSharp 的定义方式充分利用了这种语言特性。如果你在代码里看到 OnMessage 这种写法它其实就是把自定义方法挂载到框架的事件上当消息到达时框架会在内部线程池上调用所有挂载的方法。这个模型有几个明显的体验优势。首先是代码组织清晰每个生命周期阶段对应一个处理方法业务逻辑不会纠缠在一起。其次是多消息并发时天然异步框架内部会有专门的接收循环。需要注意的是事件处理程序运行在接收线程上如果你的处理逻辑很耗时会阻塞后续消息的接收。解决思路有两个把耗时操作丢到 Task.Run 里异步处理或者用队列把消息先缓存起来由独立的工作线程消费处理。我在高吞吐的场景下一般选择后者因为 Task.Run 如果瞬间来大量消息线程池调度开销也不小队列模型更可控。// 用 ConcurrentQueue 做消息缓冲 private readonly ConcurrentQueuestring _messageQueue new ConcurrentQueuestring(); ws.OnMessage (sender, e) { _messageQueue.Enqueue(e.Data); }; // 独立消费线程 Task.Run(() { while (_isRunning) { if (_messageQueue.TryDequeue(out var msg)) { ProcessMessage(msg); } else { Thread.Sleep(10); } } });3.3 多线程环境下的安全调用C# 的 WebSocket 对象在连接建立之后多个线程同时调用 Send 方法在理论上是允许的因为框架内部对发送操作做了同步处理。但我实际项目中遇到过一种特殊情况两个线程同时往同一个 WebSocket 实例发送大量数据偶发出现 “WebSocket is not connected” 的异常。查到最后发现是因为连接已经断开而发送线程还在继续调用 Send 造成的。解决方式不能只在调用前判断 IsAlive因为 IsAlive 为 true 的瞬间连接可能已经断了。正确的姿势是捕获异常并做重连或状态重置。我一般会封装一个安全发送方法。public bool TrySend(WebSocket ws, string payload) { if (ws null) return false; lock (_sendLock) { try { if (ws.IsAlive) { ws.Send(payload); return true; } } catch (Exception ex) { Console.WriteLine($发送异常: {ex.Message}); // 触发重连逻辑 } return false; } }lock 加在 Send 外面是为了避免多个线程同时调用 IsAlive 和 Send 之间的竞态。虽然框架内部可能有锁但自己加一层能显著提升安全性代价只是一点点性能损耗。3.4 SSL/TLS 加密连接的配置生产环境里 WebSocket 一般走 wss:// 协议也就是在 TLS 加密隧道之上跑 WebSocket。WebSocketSharp 对 SSL 的支持做得比较完善。服务端需要配置证书客户端只需要把 ws:// 换成 wss:// 地址即可。下面是一个服务端启用 SSL 的示例。var server new WebSocketServer(wss://0.0.0.0:8443); server.SslConfiguration.ServerCertificate new X509Certificate2(server.pfx, password); server.Start();这里踩过的坑是证书格式。如果使用 .pem 和 .key 两个文件需要先转换成 .pfx 或者 .p12 格式。另外客户端如果在局域网内自测证书往往不受信任直接用 wss:// 连接会抛证书验证失败的异常。临时方案是在客户端注册证书验证回调返回 true 跳过校验但生产环境千万不要这样写。ws.SslConfiguration.ServerCertificateValidationCallback (sender, certificate, chain, sslPolicyErrors) { // 仅限开发调试时使用生产务必做真实校验 return true; };4. 实战演练一个完整通信案例4.1 场景设定设备状态上报系统为了把前面讲的内容串起来我用一个实际做过的案例来演示完整实现。业务场景是一批工业设备通过 WebSocket 连接上位机服务端每台设备每隔 2 秒上报一次运行状态服务端收到后做出应答同时服务端可以向指定设备发送控制指令。这个场景同时用到了服务端和客户端服务端用 WebSocketServer 监听设备端模拟则用 WebSocket 客户端类写一个控制台程序。4.2 服务端完整代码实现先写服务端。我定义了一个 DeviceBehavior 类来处理所有设备连接并在内部维护一个静态的客户端连接池。using System; using System.Collections.Concurrent; using WebSocketSharp.Server; public class DeviceBehavior : WebSocketBehavior { // 用 ConcurrentDictionary 保存所有在线设备ID 直接作为键 private static readonly ConcurrentDictionarystring, DeviceBehavior OnlineDevices new ConcurrentDictionarystring, DeviceBehavior(); protected override void OnOpen() { OnlineDevices.TryAdd(ID, this); Console.WriteLine($设备接入当前在线数: {OnlineDevices.Count}); } protected override void OnMessage(MessageEventArgs e) { Console.WriteLine($设备 [{ID}] 上报: {e.Data}); // 模拟业务处理解析 JSON然后给设备回一个 ACK var response ${{\type\:\ack\,\deviceId\:\{ID}\,\time\:\{DateTime.Now:yyyy-MM-dd HH:mm:ss}\}}; Send(response); } protected override void OnClose(CloseEventArgs e) { OnlineDevices.TryRemove(ID, out _); Console.WriteLine($设备断开剩余在线数: {OnlineDevices.Count}); } // 静态方法向指定设备发送指令 public static bool SendCommand(string deviceId, string command) { if (OnlineDevices.TryGetValue(deviceId, out var behavior)) { behavior.Send(command); return true; } return false; } // 静态方法向所有设备广播 public static void BroadcastToAll(string message) { foreach (var behavior in OnlineDevices.Values) { behavior.Send(message); } } }服务端启动代码using System; using WebSocketSharp.Server; class Program { static void Main(string[] args) { var server new WebSocketServer(ws://0.0.0.0:9000); server.AddWebSocketServiceDeviceBehavior(/device); server.Start(); Console.WriteLine(设备接入服务已启动监听 ws://0.0.0.0:9000/device); // 主线程循环接收管理员命令 while (true) { var input Console.ReadLine(); if (input list) { Console.WriteLine(当前在线设备列表:); // 这里可以扩展从 DeviceBehavior 获取设备列表 } else if (input.StartsWith(send )) { // 输入格式send {deviceId} {message} var parts input.Split( , 3); if (parts.Length 3) { var ok DeviceBehavior.SendCommand(parts[1], parts[2]); Console.WriteLine(ok ? 指令已发送 : 设备不存在或已离线); } } else if (input exit) { break; } } server.Stop(); } }这个实现已经能解决真实业务里绝大部分需求。OnlineDevices 静态字典是关键设计它让外部代码无需持有具体连接实例也能向任意设备发消息。4.3 设备端模拟与客户端实现设备的 C# 模拟程序代码也很简单。这里模拟 5 台设备同时连接服务端每台设备用自己的线程每 2 秒上报一次运行状态。using System; using System.Threading; using WebSocketSharp; class DeviceSimulator { private readonly string _deviceId; private WebSocket _ws; public DeviceSimulator(string deviceId) { _deviceId deviceId; _ws new WebSocket(ws://127.0.0.1:9000/device); _ws.OnMessage OnMessage; _ws.OnClose OnClose; _ws.OnError OnError; } public void Start() { _ws.Connect(); // 启动独立线程模拟周期上报 var thread new Thread(ReportLoop); thread.IsBackground true; thread.Start(); } private void ReportLoop() { var random new Random(); while (_ws.IsAlive) { var status random.Next(0, 100); var payload ${{\deviceId\:\{_deviceId}\,\status\:{status},\temp\:{random.Next(20, 60)}}}; try { _ws.Send(payload); } catch (Exception ex) { Console.WriteLine($[{_deviceId}] 发送失败: {ex.Message}); break; } Thread.Sleep(2000); } } private void OnMessage(object sender, MessageEventArgs e) { Console.WriteLine($[{_deviceId}] 收到服务端消息: {e.Data}); } private void OnClose(object sender, CloseEventArgs e) { Console.WriteLine($[{_deviceId}] 连接关闭: {e.Reason}); } private void OnError(object sender, ErrorEventArgs e) { Console.WriteLine($[{_deviceId}] 错误: {e.Message}); } }主程序启动 5 个模拟实例class Program { static void Main(string[] args) { for (int i 1; i 5; i) { var simulator new DeviceSimulator($DEV-{i:00}); simulator.Start(); Thread.Sleep(200); // 错开启动时间避免同时连接造成瞬时压力 } Console.WriteLine(5 台模拟设备已启动); Console.WriteLine(按任意键退出...); Console.ReadKey(); } }4.4 联调过程笔记与踩坑记录我第一次跑这套联调代码时遇到了几个问题记录一下。第一个问题是最开始服务端地址监听用了 127.0.0.1导致局域网内的真实设备根本连不上。WebSocketServer 监听地址应该用 0.0.0.0 表示监听所有网卡。这一点很容易犯因为本机测试时 127.0.0.1 完全正常一上真实环境就出问题。第二个问题是设备端连续上报时服务端回复的 ACK 消息会被设备端的 OnMessage 事件处理但如果 ProcessMessage 的逻辑太慢会出现事件回调堆积。我加了一个 ConcurrentQueue 缓冲就解决了这个细节在第 3 节讲过多线程安全这里正好验证了那个方案的价值。第三个问题比较隐蔽就是 WebSocketSharp 服务端默认没有心跳机制设备端如果异常断电服务端的连接不会立刻释放OnClose 可能要过很久才触发。解决方案是给 WebSocketServer 设置 KeepAlive 属性。server.KeepAlive TimeSpan.FromSeconds(30);这样服务端每 30 秒发一次 Ping 帧对端无响应则判定连接失效能及时清理僵尸连接。5. 常见问题与排查技巧实录5.1 连接失败的第一现场排查思路WebSocketSharp 连接失败通常表现为三种Connect 方法抛异常、连接建立后马上触发 OnClose、连接没有任何消息但 OnError 被触发。面对这些情况我有一套固定的排查流程。第一步看地址。检查 ws:// 还是 wss:// 是否与服务端配置一致端口是否被防火墙拦截。局域网环境下最常见的就是 Windows 防火墙弹窗没点允许服务端代码运行正常但外部设备连不上。第二步看握手。用浏览器开发者工具直接访问 ws:// 地址如果浏览器能连上而代码连不上问题基本出在客户端代码。第三步看异常信息。WebSocketSharp 抛出的异常往往带有阶段信息比如 ProtocolError 代表握手协议有问题通常是因为路径不对或者服务端没有启动成功。下面列一个常见异常速查表。现象可能原因解决方向Connect 抛 ArgumentException地址格式不合法缺少路径检查 ws://host:port/path 格式OnError 触发 Connection refused服务端未启动或端口被占用确认 netstat 端口监听状态OnError 触发 Authentication failed服务端要求认证检查用户名密码配置OnClose 立刻触发 reason 为握手失败服务端路径写错确认 AddWebSocketService 的路径连接正常但发消息失败服务端已断开但客户端未感知启用心跳机制捕获异常重连5.2 消息收发异常与数据格式问题消息接收不到我遇到最多的情况是编码问题。服务端发送的中文消息客户端收到后变成乱码这种情况通常是两端使用的编码不一致。WebSocketFrame 默认按 UTF-8 编码处理文本消息如果服务端用了其他编码客户端解析就会出现乱码。排查方法是先用最简单的 ASCII 文本测试排除编码干扰后再引入中文。还有一种情况是消息太大。WebSocket 协议本身支持 64 位长度的消息但 WebSocketSharp 默认有一个最大消息大小限制超过限制会直接触发 OnError 然后断开连接。默认值是 64KB 吗不是我印象中默认没有明确限制但实际受内存约束更稳妥的做法是手动设置 MaxPayloadSize。server.MaxPayloadSize 1024 * 1024; // 限制为 1MB如果业务里有传文件、传大图的需求建议主动设置这个值并做好接收端的内存保护。毕竟如果把服务端最大消息限制设置为无限大遭遇恶意连接可能直接把服务器内存拖垮。5.3 资源释放与连接生命周期管理这是最容易忽略的问题。很多初学者写完 WebSocket 程序关掉窗口直接退出进程这会导致服务端的连接迟迟收不到关闭帧。正确的关闭流程应该是先调用 Close 方法再释放相关资源。// 客户端优雅关闭 ws.Close(); // 服务端停止监听 server.Stop();Close 方法还有一个带 CloseStatusCode 和 reason 的重载可以用来告知对端关闭原因。ws.Close(CloseStatusCode.Normal, 业务处理完毕);在多客户端场景下服务端 Stop 时会逐个断开所有连接。如果你在 Behavior 的 OnClose 里做了资源释放不用担心重复释放的问题框架内部会保证每个 Behavior 的 OnClose 只触发一次。在实际项目里我还习惯在应用程序退出前做一次统一的连接巡检确保所有 WebSocket 实例都被正确关闭。虽然现代操作系统在进程退出后会回收 Socket 资源但优雅关闭能减少服务端日志中大量无意义的 abruptly closed 记录。5.4 性能调优与并发连接优化如果服务端同时需要支撑上千个连接需要注意几个关键点。第一是 WebSocketServer 默认使用的线程池是 .NET 线程池连接数上来后线程切换开销会变得明显。可以考虑把业务处理部分改成异步 IO 队列模式避免在接收线程里做重活。第二是心跳间隔KeepAlive 时间设置太短会增加网络包量设置太长则僵尸连接释放不及时。我个人经验是 30 秒到 60 秒比较合适。第三是消息批量发送如果同一时间要给多个设备发送同一条指令遍历 Sessions 逐个 Send 会遇到锁竞争问题可以批量发送前先集合需要的目标 ID再分组发送。WebSocketSharp 的服务端支持通过 Sessions 属性管理所有连接。// 向所有会话广播 server.WebSocketServices[/device].Sessions.Broadcast(公告消息);如果需要定向发送可以通过 Sessions.GetEnumerator 遍历找到指定 ID 的 session或者像我在第 4 节那样自建连接池。自建连接池的好处是可以顺便维护业务状态比如设备编号和连接 ID 的映射关系。结尾最后分享两个我自己项目中沉淀下来的小技巧。第一个是连接断线重连WebSocketSharp 本身没有内置自动重连但实现起来不难在 OnClose 和 OnError 事件里启动一个延迟任务几秒后重新 Connect注意加指数退避避免服务端恢复瞬间出现惊群效应。第二个是协议设计建议所有消息都用 JSON 包一层里面带消息类型字段和消息 ID这样以后扩展功能不用改连接逻辑只需要新增消息处理器。这套做法我在多个上位机项目里验证过稳定性和可维护性都很好。如果你之前用官方 ClientWebSocket 觉得很别扭试试 WebSocketSharp大概率会让你重新喜欢上写通信代码。本文还有配套的精品资源点击获取