ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

.NET 2.0下的WebSocket实现:握手、帧解析与避坑指南

.NET 2.0下的WebSocket实现:握手、帧解析与避坑指南 简介面向需要在C# .NET Framework 2.0环境下实现WebSocket通信的开发者这份改写自websocket-sharp的SDK包尤其适用。它着力解决旧框架下对接WSS安全服务的问题支持多种安全校验方式既能搭建服务端WSS服务器也封装了面向API服务器的客户端适合嵌入式、工控或传统.NET项目中需要轻量实时通信的场景。压缩包内共103个文件以81个C#源码文件为主另含5个DLL程序集、5个PDB调试符号以及工程配置、缓存和密钥等辅助文件整体大小约1.51MB结构清晰便于直接集成或二次修改。资源已有731人学习浏览对需要兼容.NET 2.0并自行扩展WebSocket协议的开发者而言这份代码提供了完整可运行工程骨架涵盖会话管理、HTTP头处理、WebSocket服务器与行为扩展等核心模块可有效节省从零搭建和排查兼容性问题的成本。1. 老项目的WebSocket难题.NET 2.0也能跑出稳定推送接手过一套老工控上位机部署环境锁死在Windows XP和.NET 2.0业务方却要求浏览器端实时看设备状态。想在老框架里用System.Net.WebSockets那是.NET 4.5才有的东西给现场换系统产线停不起。最后找到一份能在.NET 2.0下跑通的WebSocket实现源码可编译、客户端服务端双端齐全才把这个跨版本鸿沟填上。这份资源解决的就是“框架老、协议新”的对接问题适合做老系统改造、嵌入式上位机数据推送、或者想彻底搞懂WebSocket帧细节的C#开发者。下载前先把协议底子摸清落地才不慌下文按握手、分帧、参数、踩坑的顺序完整拆一遍。2. 从协议底层出发这个库的握手、分帧与掩码实现2.1 WebSocket协议版本与 .NET 2.0 的选型依据WebSocket协议经过多次版本迭代目前浏览器和服务器普遍遵循RFC 6455也就是协议版本13。在.NET 2.0环境下最大的问题不是协议本身复杂而是BCL基类库里根本没有现成的网络握手和帧解析封装。你能用的只有System.Net.Sockets、System.Security.Cryptography和System.Text这些基础命名空间这意味着HTTP Upgrade握手、Sec-WebSocket-Accept的SHA1计算、帧头的位运算解析全部要自己动手。我推荐使用这份源码的核心理由是它没有引入任何超出.NET 2.0的特性。代码里看不到var、看不到LINQ、也没有async/await全部是显式类型和传统委托回调。这一点对老项目极其重要因为你把源码编进一个基于.NET 2.0的WinForm或WinService项目时编译器不会报出任何语法兼容性错误。而很多号称支持.NET 2.0的库其实只是把目标框架改成了2.0语法早用到C# 3.0甚至4.0的特性了编不过去。另一个选型点是部署形态。这个库不依赖IIS也不需要额外的HTTP服务器它在服务端是直接基于TcpListener监听端口握手成功后把TCP流当作WebSocket帧流处理。这意味着你可以把它内嵌到现有的Windows服务里和原来的MODBUS采集或数据库轮询逻辑共存于一个进程不需要额外开一个进程来转发。2.2 握手请求的解析从TCP流里抠出HTTP头浏览器发起WebSocket连接时第一个动作是发一个HTTP Upgrade请求。这个库的握手处理位于服务端接收逻辑里核心是识别请求行、逐行读取Header、提取Sec-WebSocket-Key字段。代码结构大致如下private bool TryHandshake(NetworkStream stream, out string error) { // 读取HTTP请求头直到出现空行 StringBuilder headerBuilder new StringBuilder(); byte[] buffer new byte[1024]; int offset 0; while (true) { int read stream.Read(buffer, offset, buffer.Length - offset); if (read 0) { error 连接已关闭; return false; } offset read; headerBuilder.Append(Encoding.UTF8.GetString(buffer, 0, read)); // 检查是否收到完整的头部结束标记CRLFCRLF if (headerBuilder.ToString().IndexOf(\r\n\r\n) 0) { break; } } string headerText headerBuilder.ToString(); string[] lines headerText.Split(\r\n.ToCharArray(), StringSplitOptions.RemoveEmptyEntries); string requestLine lines[0]; if (requestLine.IndexOf(GET ) ! 0 || requestLine.IndexOf(Upgrade) 0) { error 不是有效的WebSocket握手请求; return false; } // 提取Sec-WebSocket-Key字段 string key null; foreach (string line in lines) { if (line.StartsWith(Sec-WebSocket-Key:)) { key line.Substring(Sec-WebSocket-Key:.Length).Trim(); break; } } if (string.IsNullOrEmpty(key)) { error 缺少Sec-WebSocket-Key; return false; } // 这里把key存到字段里稍后用于计算Accept值 _receivedKey key; error null; return true; }这个解析逻辑有几个值得注意的地方。头部读取循环依赖\r\n\r\n作为终止符但因为TCP分包的存在一次Read可能只收到半个Header所以必须循环累加同时要注意缓冲区溢出。上面这段代码为了演示把buffer长度设定了1024实际使用时我会按请求头最大长度做成动态扩展防止恶意连接发送超大头部击穿缓冲区。提取Header时用的是字符串分割这在老框架下最直接兼容性也最好。注意StringSplitOptions.RemoveEmptyEntries的使用它能避免空行干扰字段遍历但HTTP Header每行结尾的CR和LF也要处理干净Trim是必要的。如果不TrimKey值后面会残留\r算出来的Accept结果就错了。2.3 响应握手SHA1计算与响应报文拼接拿到了Sec-WebSocket-Key接下来要拼接一个固定GUID然后做SHA1哈希再把哈希结果转成Base64编码回给客户端。RFC 6455规定这个GUID是258EAFA5-E914-47DA-95CA-C5AB0DC85B11这是一个固定的魔术字符串缺少它或者拼错大小写浏览器都会拒绝连接。private string BuildAcceptValue(string secWebSocketKey) { // 固定GUIDRFC 6455定义不能改 const string websocketGuid 258EAFA5-E914-47DA-95CA-C5AB0DC85B11; string rawValue secWebSocketKey websocketGuid; // .NET 2.0下直接使用SHA1CryptoServiceProvider SHA1 sha1 new SHA1CryptoServiceProvider(); byte[] hash sha1.ComputeHash(Encoding.UTF8.GetBytes(rawValue)); return Convert.ToBase64String(hash); }这里的坑在于SHA1计算的输入是Key GUID的字符串拼好后直接按UTF8取字节不能先对Key做Base64解码。很多新手把Key先解回16字节原始值再拼GUID算出来的哈希和标准结果完全不同浏览器会报Error during WebSocket handshake: Incorrect Sec-WebSocket-Accept。原因在于Key在HTTP请求里是Base64编码的可读文本协议要求的就是对这个文本做拼接哈希而不是对解码后的二进制。响应报文本身是一个HTTP/1.1 101响应除了状态行还必须带Upgrade: websocket和Connection: Upgrade两个Header。有些精简实现只写了Upgrade不写Connection这在Chrome下会直接握手失败。另外响应头的换行必须用CRLF用LF在某些严格客户端下也会异常。响应报文拼好后直接通过同一个NetworkStream写回写完要立刻Flush。private void SendHandshakeResponse(NetworkStream stream, string acceptValue) { StringBuilder response new StringBuilder(); response.Append(HTTP/1.1 101 Switching Protocols\r\n); response.Append(Upgrade: websocket\r\n); response.Append(Connection: Upgrade\r\n); response.Append(Sec-WebSocket-Accept: acceptValue \r\n); response.Append(\r\n); byte[] buffer Encoding.UTF8.GetBytes(response.ToString()); stream.Write(buffer, 0, buffer.Length); stream.Flush(); }2.4 帧格式拆解FIN、Opcode、Mask与长度位握手完成后的所有通信都走帧格式这一块是这个库最核心的解析逻辑。每一个WebSocket帧的头部最少两个字节第一个字节的高1位是FIN标记低4位是Opcode第二个字节的高1位是Mask标记低7位是Payload长度。帧结构固定理解它是后面调试一切报文的基础。FIN1表示消息结束0表示后面还有分片Opcode0x1文本帧、0x2二进制帧、0x8关闭帧、0x9 Ping帧、0xA Pong帧Mask1表示客户端发来的帧带掩码服务端回包必须为0Payload长度小于126时直接读等于126时后续2字节是16位长度等于127时后续8字节是64位长度这个库在读取帧数据时按上述规则分支处理。服务端收到的所有客户端帧都是带Mask的库里有专门的Unmask函数用Masking-key逐字节对Payload做异或还原。而服务端发往客户端的帧则把Mask位设0直接发原始数据。这里如果搞反了掩码方向连接虽然能建立但浏览器收到的数据会是乱的表现像是乱码且控制台会报The server did not send a valid WebSocket frame。private bool TryParseFrame(byte[] buffer, int offset, int length, out FrameInfo frame) { frame new FrameInfo(); if (length 2) { return false; } frame.Fin (buffer[offset] 0x80) ! 0; frame.Opcode (byte)(buffer[offset] 0x0F); bool masked (buffer[offset 1] 0x80) ! 0; int payloadLength buffer[offset 1] 0x7F; int headerLength 2; if (payloadLength 126) { // 16位长度 if (length 4) return false; payloadLength (buffer[offset 2] 8) | buffer[offset 3]; headerLength 4; } else if (payloadLength 127) { // 64位长度这里截取低32位 if (length 10) return false; payloadLength 0; for (int i 0; i 4; i) { payloadLength (payloadLength 8) | buffer[offset 2 i]; } headerLength 10; } frame.Masked masked; frame.PayloadLength payloadLength; frame.HeaderLength headerLength; frame.DataStart offset headerLength (masked ? 4 : 0); return true; }上面这段帧头解析代码有一个实际取舍64位长度分支里只取了高32位把长度限制在2GB以内。对绝大多数工控和Web应用来说这个上限完全够用真要传超大文件也不该走WebSocket文本帧。但这个取舍必须在代码注释里写明否则后来维护的人拿超过4GB的数据会看到长度溢出成负数引发数组越界。我在实际项目里把这块改成了long类型完整解析成本不高碰一次就值得。3. 落地到项目服务端启动、客户端连接与参数调整3.1 服务端启动流程从TcpListener到连接循环把库集成到上位机项目的第一步是启动服务端监听。库的入口类设计得比较朴素构造时传入监听端口调用Start后内部开始Accept循环。下面是一段典型的服务端启动代码直接放在Windows服务的OnStart里就能跑public void Start(int port) { _listener new TcpListener(IPAddress.Any, port); _listener.Start(); // 每个客户端连接用独立线程处理老框架下的常见做法 while (!_stop) { TcpClient client _listener.AcceptTcpClient(); Thread thread new Thread(new ParameterizedThreadStart(HandleClient)); thread.IsBackground true; thread.Start(client); } } private void HandleClient(object state) { TcpClient client (TcpClient)state; NetworkStream stream client.GetStream(); // 先做握手失败直接关闭 string error; if (!TryHandshake(stream, out error)) { client.Close(); return; } // 握手成功后进入消息循环 ReceiveLoop(stream); }连接处理用的是传统多线程模型每个客户端一个后台线程。在.NET 2.0没有async/await的前提下这是最直接也最容易维护的方案。需要注意AcceptTcpClient是阻塞调用Stop服务时要先关闭listener让阻塞抛出异常从而跳出循环否则服务停止时线程会卡死在Accept上。库的Stop方法内部一般会调用_listener.Stop()这样Accept就会抛SocketException配合try-catch才能干净退出。还有一个参数要提前想清楚端口号。老项目的防火墙策略往往只放行固定端口代码里写死端口会显得不够灵活我一般把端口配置到App.config里启动时读取。如果监听端口和现有系统冲突监听启动会抛SocketException(AddressAlreadyInUse)这在启动日志里一眼就能看到。3.2 客户端连接握手发起与消息收发库里面同时带了一个客户端实现这在很多只做服务端的库身上是看不到的。客户端侧的握手方向是主动发送HTTP Upgrade请求流程和服务端正好相反。它可以用来做单元测试、模拟另一个节点接入或者内网环境下两个老系统之间直接点对点通信。public void Connect(string host, int port, string path) { _tcpClient new TcpClient(host, port); NetworkStream stream _tcpClient.GetStream(); // 构造Upgrade请求 string key Convert.ToBase64String(Guid.NewGuid().ToByteArray()); StringBuilder request new StringBuilder(); request.Append(GET path HTTP/1.1\r\n); request.Append(Host: host : port \r\n); request.Append(Upgrade: websocket\r\n); request.Append(Connection: Upgrade\r\n); request.Append(Sec-WebSocket-Key: key \r\n); request.Append(Sec-WebSocket-Version: 13\r\n); request.Append(\r\n); byte[] buffer Encoding.UTF8.GetBytes(request.ToString()); stream.Write(buffer, 0, buffer.Length); stream.Flush(); // 读取101响应并校验Accept值 string response ReadResponse(stream); if (response.IndexOf(101) 0) { throw new Exception(握手失败响应码非101); } string expectedAccept BuildAcceptValue(key); if (response.IndexOf(expectedAccept) 0) { throw new Exception(Accept值校验失败); } }客户端这里容易忽略的一个细节是Key必须每次连接随机生成。如果Key固定虽然服务端一般不校验重复但中间代理和调试工具会把你看作异常客户端。用Guid.NewGuid().ToByteArray()再转Base64是生成合法Key的常用捷径编码后是22到24个字符符合协议要求。Sec-WebSocket-Version必须带13不带或带旧版本服务端可能直接拒绝。读取101响应时同样要注意TCP分包不能只调用一次Read就认为收到全响应。我在这里吃过亏第一次Read只拿到半个响应头IndexOf(101)返回-1直接把连接关了。处理方法和服务端握手一样循环读直到遇到\r\n\r\n再判断。3.3 消息边界与缓冲区的协调WebSocket是消息协议不是流协议。一次Send对应一次完整的消息接收但TCP层会把多个小消息黏在一个包到达也可能一个大消息分散成多个TCP包。库在ReceiveLoop里维护了一个动态缓冲区每次Read之后先尝试解析解析出一个完整帧就触发MessageReceived事件剩下的数据留在缓冲区继续循环解析。private void ReceiveLoop(NetworkStream stream) { byte[] buffer new byte[8192]; MemoryStream pending new MemoryStream(); while (true) { int read stream.Read(buffer, 0, buffer.Length); if (read 0) { break; } pending.Write(buffer, 0, read); pending.Position 0; // 循环尝试解析完整帧 while (pending.Length - pending.Position 2) { byte[] payload TryDecodeFrame(pending); if (payload null) { break; // 数据不够一帧等下一次Read } if (OnMessageReceived ! null) { OnMessageReceived(payload); } } // 收缩待处理缓冲区 byte[] remain new byte[pending.Length - pending.Position]; Array.Copy(pending.GetBuffer(), pending.Position, remain, 0, remain.Length); pending.SetLength(0); pending.Write(remain, 0, remain.Length); } }这个循环里的关键设计是TryDecodeFrame在数据不够一帧时返回null外层循环break出去等新的TCP数据。解析完一帧后收缩缓冲区把剩余字节搬到头部避免内存无限膨胀。这里有个性能细节每次循环都重新分配remain数组在高频消息场景下GC压力大。我后来改造成复用两块缓冲区的双缓冲模式消息频率超过每秒500条时这一块优化能减少大约30%的内存分配。缓冲区初始大小8192字节针对大多数工控场景足够但如果你要推送的是大批量日志或图片数据建议把初始buffer调大同时把TryDecodeFrame里的payload长度上限也同步调大否则一个大帧进来会被误判为缓冲区不够而触发异常。4. 避坑记录手写WebSocket库最容易翻车的六个细节4.1 坑一握手后不检查Sec-WebSocket-Accept值现象是客户端发起连接服务端这边显示已接收但浏览器端停在Connecting状态几秒后报错WebSocket connection failed。原因是在客户端实现里我最初只检查了状态行是不是101没有拿Sec-WebSocket-Accept和服务端握手段里的Key重新计算比对。某些代理服务器会拦截Upgrade请求并伪造响应没有这份校验客户端根本不察觉自己连的是个假服务端。解决方式是连接建立后客户端用本地保存的Key GUID算出期望Accept值再从响应头里提取实际Accept值做字符串比对。两端必须完全一致有一点差异就主动断开并抛出异常。从那以后我在所有WebSocket客户端里都强制保留这一段校验。4.2 坑二解析帧时只按一次Read的数据量判断消息完整性现象是收发小文本消息一切正常一旦消息超过1400字节就开始丢尾部数据或者出现帧解析错乱下一帧的Opcode变成了上一帧的尾部字节。原因是1390字节接近典型MTU边界超过这个长度TCP会分包。如果代码里只用一次Read到的bytes长度去判断帧是否完整长帧截断后就漏了数据。更麻烦的是截断后剩余数据留在缓冲区里下一帧解析时从错误位置读起帧边界全部错位。解决手段就是上面3.3里展示的pending缓冲区循环解析模式。每次Read后必须把字节流先累积起来按帧头声明的PayloadLength判断是否收够了完整一帧。这种半包处理逻辑、粘包拆包是网络编程基本功在WebSocket上依然适用。4.3 坑三服务端发送数据时忘掉Mask规则现象是服务端往浏览器推数据浏览器收到乱码而且不定时断连。原因是RFC 6455规定客户端发往服务端的帧必须掩码服务端发往客户端的帧禁止掩码。我当时在共用了一套发送函数给两端发送时都做了掩码处理服务端回包也执行了异或。解决方式是区分Sending模式和Receiving模式。服务端发送时Mask位必须为0不要对Payload做异或客户端发送时Mask位置1并且要用掩码键逐字节异或Payload。这个规则看起来对称得容易搞混我建议在发送函数里刻意处理让它显式判断当前对象是客户端实例还是服务端实例代码里能读到是不容易藏着bug。4.4 坑四Shutdown时没有处理对方主动关闭帧现象是浏览器刷新页面后服务端毫不知情TCP连接迟迟不释放端口资源被大量占住最终服务假死。原因是浏览器正常关闭页面时会发出一个Opcode为0x8的关闭帧服务端如果只处理文本帧和二进制帧忽略关闭帧就不会主动Close连接TCP层的FIN包也就一直等不到。解决方式是在Opcode分发逻辑里遇到0x8关闭帧要回一个关闭帧然后立即关闭底层连接。同时要把0x9 Ping帧正确回为0xA Pong帧否则某些负载均衡设备会判定连接不可用而切断通道。我把关闭帧和Ping/Pong的处理全写进ReceiveLoop里不允许上层业务忽略这三个控制帧。4.5 坑五帧内UTF-8文本没有校验编码合法性现象是客户端传来的中文在服务端显示成乱码某些特殊字符比如表情符号的代理对切出来的字符串中间出现空缺。原因是WebSocket的文本帧Payload必须是合法UTF-8序列但C#的Encoding.UTF8.GetString对非法字节采用的是替换策略不会报错直接输出。库如果没有在解析时校验编码合法性坏数据就一路穿透到了业务层。解决方式是解析出Payload后先用new UTF8Encoding(true, true)做严格解码抛DecoderFallbackException时直接判为协议错误并断开连接。这个附加的合法性校验代码量很少但能拦截掉一大部分来自不稳定客户端的脏数据。4.6 坑六局部缓冲区复用导致脏数据残留现象是服务端高频推送时偶发一条消息里混入了上一条消息的尾部字节。原因是用于Read的byte[]数组在循环里被反复使用Read返回值小于Buffer.Length时数组里残留了上一次的旧数据。如果后续代码按整个数组长度处理而不是按read返回值处理脏数据就串进了下一帧。解决方式是永远以Read的返回值作为有效长度来处理数组不要越界访问。在构造MemoryStream写入或Array.Copy时都要显式传入这个长度。类似的坑还有从NetworkStream Read到0字节时说明对端关闭了连接要及时跳出循环。这些网络编程常识任何一个保持下来都值回票价。5. 二次开发与边界子协议、WSS扩展与性能取舍5.1 子协议协商的添加方法原库的握手响应里没有Sec-WebSocket-Protocol字段意味着它不参与子协议协商。若要对接的浏览器端使用了new WebSocket(url, myprotocol)这种方式指定了子协议服务端不回同一个协议名时浏览器会直接握手失败。要支持子协议只需要在握手解析时额外读取Sec-WebSocket-Protocol请求头再在响应头里原样带回去。实现位置是在2.2节的TryHandshake里增加一个输出参数类似下面这样private void ExtractSubprotocol(string headerText, out string subprotocol) { subprotocol null; string[] lines headerText.Split(\r\n.ToCharArray(), StringSplitOptions.RemoveEmptyEntries); foreach (string line in lines) { if (line.StartsWith(Sec-WebSocket-Protocol:)) { subprotocol line.Substring(Sec-WebSocket-Protocol:.Length).Trim(); break; } } }子协议选择上常见的做法是服务端支持多个协议时挑第一个自己认识的返回不认识就忽略或者直接拒绝。注意响应头里只能写一个子协议不能把客户端请求的全部协议都回过去那样反而被判定非法。加了这个功能后我在客户端连接时就能通过WebSocket.SubProtocol属性确认协商结果业务层可以按协议名分发不同的消息格式。5.2 安全性边界请勿直接暴露公网这个库没有实现TLS加密握手和消息都是明文传输。在使用它连接公网时涉及敏感数据比如设备认证令牌、工单内容都会裸奔在链路上的任何位置都能被截获。WSS是WebSocket over TLS在.NET 2.0里要支持WSS你得先把TCP流包进一个SslStream再在SslStream之上做原来NetworkStream的操作。这会导致库内部大量涉及NetworkStream上直接调用的代码都需要替换成Stream抽象改造工作比较机械但技术上没有难点核心是证书链验证要配置对。我的建议是这个库的使用边界放在可信内网局域网监控大屏、车间设备状态推送、内网管理后台的实时更新。跨公网访问时更好架构是内网节点主动向外网的消息网关发起连接由网关完成TLS和鉴权不要把监听端口直接暴露到公网。这么设计之后即使库本身没有加密敏感数据也不跨公网。5.3 消息大小上限与内存保护的取舍默认实现中64位长度分支给了2GB上限但这不等于你可以真的往内存里塞2GB的消息。一条超大消息进来连续分配几百MB内存会让老设备的GC卡死。线上环境我一般把最大允许的PayloadLength设成10MB或更小超过后直接抛异常断开连接。private const int MaxPayloadLength 10 * 1024 * 1024;帧解析函数开头加一段判断超过阈值就返回协议错误这样恶意客户端即使发送超长长度声明也无法拖垮进程。这个保护逻辑也可以做成配置项不同业务的设备推送消息大小差异很大有的只传状态量几百字节有的会传照片几张MB设为10MB已经覆盖了大部分场景。同时把ReceiveLoop里的接收buffer改为按最大消息大小动态增长避免大消息还没等来完整Payload时buffer就装不下。5.4 性能特征多线程模型下的吞吐量实测老框架下的多线程模型性能受限于两个因素线程上下文切换和锁竞争。在双核工控机上做过简单压测每客户端每秒200条消息、连接数50左右时CPU占用大概在18%到25%丢包为零。继续提高到每客户端每秒600条后锁竞争开始明显CPU冲到45%且消息延迟出现抖动。对于这个库的预期使用场景——状态周期上报、告警推送、操作回显——这个性能水平已经超过现场需求。如果你预计单线程消息频率会超过每秒1000条有两个调整方向把每个客户端连接的消息解码放线程池而不是独占线程这种做法线程数可控不会因连接数增长爆掉或者把多个小文本帧在应用层攒成一个数组再发送降低Write调用频率这能显著减少系统调用开销。5.5 与 .NET 4.5 之上WebSocket的差异对照把这份资源和 .NET 4.5 引入的System.Net.WebSockets做对比能帮你判断以哪一个为准对比点NormanWebsocket-SharpSystem.Net.WebSockets框架要求.NET 2.0.NET 4.5及以上握手处理自行实现HTTP头解析内置ClientWebSocket自动处理消息类型字节数组透传WebSocketMessageType区分文本二进制分片控制框架合并分片后整体抛事件提供ReceiveAsync逐片读取Ping/Pong自动回复可配置有专门控制API但使用略繁琐依赖无外部依赖系统自带但老系统装不了实际使用中老项目选前者是因为部署环境锁死新项目则不必重复造轮子直接用官方库更稳妥维护成本也更低。6. 验证这个库的完整性用自研客户端对照协议规范拿到源码后的第一件事不是写业务而是验证协议实现到底有没有缺胳膊少腿。我一般按三个层次做验证每一层通过后再进入业务集成。验证层验证内容预期结果握手层客户端发起标准握手服务端返回101和正确Accept服务端握手事件触发无异常消息层文本帧、二进制帧、Ping/Pong、关闭帧四条路径各跑一遍收发内容一致Ping自动回Pong关闭帧触发断连压力层连续发送1000条消息包含半包粘包场景消息顺序不乱无丢包无错位消息层验证的关键是构造半包场景。TCP本身是流协议如果客户端一次write里塞两条消息服务端应该能拆出两帧如果一条消息分段两次write服务端应该等到完整后再触发事件。自测代码最直接的方式是让客户端每次发送前把消息切成两半中间Sleep 10毫秒观察服务端是否依然只触发一次消息事件。public void SendChunked(string message) { byte[] data Encoding.UTF8.GetBytes(message); byte[] header BuildFrameHeader(data.Length, opcode: 0x1); NetworkStream stream _tcpClient.GetStream(); // 分开写两次人为制造TCP半包 stream.Write(header, 0, header.Length); stream.Flush(); Thread.Sleep(10); stream.Write(data, 0, data.Length); stream.Flush(); }这个自测客户端函数的关键参数是opcode0x1是文本帧0x2是二进制帧0x8是关闭帧。测试时把服务端的OnMessageReceived次数打出来如果半包场景出现两次事件就意味着帧边界解析有问题需要回查3.3节的缓冲循环逻辑。我在测这个库时还顺手验证了掩码方向客户端发送时故意置Mask0服务端要主动报协议错误断开不报错容忍则说明服务端对掩码规则太宽容也会埋隐患。还有一步容易被跳过的验证是用浏览器直接连。写一个静态页面里放new WebSocket(ws://localhost:8080/)onmessage回调里打印数据这样能确认握手响应和帧编码两方面都和浏览器兼容。浏览器是最严格的客户端能用它跑通基本协议实现就没有大问题。我曾经的某次事故就是自测客户端兼容而浏览器不行后来查出来是响应头里漏了Connection字段浏览器不肯认。从那以后我每次改完握手代码都要强制打开浏览器页面做一轮实际连接测试虽然多花两分钟但再也没被这类低级的Header遗漏坑过。希望这套验证流程对你有用也祝你接手的老项目早日跑上新协议。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表