不同浏览器WebSocket协议实现存在差异?自研回显服务器遇异常
WebSocket回显服务器在Opera/Yandex浏览器握手后收到未知数据的问题修复
你遇到的问题根源是:部分浏览器(Opera/Yandex)在WebSocket握手完成后会立即发送Ping控制帧(WebSocket协议规定的连接保活机制),而你的代码没有正确解析WebSocket帧的类型,把控制帧当成数据帧处理,导致出现“未知字节”。同时代码中还存在多处WebSocket协议解析的逻辑错误,进一步放大了问题。
核心修复点
必须解析WebSocket帧的Opcode:
WebSocket帧第一个字节的低4位是Opcode,用来区分帧类型:- 0x1:文本数据帧
- 0x2:二进制数据帧
- 0x8:关闭帧
- 0x9:Ping帧
- 0xA:Pong帧
需先判断Opcode再执行对应逻辑,避免把控制帧当成数据处理。
正确处理ReceiveAsync的返回值:
ReceiveAsync会返回实际接收的字节数,必须用这个值确定有效数据范围——TCP是流式传输,可能分多次接收一个完整帧,不能直接假设整个缓冲区都有数据。修正掩码与Payload解析逻辑:
客户端发送的帧带4字节掩码,你的代码之前掩码截取位置完全错误;同时Payload长度计算、掩码解码循环的条件也存在错误,需严格遵循WebSocket协议规范。
修正后的完整代码
using System; using System.Net; using System.Net.Sockets; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; class WS { private readonly string GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; private readonly string ip; private readonly int port; byte[] Segment { get; } = new byte[1024]; // 增大缓冲区避免握手请求截断 public Socket socket { get; set; } public WS(int port, string ip) { this.port = port; this.ip = ip; IPAddress[] ipAddress = Dns.GetHostAddresses(this.ip); IPEndPoint ipLocalEndPoint = new IPEndPoint(ipAddress[0], this.port); this.socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); this.Bind(ipLocalEndPoint); this.Listen(); } private byte[] GetHandshake(string key) { string acceptSHA1 = key + GUID; byte[] encodingBytes = Encoding.ASCII.GetBytes(acceptSHA1); byte[] hash = SHA1.HashData(encodingBytes); var hashString = Convert.ToBase64String(hash); byte[] msg = Encoding.UTF8.GetBytes( "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Accept: " + hashString + "\r\n\r\n"); Console.WriteLine(Encoding.UTF8.GetString(msg)); return msg; } public async Task<int> GetKeyWS(Socket acceptData) { // 接收握手请求,读取完整HTTP头 int receivedBytes = await acceptData.ReceiveAsync(this.Segment); string handshakeStr = Encoding.UTF8.GetString(Segment, 0, receivedBytes); HttpInfo headersKey = new HttpInfo(handshakeStr); byte[] buffer = GetHandshake(headersKey.GetHeaderValue("Sec-WebSocket-Key")); await acceptData.SendAsync(buffer); byte[] frameBuffer = new byte[65536]; while (true) { // 获取实际接收的帧字节数 int frameLength = await acceptData.ReceiveAsync(frameBuffer); if (frameLength == 0) break; // 解析帧头:FIN位与Opcode byte firstByte = frameBuffer[0]; byte opcode = (byte)(firstByte & 0x0F); // 解析Payload长度与掩码位 byte secondByte = frameBuffer[1]; bool hasMask = (secondByte & 0x80) != 0; int payloadLength = secondByte & 0x7F; int offset = 2; byte[] mask = null; // 处理不同长度的Payload if (payloadLength == 126) { payloadLength = BitConverter.ToInt16(new byte[] { frameBuffer[3], frameBuffer[2] }, 0); offset += 2; } else if (payloadLength == 127) { payloadLength = (int)BitConverter.ToInt64(new byte[] { frameBuffer[7], frameBuffer[6], frameBuffer[5], frameBuffer[4], frameBuffer[3], frameBuffer[2], frameBuffer[1], frameBuffer[0] }, 0); offset += 8; } // 提取客户端帧的掩码 if (hasMask) { mask = new byte[4]; Array.Copy(frameBuffer, offset, mask, 0, 4); offset += 4; } // 根据Opcode处理不同帧类型 switch (opcode) { case 0x1: // 文本帧 case 0x2: // 二进制帧 // 解码Payload byte[] decodedPayload = new byte[payloadLength]; for (int i = 0; i < payloadLength; i++) { decodedPayload[i] = (byte)(frameBuffer[offset + i] ^ mask[i % 4]); } // 构造回显帧(服务器发送无需掩码) byte[] responseFrame = new byte[2 + payloadLength]; responseFrame[0] = (byte)(0x80 | opcode); responseFrame[1] = (byte)payloadLength; Array.Copy(decodedPayload, 0, responseFrame, 2, payloadLength); await acceptData.SendAsync(responseFrame); break; case 0x8: // 关闭帧 Console.WriteLine("收到关闭请求"); byte[] closeFrame = new byte[] { 0x88, 0x00 }; await acceptData.SendAsync(closeFrame); acceptData.Close(); return 0; case 0x9: // Ping帧:回复Pong Console.WriteLine("收到Ping帧,回复Pong"); byte[] pongFrame = new byte[frameLength]; Array.Copy(frameBuffer, pongFrame, frameLength); pongFrame[0] = 0x8A; // 修改Opcode为Pong await acceptData.SendAsync(pongFrame); break; case 0xA: // Pong帧:无需处理 Console.WriteLine("收到Pong帧"); break; default: Console.WriteLine("收到未知Opcode帧: " + opcode); break; } } return 0; } private void Bind(IPEndPoint ep) { this.socket.Bind(ep); } private void Listen() { this.socket.Listen(); } } // 简化的HTTP头解析类 internal class HttpInfo { private readonly string _headerStr; public HttpInfo(string headerStr) { _headerStr = headerStr; } public string GetHeaderValue(string headerName) { string[] lines = _headerStr.Split(new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries); foreach (string line in lines) { if (line.StartsWith(headerName + ": ", StringComparison.OrdinalIgnoreCase)) { return line.Substring(headerName.Length + 2).Trim(); } } return null; } } internal class Program { static async Task<int> Main() { WS ws = new(7878, "192.168.0.2"); try { while (true) { var acceptAsync = await ws.socket.AcceptAsync(); _ = Task.Run(() => ws.GetKeyWS(acceptAsync)); } } catch (Exception ex) { Console.WriteLine(ex.Message); return 0; } } }
额外优化说明
- 调整
HttpInfo类的初始化方式,用字符串解析HTTP头,避免字节数组截断导致的解析错误 - 用
Task.Run代替new Thread处理异步连接,更符合.NET异步编程规范 - 增大握手请求缓冲区,避免长HTTP头被截断
内容的提问来源于stack exchange,提问作者Zend
相关产品推荐
相关产品推荐

