C# WebSocketSharp 实战:从连接、心跳到广播与踩坑排查
2026/8/30 5:07:09 网站建设 项目流程

简介:本资源是一份面向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 CLI:

dotnet 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.IsTexte.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.AddWebSocketService<EchoBehavior>("/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,接收端通过MessageEventArgsIsText/IsBinary/RawData属性来区分和处理。这里有个坑:e.Data默认是按 UTF-8 解码的字符串,如果你收到的是二进制消息,访问e.Data拿到的会是乱码。正确做法是先判断IsBinary,再用e.RawData拿原始字节数组。

对于耗时或者大消息的发送,我一般用异步版本:

ws.SendAsync(Encoding.UTF8.GetBytes("large message"), (completed) => { Console.WriteLine($"异步发送完成: {completed}"); });

SendAsync的回调参数是一个布尔值,表示发送是否成功。用异步方式可以避免阻塞主线程,在 UI 界面或者需要保持高频收发的场景下非常有用。

另外,WebSocketSharpSend不是严格意义上的线程安全方法。虽然框架内部做了一定程度的同步,但我在实际项目中还是会在高频多线程推送时加一把锁,或者用一个发送队列来串行化发送请求,防止偶发性的状态异常。

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枚举:ConnectingOpenClosingClosed。只有在Open状态才允许发送数据。

心跳间隔的选择有个经验值:不要小于 10 秒,太频繁会给服务端造成无意义的压力;也不要大于 60 秒,太长会导致 NAT 超时或负载均衡器来不及响应。我个人常用 20~30 秒。

3.3 自定义请求头与协议控制

有的服务端要求客户端在握手阶段带上 Token 验证身份,WebSocketSharp 的客户端允许在连接之前设置自定义 Header 和 Cookie:

using System.Net; var ws = new WebSocket("ws://localhost:8080/chat"); // 设置自定义请求头 ws.CustomHeaders = new Dictionary<string, 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?uid=10086&token=abc"); // 服务端 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.AddWebSocketService<EchoBehavior>("/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,通过WebSocketBehaviorSessions属性可以拿到连接集合的管理器。这组 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)的映射关系保存下来,后续收到消息就能直接知道是谁发来的。这个映射表用ConcurrentDictionary<string, string>就行,注意在OnClose里记得把对应的键值删掉,防止内存泄漏。

4. 进阶场景:从 Demo 到可用的实时通信服务

4.1 广播与定向推送的完整实现

前面讲到Sessions.Broadcast是全局广播,但真实业务里往往需要"按房间、按分组"推送。WebSocketSharp 没有现成的分组 API,不过这并不难,我们自己维护一个分组字典:

public class ChatBehavior : WebSocketBehavior { // 房间号 -> 会话ID列表 private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, HashSet<string>> _rooms = new System.Collections.Concurrent.ConcurrentDictionary<string, HashSet<string>>(); protected override void OnOpen() { string room = Context.QueryString["room"] ?? "default"; var ids = _rooms.GetOrAdd(room, new HashSet<string>()); 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 HashSet<string>()); lock (ids) { foreach (var id in ids) { Sessions.SendTo(e.Data, id); } } } }

这段代码的思路很直白:用ConcurrentDictionary<string, HashSet<string>>维护一个房间与连接 ID 的映射,OnOpen时把当前 ID 加进对应房间,OnClose时从所有房间移除,OnMessage时遍历房间内所有 ID 做定向发送。用lock保护HashSet的读写,避免多线程并发修改。

实际项目里我会在这个基础上再做一层封装,把房间操作提取成独立的RoomManager类,WebSocketBehavior只负责调用,这样代码更清晰。另外,房间内消息量大时(比如行情推送),建议用异步发送,防止一个慢客户端拖慢整个广播循环。

4.2 大消息与二进制数据的传输控制

WebSocket 本身没有硬性限制单条消息大小,但由于 HTTP 握手和帧缓冲都在内存里,消息过大容易导致内存飙升或者 "OutOfMemory"。

WebSocketSharp 服务端的消息大小限制用WebSocketServerMaxMessageLength属性控制,单位是字节:

var server = new WebSocketServer("ws://localhost:8080"); server.MaxMessageLength = 1024 * 1024 * 16; // 16MB

默认情况下这个值是多少?我记得没有显式设置时,框架内部对消息帧的长度做了基础的检查,但为了保险,强烈建议生产环境显式设置一个合理上限,避免被恶意的大消息打爆内存。

传输真正的超大文件(比如几百 MB),应该考虑分片:

  1. 客户端先把文件切分成 1~2 MB 的小块
  2. 每一块用二进制帧发送,并附带一个格式约定的头(比如 JSON 元信息或自定义的帧头)
  3. 服务端收到所有分片后,在业务层重组
// 客户端:发送文件分片 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 就成了一个很合适的通道。

我的一个典型方案是:

  1. 上位机程序(C# 写的桌面应用或 Windows 服务)作为 WebSocket 客户端,主动连接后台服务端
  2. 后台服务端用 WebSocketSharp 起一个WebSocketServer,负责接收各上位机上报的状态数据
  3. 后台同时发布一个 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握手阶段超时(默认 WaitTime=5秒)服务端没启动 / 网络不通 / 等待时间太短;调整ws.WaitTime到 10 秒以上
The connection has already been established对同一个 WebSocket 对象重复调用Connect()重新 new 一个 WebSocket 实例,或者先 Close 再 Connect
The connection is not established在连接未建立时调用了SendOnOpen事件中发消息,或者发送前检查ReadyState == WebSocketState.Open
服务端收到错误帧或消息不完整发送了过大的单条消息检查MaxMessageLength设置,必要时分片传输
握手 404 或路由不匹配客户端连接地址与服务端注册路径不一致检查AddWebSocketService<T>("/echo")中的路径是否完全匹配
SSL/TLS 握手失败客户端或服务端 TLS 版本太低显式指定SslConfiguration.EnabledSslProtocols = Tls12或 Tls13
ObjectDisposedException在释放 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.ConcurrentDictionary<string, string> _connMap = new System.Collections.Concurrent.ConcurrentDictionary<string, 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,再逐步加上业务逻辑和可靠性保障,这套思路放哪个项目里都适用。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询