拓冰建站拓冰建站
首页 / 资讯中心 / 正文

C# WebSocketSharp 实战:从连接、心跳到广播与踩坑排查

简介本资源是一份面向C#开发者、聚焦WebSocket实时通信实战的完整学习包适用于构建在线聊天、股票行情推送、实时游戏等双向交互应用。资源包含客户端与服务器双端可运行示例工程WebSocketSharpClient与WebSocketSharpServer涵盖连接管理、消息收发、事件回调OnOpen/OnMessage/OnClose、SSL配置及自定义行为扩展等核心用法适合中初级.NET开发者快速掌握WebSocketSharp框架落地能力。压缩包共94个文件以17个C#源码文件.cs为核心辅以8个动态库.dll、6个配置文件.config、4个资源文件.resx及VS解决方案相关文件.sln/.csproj结构清晰、即开即用总大小1.91MB。已有909人下载学习配套代码经过实际编译验证含完整项目目录、依赖管理nupkg与调试符号pdb便于逐层理解通信流程与异常处理机制。 做 C# 实时通信大多数人第一反应是 SignalR但如果你只是想要一个轻量、干净、不依赖 ASP.NET Core 的 WebSocket 方案WebSocketSharp 是绕不开的名字。这个库我在好几个项目里用过从设备状态上报到客户端主动推送它基本上就是 C# 世界里最省心的 WebSocket 实现之一。这篇文章不打算把官方文档复述一遍我直接用实际项目里的用法来讲从选型、基础连接、核心 API 实操到踩坑排查尽量把你在用这个框架时可能遇到的情况都覆盖到。1. 为什么选 WebSocketSharp选型背后的思考1.1 技术背景与选型对比WebSocketSharp 是一个纯 C# 实现的 WebSocket 协议库最突出的特点就是轻和全。轻是指它没有任何重量级依赖不要求你引入 ASP.NET Core 或者 OWIN 那套东西全是指它同时实现了客户端和服务端两头不像有些库只能做一端或者只支持客户端。在选择实时通信方案时我通常把候选方案分成三类方案适用场景不适用场景SignalR已经有 ASP.NET Core 项目需要自动重连、分组、Hub 抽象项目没有 .NET Web 服务端或想对接非 .NET 客户端原生 System.Net.WebSockets想要最底层的控制追求零依赖要自己管理连接池、帧处理、握手细节开发成本高WebSocketSharp轻量 C/S 项目、上位机通信、Unity 客户端、需要同时做客户端和服务端已深度嵌入 ASP.NET Core 生态需要高层业务抽象拿我实际做过的一个上位机数据采集项目来说服务端是一个独立的 Windows 服务进程不跑 IIS 也没有 Kestrel只负责对接多台设备、接收采集数据并把状态推给多个监控端。这种情况下引入整个 ASP.NET Core 太重了原生 WebSocket 又需要自己处理很多协议细节。WebSocketSharp 正好卡在中间一个类就能起服务端一个类就能当客户端而且和各种语言写的标准 WebSocket 服务端都能互通因为它是按照 RFC 6455 规范实现的。这里特别解释一下为什么我强调标准 WebSocket 互通。SignalR 的客户端和服务端必须配套使用它有自己的协议格式跨语言对接很麻烦。WebSocketSharp 是纯粹的 WebSocket 协议对接方的服务端可以是 Go、Java、Node.js 或者 Python 写的只要是标准协议实现就能通信。这在物联网和异构系统集成的场景下非常关键。1.2 WebSocketSharp 的特性边界用过一段时间后我把它能干的事和不能干的事分得很清楚。能做的客户端连接、发送文本和二进制消息、主动关闭连接服务端监听、路径路由、多客户端连接管理WSS 安全连接也就是 WebSocket over TLS协议层的 Ping/Pong 心跳检测自定义握手请求头、Cookie消息的异步发送服务端会话管理包括广播、定向发送、关闭连接不能做的没有内置重连机制断线重连要自己写没有消息路由、消息持久化这些业务层能力没有像 SignalR 那样的 Hub 模型每个 WebSocketBehavior 实例对应一个 WebSocket 连接官方仓库更新频率不高虽有社区维护的 netstandard 分支但遇到新平台问题时可能需要自己改源码打补丁顺便提醒一句NuGet 上有两个包名很接近一个是WebSocketSharp一个是WebSocketSharp-netstandard。前者偏老支持 .NET Framework 4.5 和 .NET Standard 2.0后者是社区 fork专门用于 .NET Core/.NET 5 场景。我个人在新项目里优先用WebSocketSharp-netstandard旧项目如果停在 .NET Framework 上就用原版。选错包很容易在运行时遇到一些莫名其妙的兼容问题这点后面踩坑部分还会细说。2. 从零搭建环境安装与基础连接2.1 安装与引用新建一个控制台项目或者类库项目在程序包管理器里执行Install-Package WebSocketSharp-netstandard或者用 .NET CLIdotnet add package WebSocketSharp-netstandard如果是普通控制台项目装完包之后直接using WebSocketSharp;就能用了。服务端相关的类型在WebSocketSharp.Server命名空间下需要另外引一行using WebSocketSharp; using WebSocketSharp.Server;安装这步通常不会出什么幺蛾子唯一要注意的是项目目标框架。老的 .NET Framework 项目如果用了原版WebSocketSharp包依赖项会自动带上但 .NET 6/8 项目引用老包时有可能会出现缺少System.Security.Cryptography.X509Certificates这类基础类型的情况。所以新平台直接选 -netstandard 包省去后面排查的时间。2.2 客户端连接代码实战先写一个最基础的客户端 Demo这个结构我几乎每个项目都会复用using System; using WebSocketSharp; class ClientDemo { static void Main(string[] args) { using (var ws new WebSocket(ws://localhost:8080/echo)) { // 连接建立成功 ws.OnOpen (sender, e) { Console.WriteLine(连接已建立); ws.Send(Hello, WebSocketSharp!); }; // 收到服务端消息 ws.OnMessage (sender, e) { if (e.IsText) { Console.WriteLine(收到文本消息: e.Data); } else if (e.IsBinary) { Console.WriteLine($收到二进制消息长度: {e.RawData.Length} 字节); } }; // 连接关闭 ws.OnClose (sender, e) { Console.WriteLine($连接关闭状态码: {e.Code}, 原因: {e.Reason}); }; // 出错 ws.OnError (sender, e) { Console.WriteLine(发生错误: e.Message); }; ws.Connect(); Console.WriteLine(按任意键退出...); Console.ReadKey(); } } }这段代码有几点值得注意。ws.Send在未连接成功时调用会抛出异常所以我的习惯是在OnOpen里发第一条消息这个时序能保证连接已经就绪。OnMessage事件里用e.IsText和e.IsBinary来区分文本和二进制帧这在对接不同消息类型时很实用比如文本传 JSON 指令二进制传文件或图片。还有一个隐藏属性ws.WaitTime它控制连接握手阶段的超时时间默认是 5 秒。在弱网环境或者服务端处理握手比较慢的时候这个默认值可能导致连接失败我一般会调大到 10~15 秒ws.WaitTime TimeSpan.FromSeconds(10);连接超时异常是 The operation has timed out遇到这个先别怀疑代码逻辑优先检查 WaitTime 和服务端是否真的在监听。2.3 服务端监听与第一个 Demo 测试服务端的使用方式和客户端不太一样。WebSocketSharp 把每个 WebSocket 连接抽象成一个WebSocketBehavior子类你在这个子类里面写消息处理逻辑然后通过服务端注册到某个路径上。using System; using WebSocketSharp; using WebSocketSharp.Server; public class EchoBehavior : WebSocketBehavior { protected override void OnMessage(MessageEventArgs e) { // 把收到的消息原样返回给客户端 Send(e.Data); } protected override void OnOpen() { Console.WriteLine($新连接: {ID}); } protected override void OnClose(CloseEventArgs e) { Console.WriteLine($连接关闭: {ID}, 状态码: {e.Code}); } } class ServerDemo { static void Main(string[] args) { var server new WebSocketServer(ws://localhost:8080); // 注册路径 /echo server.AddWebSocketServiceEchoBehavior(/echo); server.Start(); Console.WriteLine(服务端已启动监听 ws://localhost:8080/echo); Console.ReadKey(); server.Stop(); // 或者 server.Stop(CloseStatusCode.Normal, 服务端关闭); } }跑起来之后用 2.2 里的客户端连ws://localhost:8080/echo就能看到服务端把消息原样返回。或者直接用浏览器 console 也能测var ws new WebSocket(ws://localhost:8080/echo); ws.onmessage function (event) { console.log(event.data); }; ws.onopen function () { ws.send(test from browser); };服务端和客户端集成在一个库里这种一条龙的设计让本地联调非常方便不需要同时准备两套依赖。关于WebSocketServer构造参数我再补充一个细节。你可以直接传 URI 字符串也可以分开传端口号var server new WebSocketServer(8080);传端口号时默认监听所有网卡也就是0.0.0.0:8080。如果只想监听本机回环地址用 URI 方式并指定 hostname 就行ws://127.0.0.1:8080。3. 核心实操7 个必须掌握的开发细节3.1 消息收发模型文本、二进制与异步发送WebSocketSharp 的Send方法有字符串和字节数组两种重载对应 WebSocket 协议里的文本帧和二进制帧// 发送文本 ws.Send({\type\:\ping\,\ts\:123456}); // 发送二进制 byte[] buffer System.Text.Encoding.UTF8.GetBytes(hello); ws.Send(buffer); // 发送文件片段 byte[] fileChunk File.ReadAllBytes(C:\temp\photo.jpg); ws.Send(fileChunk);文本和二进制在协议层面是两种不同的 Opcode接收端通过MessageEventArgs的IsText/IsBinary/RawData属性来区分和处理。这里有个坑e.Data默认是按 UTF-8 解码的字符串如果你收到的是二进制消息访问e.Data拿到的会是乱码。正确做法是先判断IsBinary再用e.RawData拿原始字节数组。对于耗时或者大消息的发送我一般用异步版本ws.SendAsync(Encoding.UTF8.GetBytes(large message), (completed) { Console.WriteLine($异步发送完成: {completed}); });SendAsync的回调参数是一个布尔值表示发送是否成功。用异步方式可以避免阻塞主线程在 UI 界面或者需要保持高频收发的场景下非常有用。另外WebSocketSharp的Send不是严格意义上的线程安全方法。虽然框架内部做了一定程度的同步但我在实际项目中还是会在高频多线程推送时加一把锁或者用一个发送队列来串行化发送请求防止偶发性的状态异常。3.2 心跳保持与连接保活WebSocket 的连接长时间没有数据流动很多网络设备比如路由器、云平台的负载均衡器会默默掐掉空闲连接。为了让连接保持存活需要定期发送心跳消息。WebSocket 协议本身有两种Ping/Pong帧WebSocketSharp 在客户端和服务端都有对应的处理// 客户端主动发 Ping bool pingResult ws.Ping(); Console.WriteLine(Ping 结果: pingResult); // 带数据的 Ping byte[] pingData Encoding.UTF8.GetBytes(heartbeat); bool result ws.Ping(pingData);Ping()方法发出一个 Ping 帧如果对端在超时时间内返回了 Pong 帧方法返回true否则返回false。这就是最基础的心跳检测机制。但实际项目里光靠协议层的 Ping 还不够因为有些代理服务器只转发业务数据帧对 Ping/Pong 不敏感。所以我习惯做一套应用层心跳客户端每隔一段时间发一个业务心跳消息比如 JSON 里的{type:heartbeat}服务端收到后回一个{type:heartbeat_ack}。客户端如果在指定时间内没有收到任何服务端消息就认为连接已经死了主动重连。using System.Timers; private Timer _heartbeatTimer; void StartHeartbeat(WebSocket ws) { _heartbeatTimer new Timer(30000); // 每 30 秒 _heartbeatTimer.Elapsed (sender, e) { if (ws.ReadyState WebSocketState.Open) { ws.Send({\type\:\heartbeat\}); } }; _heartbeatTimer.Start(); }判断连接是否正常的属性是ReadyState它取值为WebSocketState枚举Connecting、Open、Closing、Closed。只有在Open状态才允许发送数据。心跳间隔的选择有个经验值不要小于 10 秒太频繁会给服务端造成无意义的压力也不要大于 60 秒太长会导致 NAT 超时或负载均衡器来不及响应。我个人常用 20~30 秒。3.3 自定义请求头与协议控制有的服务端要求客户端在握手阶段带上 Token 验证身份WebSocketSharp 的客户端允许在连接之前设置自定义 Header 和 Cookieusing System.Net; var ws new WebSocket(ws://localhost:8080/chat); // 设置自定义请求头 ws.CustomHeaders new Dictionarystring, string { { Authorization, Bearer your_token_here }, { X-Client-Version, 1.0.3 } }; // 设置 Cookie ws.SetCookie(new Cookie(session_id, abc123)); ws.Connect();服务端在WebSocketBehavior里怎么拿到这些信息通过Context属性public class ChatBehavior : WebSocketBehavior { protected override void OnOpen() { var headers Context.Headers; string auth headers[Authorization]; var cookies Context.CookieCollection; string sessionId cookies[session_id]?.Value; // 验证失败可以拒绝连接 if (string.IsNullOrEmpty(auth)) { Context.WebSocket.Close(CloseStatusCode.PolicyViolation, 未授权); return; } Console.WriteLine($客户端会话: {sessionId}); } }这里用到了Context对象的两个兄弟属性Headers是 NameValueCollection 类型的请求头集合CookieCollection是客户端的 Cookie 集合。另外Context.QueryString也很有用它是 URL 里?后面的查询参数集合。我经常让客户端用查询参数传递临时 token 或标识比在 Header 里设置要省事// 客户端 var ws new WebSocket(ws://localhost:8080/chat?uid10086tokenabc); // 服务端 protected override void OnOpen() { string uid Context.QueryString[uid]; string token Context.QueryString[token]; }3.4 SSL/WSS 安全连接配置在公网环境跑 WebSocket 服务十有八九要上加密也就是 WSS 协议。WebSocketSharp 对 SSL 的支持还算完善服务端这样配置using System.Security.Cryptography.X509Certificates; using WebSocketSharp.Server; var server new WebSocketServer(443, true); // 第二参 true 表示启用 SSL server.SslConfiguration.ServerCertificate new X509Certificate2(C:\certs\server.pfx, password); server.SslConfiguration.EnabledSslProtocols System.Security.Authentication.SslProtocols.Tls12; server.AddWebSocketServiceEchoBehavior(/echo); server.Start();WebSocketServer构造函数第一个参数是端口第二个参数true表示使用 SSL。此时客户端的连接地址也要改成wss://前缀var ws new WebSocket(wss://yourdomain.com:443/echo); ws.SslConfiguration.ServerCertificateValidationCallback (sender, certificate, chain, sslPolicyErrors) true; ws.Connect();最后那个ServerCertificateValidationCallback是证书校验回调。生产环境中我强烈不建议永远返回true这会跳过证书链校验很容易被中间人攻击。这个写法只推荐在调试环境或者证书还没申请下来的时候临时用。在 .NET Core/.NET 5 下还要注意 TLS 版本的问题。老版本的WebSocketSharp-netstandard可能默认使用 TLS 1.0/1.1这在现代服务器上会被拒绝握手。解决方法是在客户端也显式指定协议版本ws.SslConfiguration.EnabledSslProtocols System.Security.Authentication.SslProtocols.Tls12 | System.Security.Authentication.SslProtocols.Tls13;如果两边都配置了依然握手失败优先检查服务器证书是否过期以及证书链是否完整有些中间证书没装全会导致客户端校验失败。3.5 会话管理Sessions 的正确打开方式服务端每一路 WebSocket 连接都对应一个IWebSocketSession通过WebSocketBehavior的Sessions属性可以拿到连接集合的管理器。这组 API 是做广播和定向推送的基础。在WebSocketBehavior内部可以直接使用public class ChatBehavior : WebSocketBehavior { protected override void OnMessage(MessageEventArgs e) { // 向所有已连接客户端广播消息 Sessions.Broadcast(广播消息: e.Data); // 向指定 ID 的客户端发送消息 Sessions.SendTo(私聊消息, session_id_string); // 关闭指定连接 Sessions.CloseSession(session_id_string); } }下面列举Sessions提供的主要方法方法作用使用场景Sessions.IDs获取所有会话 ID 的集合遍历连接Sessions.Count当前连接数量监控在线数Sessions.Broadcast(data)向所有连接广播消息系统通知、公告Sessions.SendTo(data, id)向指定连接发送消息定向推送Sessions.CloseSession(id)关闭指定连接踢下线、清理Sessions.BroadcastAsync(data, completed)异步广播大数据量广播有一点要特别提醒Sessions虽然是线程安全的迭代时使用Sessions.IDs是安全的但如果你在多个线程同时对 Sessions 做增删操作尽量避免在自己的业务代码里再叠加一层无序的遍历逻辑否则可能出现 Collection was modified 之类的异常。我的做法是服务端收到消息后统一放到一个ConcurrentQueue由一个专门的发送线程去处理避免多个线程同时操作Sessions造成竞争。另外一个经验尽量在OnOpen里把ID和业务标识比如设备编号、用户 ID的映射关系保存下来后续收到消息就能直接知道是谁发来的。这个映射表用ConcurrentDictionarystring, string就行注意在OnClose里记得把对应的键值删掉防止内存泄漏。4. 进阶场景从 Demo 到可用的实时通信服务4.1 广播与定向推送的完整实现前面讲到Sessions.Broadcast是全局广播但真实业务里往往需要按房间、按分组推送。WebSocketSharp 没有现成的分组 API不过这并不难我们自己维护一个分组字典public class ChatBehavior : WebSocketBehavior { // 房间号 - 会话ID列表 private static readonly System.Collections.Concurrent.ConcurrentDictionarystring, HashSetstring _rooms new System.Collections.Concurrent.ConcurrentDictionarystring, HashSetstring(); protected override void OnOpen() { string room Context.QueryString[room] ?? default; var ids _rooms.GetOrAdd(room, new HashSetstring()); lock (ids) { ids.Add(ID); } // 通知房间内其他成员 lock (ids) { foreach (var id in ids) { if (id ! ID) { Sessions.SendTo($[系统] 用户 {ID} 加入房间 {room}, id); } } } } protected override void OnClose(CloseEventArgs e) { foreach (var kv in _rooms) { lock (kv.Value) { kv.Value.Remove(ID); } } } protected override void OnMessage(MessageEventArgs e) { string room Context.QueryString[room] ?? default; var ids _rooms.GetOrAdd(room, new HashSetstring()); lock (ids) { foreach (var id in ids) { Sessions.SendTo(e.Data, id); } } } }这段代码的思路很直白用ConcurrentDictionarystring, HashSetstring维护一个房间与连接 ID 的映射OnOpen时把当前 ID 加进对应房间OnClose时从所有房间移除OnMessage时遍历房间内所有 ID 做定向发送。用lock保护HashSet的读写避免多线程并发修改。实际项目里我会在这个基础上再做一层封装把房间操作提取成独立的RoomManager类WebSocketBehavior只负责调用这样代码更清晰。另外房间内消息量大时比如行情推送建议用异步发送防止一个慢客户端拖慢整个广播循环。4.2 大消息与二进制数据的传输控制WebSocket 本身没有硬性限制单条消息大小但由于 HTTP 握手和帧缓冲都在内存里消息过大容易导致内存飙升或者 OutOfMemory。WebSocketSharp 服务端的消息大小限制用WebSocketServer的MaxMessageLength属性控制单位是字节var server new WebSocketServer(ws://localhost:8080); server.MaxMessageLength 1024 * 1024 * 16; // 16MB默认情况下这个值是多少我记得没有显式设置时框架内部对消息帧的长度做了基础的检查但为了保险强烈建议生产环境显式设置一个合理上限避免被恶意的大消息打爆内存。传输真正的超大文件比如几百 MB应该考虑分片客户端先把文件切分成 1~2 MB 的小块每一块用二进制帧发送并附带一个格式约定的头比如 JSON 元信息或自定义的帧头服务端收到所有分片后在业务层重组// 客户端发送文件分片 byte[] fileBytes File.ReadAllBytes(C:\large_file.bin); int chunkSize 1024 * 1024; // 1MB int offset 0; while (offset fileBytes.Length) { int len Math.Min(chunkSize, fileBytes.Length - offset); byte[] chunk new byte[len]; Array.Copy(fileBytes, offset, chunk, 0, len); // 在头部附加序号信息例如用固定 4 字节序号 4 字节总长度 byte[] header new byte[8]; BitConverter.GetBytes(offset).CopyTo(header, 0); BitConverter.GetBytes(fileBytes.Length).CopyTo(header, 4); byte[] frame new byte[header.Length chunk.Length]; Array.Copy(header, 0, frame, 0, header.Length); Array.Copy(chunk, 0, frame, header.Length, chunk.Length); ws.Send(frame); offset len; }这种方案的优点是实现简单不依赖 WebSocket 协议的分片控制业务逻辑自己说了算。缺点是要自己管理序号和重组逻辑出现丢包或乱序时要有重传机制。如果你的应用场景只是传 1~2 MB 的小文件其实也不用搞这么复杂直接一次性Send(byte[])就行配合上面设置好的MaxMessageLength足够。4.3 与上位机/硬件场景的联动思路看热搜词里有 C# 上位机 这个热词WebSocketSharp 在上位机领域用得确实不少。传统上位机多用串口或 TCP但越来越多的设备监控系统需要把数据推送到 Web 前端展示。这时候 WebSocket 就成了一个很合适的通道。我的一个典型方案是上位机程序C# 写的桌面应用或 Windows 服务作为 WebSocket 客户端主动连接后台服务端后台服务端用 WebSocketSharp 起一个WebSocketServer负责接收各上位机上报的状态数据后台同时发布一个 WebSocket 接口给浏览器端Web 页面实时展示设备状态这样做的好处是上位机不需要有公网 IP只要能访问到服务端就行整个通信链路都是标准的 WebSocket防火墙策略也好配置。数据上报的时候我习惯统一封装成一个 JSON 消息格式{ type: device_status, device_id: device_001, status: running, temperature: 36.5, timestamp: 1718582400000 }上位机端用Send方法把这个 JSON 序列化后发出去后台解析后决定是入库还是转发给前端。如果设备数量多、消息频率高比如每台设备每秒钟上报 10 条建议在上位机端加一个批量缓冲攒够一定条数或者一定时间间隔再统一打包发送能显著降低服务端的消息处理压力。这个思路和数据库批量插入是异曲同工的。5. 踩坑实录常见问题与排查技巧速查表5.1 典型异常与解决方案以下这些问题是 WebSocketSharp 使用中最常被搜索的我直接把现象、原因和解决办法整理成一张表异常/现象可能原因解决办法The operation has timed out握手阶段超时默认 WaitTime5秒服务端没启动 / 网络不通 / 等待时间太短调整ws.WaitTime到 10 秒以上The connection has already been established对同一个 WebSocket 对象重复调用Connect()重新 new 一个 WebSocket 实例或者先 Close 再 ConnectThe connection is not established在连接未建立时调用了Send在OnOpen事件中发消息或者发送前检查ReadyState WebSocketState.Open服务端收到错误帧或消息不完整发送了过大的单条消息检查MaxMessageLength设置必要时分片传输握手 404 或路由不匹配客户端连接地址与服务端注册路径不一致检查AddWebSocketServiceT(/echo)中的路径是否完全匹配SSL/TLS 握手失败客户端或服务端 TLS 版本太低显式指定SslConfiguration.EnabledSslProtocols Tls12或 Tls13ObjectDisposedException在释放 WebSocket 对象后访问其成员使用using块时注意作用域不要在释放后继续调用在 .NET 6/8 上无法加载程序集引用了老版本的原版包换用WebSocketSharp-netstandard包Unity 中使用报 IL2CPP 相关错误AOT 平台不支持反射调用考虑改用 UnityWebSocket 或原生库或测试 IL2CPP 兼容性排查思路我一般遵循三步走第一步排除网络层问题。先确认服务端端口有没有监听用一个最简单的 WebSocket 测试工具比如浏览器的 console连一下能通就说明协议层没问题问题出在代码逻辑。第二步看日志。WebSocketSharp 自带了日志功能可以通过ws.Log.Level LogLevel.Debug;打开详细日志或者把Log输出挂到自己的日志系统里。服务端也有server.Log.Level。Debug 级别的日志会显示握手请求、帧收发等详细信息排查问题非常有用。第三步复现问题时把客户端和服务端日志同时打开对照时间戳看是哪一端先出的问题。这个习惯帮我解决过好几个看起来像是网络问题实际上是对端异常关闭连接的案例。5.2 稳定性与性能优化建议关于稳定性我把自己的经验总结成几条心跳必做不管协议层有没有 Ping/Pong应用层心跳一定要做。做心跳不只是为了让中间设备不掉线更重要的是能快速发现死连接。服务端如果发现某个连接长时间没有收到客户端消息应该主动CloseSession清理掉否则连接池会被慢慢耗光。会话管理要清理干净OnOpen里保存的映射表一定要在OnClose里删除。内存泄漏往往就是从这种只增不删的细节开始的。建议定期检查Sessions.Count和自定义映射表的 Count 是否一致不一致就说明有清理遗漏。消息队列缓冲生产者和消费者速度不匹配时引入一个有界队列。队列满了可以丢弃旧消息或者采取背压策略而不是无脑往 WebSocket 里塞数据。日志脱敏不要在日志里直接打印完整的 Token、密码等敏感信息。WebSocket 消息可能会包含业务数据排错时最好只打印消息长度和类型不打印完整内容。关于性能除非你的连接数真的非常大几千路以上WebSocketSharp 本身的表现是够用的。几个常用优化手段优化手段说明合理设置MaxMessageLength防止超大消息占用过多内存使用SendAsync避免大消息阻塞主线程多线程广播时加锁保护防止并发访问Sessions产生异常业务处理与消息收发解耦用 Channel/Queue 做异步处理避免在 OnMessage 里做耗时操作开启 GC 优化高频消息场景下注意减少临时对象分配能复用临时 buffer还有一个容易忽略的点如果你的服务端同时承载大量连接记得检查操作系统层面的端口数和文件句柄限制。Windows 上默认动态端口范围有限Linux 上也有ulimit限制这些底层资源耗尽的表现往往是连接突然大量失败或进程无响应。5.3 一个小技巧在OnOpen里注册业务标识最后再分享一个我特别推荐的小习惯。我在每个 WebSocket 项目里都会做同一件事在服务端OnOpen触发时把当前连接的ID和一个业务标识设备编号、用户名、楼栋号等等绑定起来。private static readonly System.Collections.Concurrent.ConcurrentDictionarystring, string _connMap new System.Collections.Concurrent.ConcurrentDictionarystring, string(); protected override void OnOpen() { string deviceId Context.QueryString[device_id] ?? Guid.NewGuid().ToString(); _connMap[ID] deviceId; Console.WriteLine($设备 {deviceId} 已上线连接ID: {ID}); } protected override void OnClose(CloseEventArgs e) { string deviceId; if (_connMap.TryRemove(ID, out deviceId)) { Console.WriteLine($设备 {deviceId} 已离线连接ID: {ID}); } }这个映射表的最大价值在于排查问题时你可以直接根据业务标识找到对应的 WebSocket 连接而不需要去翻一个个自增的会话 ID。做过线上问题定位的人一定懂这种效率提升。项目上线之后我经常靠着这个映射表快速定位某台设备为什么断连之类的问题比翻海量日志要高效得多。WebSocketSharp 虽小但用熟练了之后你会发现 C# 底座上做实时通道其实没有想象中那么麻烦。先跑通一个最小 Demo再逐步加上业务逻辑和可靠性保障这套思路放哪个项目里都适用。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门