You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

不同浏览器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;
        }
    }
}

额外优化说明

  1. 调整HttpInfo类的初始化方式,用字符串解析HTTP头,避免字节数组截断导致的解析错误
  2. 用Task.Run代替new Thread处理异步连接,更符合.NET异步编程规范
  3. 增大握手请求缓冲区,避免长HTTP头被截断

内容的提问来源于stack exchange,提问作者Zend

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 11:42:35