.NET控制台WebSocket服务器未接收客户端全部消息问题排查
嘿,我帮你找出消息丢失的核心问题了!主要是你的WebSocket服务器在处理TCP流和WebSocket消息的逻辑上有几个关键漏洞,导致快速发送的消息大量丢失,具体原因和修复方法如下:
为啥消息会丢?
1. 没处理「一次收到多条消息」的情况
你客户端一口气发1000条消息,TCP为了省流量,会把好多小消息打包成一个TCP段发给服务器。但你的服务器每次读取完当前可用的字节后,只解析第一条WebSocket消息,剩下的消息字节直接就丢了——因为下一次循环又重新读取新数据,根本没管上次剩下的内容。
举个例子:服务器一次读了3条消息的字节,结果只处理第一条,剩下两条的字节数据直接被抛弃,自然就丢了。
2. 依赖client.Available读数据不靠谱
你用client.Available来创建字节数组然后读取数据,但这个值只是当前瞬间可用的字节数,TCP流是连续的,Read方法实际读到的字节数可能比这个少(比如数据还在网络路上没到全)。这就可能导致部分数据没被正确读取,或者后续新数据覆盖了之前没处理的旧数据。
3. 没保留未处理的剩余字节
每次处理完一条消息,如果当前读取的字节里还有属于下一条消息的剩余数据,你的代码没把这些数据存下来,直接进入下一轮循环重新读,这部分数据就直接丢失了。
怎么修复?
核心思路是维护一个全局缓冲区,把所有读到的字节都存起来,然后循环从缓冲区里解析完整的WebSocket消息,直到缓冲区里的字节不够解析下一条为止。
第一步:加个缓冲区存未处理数据
在Main函数开头声明一个列表当缓冲区:
List<byte> messageBuffer = new List<byte>();
第二步:把读到的数据都追加到缓冲区
替换原来的读取逻辑,别直接处理单次读取的字节,先存到缓冲区里:
// 替换原来的bytes数组读取代码 byte[] tempBuffer = new byte[1024]; int bytesRead = stream.Read(tempBuffer, 0, tempBuffer.Length); if (bytesRead > 0) { messageBuffer.AddRange(tempBuffer.Take(bytesRead)); }
第三步:循环解析缓冲区里的消息
只要缓冲区里的字节够解析一条消息的头部,就尝试解析,直到不够为止:
// 循环解析缓冲区中的完整消息 while (messageBuffer.Count >= 2) // WebSocket消息至少有2字节头部 { bool isFinalFrame = (messageBuffer[0] & 0b10000000) != 0; int opcode = messageBuffer[0] & 0b00001111; bool hasMask = (messageBuffer[1] & 0b10000000) != 0; int msgLength = messageBuffer[1] & 0b01111111; int offset = 2; // 处理不同长度的消息 if (msgLength == 126) { if (messageBuffer.Count < 4) break; // 长度字节不够,等更多数据 msgLength = BitConverter.ToUInt16(new byte[] { messageBuffer[3], messageBuffer[2] }, 0); offset = 4; } else if (msgLength == 127) { if (messageBuffer.Count < 10) break; // 64位长度需要更多字节,等一等 // 注意RFC 6455规定长度是大端字节序,所以要反转 msgLength = (int)BitConverter.ToUInt64(messageBuffer.Skip(2).Take(8).Reverse().ToArray(), 0); offset = 10; } // 检查缓冲区里的字节是否足够包含掩码和消息内容 int totalNeededBytes = offset + (hasMask ? 4 : 0) + msgLength; if (messageBuffer.Count < totalNeededBytes) break; // 数据不全,等后续数据 // 解码消息内容 if (hasMask) { byte[] maskKey = messageBuffer.Skip(offset).Take(4).ToArray(); offset += 4; byte[] decodedBytes = new byte[msgLength]; for (int i = 0; i < msgLength; i++) { decodedBytes[i] = (byte)(messageBuffer[offset + i] ^ maskKey[i % 4]); } string receivedMsg = Encoding.UTF8.GetString(decodedBytes); Console.WriteLine(receivedMsg); } else { // 按照RFC 6455,客户端发的消息必须带掩码,这里可以打个日志 Console.WriteLine("警告:收到不带掩码的客户端消息,违反协议规范"); } // 移除已经处理完的字节,剩下的留着下次解析 messageBuffer.RemoveRange(0, totalNeededBytes); }
第四步:调整主循环逻辑
把原来处理消息的分支替换成上面的缓冲区解析逻辑,确保所有读到的数据都被累积和处理,不会漏掉。
额外提醒
- TCP是面向流的,没有消息边界,必须靠WebSocket的帧结构来拆分消息,不能想当然认为一次读取就是一条消息。
- 要符合RFC 6455规范:客户端发的消息必须带掩码,服务器发的消息不能带掩码,别搞反了。
- 如果要处理超大量消息,可以调整缓冲区的初始大小,减少内存分配的开销。
内容的提问来源于stack exchange,提问作者millie

