简介这份资源是作者自行改写的 WebSocket SDK基于 websocket-sharp 构建面向需要在 C# .NET Framework 2.0 环境下实现 WebSocket 通信的开发者。它解决了低版本 .NET 平台缺乏完善 WebSocket 支持的问题支持多种安全校验方式可用于搭建 wss 服务器并额外提供了作为 API 服务器的客户端实现适合有一定 C# 基础、从事即时通讯或服务端接口开发的技术人员参考。压缩包共 103 个文件约 1.51MB以 81 个 cs 源码文件为核心辅以 5 个 dll 与 5 个 pdb 调试文件另含项目工程文件、密钥文件及少量缓存与说明文本整体结构完整。目前已有 731 人学习下载。读者可从中获取 WebSocket 服务端与客户端的完整实现思路包括会话管理、HTTP 服务、请求头处理及行为扩展等模块便于直接集成到自己的 .NET 2.0 项目中或作为二次开发与排错参考。1. 从一份 C# WebSocket 客户端包说起.NET 2.0 老项目怎么接实时通信手上维护着一套跑在 .NET 2.0 上的老工控上位机界面还是 WinForm业务逻辑全塞在几个巨型类里。某天需求来了设备状态要实时推给服务端服务端也要能主动下发指令。团队第一反应是上 SignalR结果 NuGet 一装就报框架不兼容——SignalR 最低要 .NET 4.5。换 gRPC同样跑不起来。最后翻出一个叫 NormanWebsocket-Sharp 的压缩包解压一看是纯 C# 实现的 WebSocket 客户端库目标框架直接标着 .NET 2.0没有第三方依赖源码全在里面。这类资源解决的就是「老框架 实时通信」这个夹缝需求不是让你学 WebSocket 协议原理而是给你一份能在 .NET 2.0/3.5 环境里直接编译、直接调用的客户端实现。适合谁维护遗留系统的 C# 工程师、做工业上位机或医疗设备软件的开发者以及需要在低版本框架里塞进长连接能力的人。下面我按「它是什么 → 怎么编译 → 怎么收发 → 坑在哪 → 怎么调稳」的顺序拆一遍。2. 拆开压缩包目录结构、目标框架与协议实现选型2.1 解压后先看什么文件清单与依赖判断拿到压缩包别急着双击 sln。先解压到纯英文路径用文本编辑器扫一遍目录。这类库通常长这样一个src放核心实现一个samples或demo放调用示例根目录一个.sln和若干.csproj。重点看.csproj里的TargetFrameworkVersion标签它决定了你能在哪个 VS 版本里打开。如果标的是v2.0那 Visual Studio 2022 默认是打不开的得用 VS 2010 或 2012或者手动改工程文件里的工具集版本。判断依赖是否干净看References节点里有没有System.Web、System.Net之外的第三方程序集。纯 WebSocket 客户端实现一般只依赖System、System.Net、System.Text再加一个System.Security用于 TLS。如果发现引用了Newtonsoft.Json之类说明它把消息序列化也包进去了这时候要确认那个 dll 的版本是否也兼容 .NET 2.0——很多新版 Json.NET 早就放弃 2.0 了。提示解压后先复制一份原始包留底所有改动在副本上做。老框架工程一旦被新版 VS 打开过工程文件可能被静默升级回不去。2.2 为什么是手写 WebSocket 而不是用现成库.NET 2.0 时代没有System.Net.WebSockets那个命名空间是 .NET 4.5 才有的。所以这份资源的实现方式必然是「自己处理握手 自己拆帧」。握手阶段就是发一个 HTTP Upgrade 请求带Sec-WebSocket-Key服务端回101 Switching Protocols之后的数据帧按 RFC 6455 的格式解析第一个字节是 FIN 和 opcode第二个字节是 MASK 和 payload 长度客户端发出去的数据必须做掩码处理服务端发回来的不带掩码。选型上要清楚它的边界这类手写实现通常只覆盖 RFC 6455 的核心部分——文本帧、二进制帧、ping/pong、关闭帧。扩展协议比如permessage-deflate压缩大概率不支持wss走的是SslStream包装。如果你的服务端强制要求压缩扩展这个库可能连不上得先确认服务端配置。2.3 编译前必须核对的三处工程配置打开.csproj逐项核对下面三个地方任何一处不对都会导致编译报错或运行时玄学问题。配置项正确值错误后果TargetFrameworkVersionv2.0 或 v3.5高版本框架下 API 行为差异PlatformTargetx86 或 AnyCPUx64 下指针操作可能越界AllowUnsafeBlockstrue若用到指针编译直接失败如果工程里用了unsafe代码块做字节操作AllowUnsafeBlocks必须为true。改完保存用对应版本的 VS 打开 sln先执行一次「重新生成解决方案」确认零错误再往下走。常见做法是先在 Debug 配置下编译通过再切 Release 做实际部署。3. 把连接跑起来握手、收发与心跳的代码落地3.1 建立连接URI 拼接与握手参数核心调用一般是一个客户端类构造时传入 URI然后调Connect()。URI 的 scheme 是ws或wss注意端口别写错很多服务端 WebSocket 和 HTTP 共用一个端口靠 Upgrade 区分也有单独开 8080、9001 的。下面是一段典型调用// 创建客户端实例URI 指向服务端 WebSocket 端点 var client new WebSocketClient(ws://192.168.1.100:9001/device); // 注册三个关键事件收到消息、连接关闭、发生错误 client.OnMessage (sender, msg) { // msg 是已解码的字符串直接进业务分发 Console.WriteLine(收到: msg); }; client.OnClose (sender, reason) { Console.WriteLine(连接关闭: reason); }; client.OnError (sender, ex) { Console.WriteLine(出错: ex.Message); }; // 发起连接内部会完成 HTTP Upgrade 握手 client.Connect();逻辑说明Connect()内部先拼 HTTP 请求头Host取自 URISec-WebSocket-Key是随机生成的 16 字节 Base64。服务端返回 101 后把底层NetworkStream交给帧解析器。参数上要注意 URI 末尾不要多加斜杠有些服务端做路径匹配时对/device和/device/区别对待会直接返回 404 导致握手失败。3.2 发送数据文本帧与二进制帧的区别发送接口通常分Send(string)和Send(byte[])两个重载。文本帧 opcode 是 0x1二进制帧是 0x2。工控场景里传结构体一般用二进制传 JSON 指令用文本。发送前库会自动加掩码这个不用你管但 payload 长度超过 125 字节时长度字段会扩展到 2 字节或 8 字节这部分实现如果有 bug大消息就会截断。// 发送文本指令适合 JSON 格式的控制命令 client.Send({\cmd\:\start\,\id\:42}); // 发送二进制数据适合定长结构体或传感器原始字节 byte[] payload new byte[] { 0x01, 0x02, 0x03, 0x04 }; client.Send(payload);参数说明文本发送时确保字符串是 UTF-8 编码库内部一般会做Encoding.UTF8.GetBytes。二进制发送时注意字节序如果服务端按大端解析而你的结构体是小端数据就全乱了。常见做法是在协议文档里明确字节序发送前用BitConverter统一转换。3.3 心跳与重连别让连接悄悄死掉WebSocket 长连接最怕中间网络设备静默断链——你以为还连着实际早就断了。标准做法是客户端定时发 ping 帧服务端回 pong。这份库如果暴露了Ping()方法就直接调没有的话就自己发一个约定好的文本心跳包。// 用定时器每 30 秒发一次心跳检测连接是否存活 var timer new System.Timers.Timer(30000); timer.Elapsed (s, e) { try { client.Send({\type\:\ping\}); } catch (Exception ex) { // 发送失败说明连接已断触发重连逻辑 Console.WriteLine(心跳失败准备重连: ex.Message); Reconnect(client); } }; timer.Start();逻辑说明心跳间隔要小于服务端或中间设备的空闲超时时间常见设备是 60 秒所以 30 秒发一次比较稳。重连逻辑里要有退避别一断就疯狂重试把服务端打挂。我一般用「首次 1 秒之后翻倍上限 30 秒」的策略。4. 避坑排查老框架下 WebSocket 最容易翻车的五个点4.1 握手返回 400 或 404现象Connect()抛异常抓包看到服务端返回 400 Bad Request 或 404。原因通常是 URI 路径不对或者请求头里缺了Upgrade: websocket、Connection: Upgrade这两个字段。有些手写实现把 header 名大小写写错HTTP 头虽然理论上不区分大小写但个别服务端框架解析时较真。解决先用浏览器或 curl 手动发一次 Upgrade 请求确认服务端路径和头字段要求再对照库里的握手代码逐行核对。4.2 收到大消息就断连现象小指令收发正常一传超过 125 字节的数据就断开或收到乱码。原因多半是帧长度解析没处理扩展长度字段。RFC 6455 规定 payload 长度 126 时后面跟 2 字节长度127 时跟 8 字节长度。如果实现里只读了 7 位就往下走大帧必然解析错。解决找到帧解析函数确认它对 126 和 127 两个分支都有处理并且字节序是大端。4.3 在 .NET 2.0 下 TLS 握手失败现象连wss://时抛AuthenticationException。原因是 .NET 2.0 默认只支持 TLS 1.0而现代服务端普遍要求 TLS 1.2。解决在Connect()之前显式设置ServicePointManager.SecurityProtocol但 .NET 2.0 的枚举里可能没有Tls12值这时候要么打系统补丁要么在SslStream的认证回调里放宽证书校验仅限内网可信环境。4.4 中文消息变问号现象服务端发来的中文显示为???。原因是编码没统一库内部可能默认用了Encoding.Default而不是 UTF-8。解决全局搜Encoding.的调用把解码处统一改成Encoding.UTF8。发送侧同理别依赖系统默认编码。4.5 关闭连接时线程卡死现象调Close()后程序不退出或者界面卡住。原因是关闭握手没走完底层接收线程还在阻塞读。解决关闭时先发关闭帧等对方回关闭帧或超时 3 秒后强制Abort()底层 socket再Join()接收线程并设一个超时。5. 进阶调优把这份库改造成能扛住产线环境的长连接组件5.1 接收缓冲区与粘包处理产线环境数据频率高一次Receive可能读到多个帧也可能只读到半个帧。这份库如果按「一次读一个完整帧」来写在高频场景下会频繁分配内存。我的做法是加一个环形缓冲区把 socket 读到的原始字节先塞进去再按帧头逐步消费。这样既解决了粘包又减少了 GC 压力。具体是在接收循环里维护一个byte[] buffer和int dataLen每次Read追加然后 while 循环尝试解析完整帧解析出一个就移走一个。5.2 用状态机管理连接生命周期别用一堆 bool 标志位记「是否已连接」「是否正在重连」那种写法迟早出竞态。改成枚举状态机Disconnected → Connecting → Connected → Closing → Disconnected。每次状态迁移都走一个统一入口在里面做资源分配和释放。这样心跳线程、重连线程、业务发送线程看到的状态永远一致不会出现「心跳以为连着、实际已断」的黑匣子情况。5.3 日志与抓包出问题时先看什么线上出问题第一件事是确认「断在哪一层」。我的习惯是三层日志握手阶段打请求头和响应码帧收发阶段打 opcode 和 payload 长度不打内容避免日志爆炸异常阶段打完整堆栈。配合 Wireshark 抓包过滤tcp.port 9001看 FIN 是谁先发的。如果是服务端先发 FIN说明是服务端主动踢人得查服务端的空闲超时配置如果是客户端先发那就是本地逻辑触发了关闭。5.4 一个可复用的重连封装把重连逻辑抽成一个独立方法带退避和最大重试次数。下面是我常用的骨架private int retryCount 0; private const int MaxRetry 10; private void Reconnect(WebSocketClient client) { if (retryCount MaxRetry) { // 超过上限上报监控并停止重试 Console.WriteLine(重连次数超限放弃); return; } // 退避时间1s, 2s, 4s ... 上限 30s int delay Math.Min(1000 * (int)Math.Pow(2, retryCount), 30000); retryCount; System.Threading.Thread.Sleep(delay); try { client.Connect(); retryCount 0; // 连上后重置计数 } catch { Reconnect(client); // 失败继续退避重试 } }参数说明MaxRetry按业务容忍度设产线设备一般 10 次够了再多说明服务端真挂了重试也没意义。退避上限 30 秒是经验值太短会打爆服务端太长恢复不及时。连上后一定要把retryCount归零否则下次断线直接触发上限。从那以后我每次接老框架的实时通信需求都先把这份库的帧解析和重连逻辑过一遍确认长度字段和退避策略没坑再上产线。希望帮到你。本文还有配套的精品资源点击获取