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

为何Net.socket接收分片WebSocket帧时数据异常?

WebSocket分片帧解析异常问题

我手动用Node.js的net模块实现了WebSocket服务端,浏览器客户端发送小文本消息时能正常接收解析,但发送大文件触发帧分片后,仅第一帧能正常处理,后续帧要么乱码要么格式错误。通过Wireshark抓包确认对应文件会发送19个WebSocket包,服务端也输出19次日志,但只有初始hello帧和首个二进制帧正常。

服务端代码(Node.js)

const server = net.createServer((c) => {
    console.log('client connected');
    c.on('data',(data) => {
        let parsedData = parseHTML(data)
        // check if incoming data is an http request
        if(typeof parsedData != "boolean" && parsedData.type == "GET" ){
            let clientKey = parsedData.headers["Sec-WebSocket-Key"]
            // create an http response
            let response = constructWebSocketHttpResponse(clientKey)
            c.write(Buffer.from(response,"utf-8"))
        }
        else{
            // print out websocket data
            console.log(data)
        }
        })
    })

  server.listen(8124, () => {
    console.log('server bound');
  });

客户端代码(浏览器JS)

let ws = new WebSocket("ws://192.168.1.79:8124");

ws.addEventListener('open', (event) => {
    console.log("sending hello")
    ws.send("hello")
});

const fileInput = <HTMLInputElement>document.getElementById("file")
const send = document.getElementById("send")

send!.addEventListener("click",() => {
    if(fileInput.files)
        ws.send(fileInput.files[0])
})

问题根源

你直接将net模块data事件中的Buffer当作完整WebSocket帧处理,但TCP是流式协议,data事件触发时传递的Buffer可能只是单帧的一部分,或是多帧拼接的结果。针对分片传输的大文件,后续分片帧可能被拆分成多个data块接收,或与其他帧混在一起,直接解析必然出错。

另外,WebSocket协议有明确的帧格式规范:

  • 分片帧需要跟踪FIN标志位判断是否为最后一片
  • 客户端发送的帧都带掩码,必须解码才能得到原始数据
  • 分片帧的首个帧携带真实opcode(文本/二进制),后续分片opcode为0x0

修复方案

核心思路

  1. 为每个客户端连接维护帧状态,记录当前分片的累积数据、opcode、FIN状态等
  2. 按照WebSocket协议规范逐字节解析data块,拆分出完整帧
  3. 对客户端发送的帧进行掩码解码

修复后的核心代码

const net = require('net');

const server = net.createServer((c) => {
    console.log('client connected');
    // 维护单个连接的帧状态
    let frameState = {
        opcode: null,
        payloadChunks: [],
        isFinal: false,
        mask: null,
        remainingPayloadLength: 0
    };

    c.on('data', (data) => {
        let offset = 0;
        while (offset < data.length) {
            // 初始化新帧(或分片首帧)
            if (frameState.opcode === null) {
                // 解析第一个字节:FIN位 + opcode
                const firstByte = data.readUInt8(offset);
                frameState.isFinal = (firstByte & 0x80) !== 0;
                frameState.opcode = firstByte & 0x0F;
                offset++;

                // 解析第二个字节:掩码位 + 载荷长度
                const secondByte = data.readUInt8(offset);
                const isMasked = (secondByte & 0x80) !== 0;
                let payloadLength = secondByte & 0x7F;
                offset++;

                // 处理不同长度的载荷
                if (payloadLength === 126) {
                    payloadLength = data.readUInt16BE(offset);
                    offset += 2;
                } else if (payloadLength === 127) {
                    // 大长度处理,这里简化为32位(实际是64位)
                    payloadLength = data.readUInt32BE(offset);
                    offset += 8;
                }
                frameState.remainingPayloadLength = payloadLength;

                // 解析掩码
                if (isMasked) {
                    frameState.mask = data.slice(offset, offset + 4);
                    offset += 4;
                }
            }

            // 提取当前data块中的可用载荷
            const takeLength = Math.min(frameState.remainingPayloadLength, data.length - offset);
            const chunk = data.slice(offset, offset + takeLength);
            offset += takeLength;
            frameState.remainingPayloadLength -= takeLength;

            // 掩码解码(仅客户端发送的帧需要)
            if (frameState.mask) {
                for (let i = 0; i < chunk.length; i++) {
                    chunk[i] ^= frameState.mask[i % 4];
                }
            }

            frameState.payloadChunks.push(chunk);

            // 当前帧(或分片)接收完成
            if (frameState.remainingPayloadLength === 0) {
                if (frameState.isFinal) {
                    // 拼接完整载荷并处理
                    const fullPayload = Buffer.concat(frameState.payloadChunks);
                    switch (frameState.opcode) {
                        case 0x1: // 文本帧
                            console.log('接收文本:', fullPayload.toString('utf8'));
                            break;
                        case 0x2: // 二进制帧
                            console.log('接收二进制数据,长度:', fullPayload.length);
                            // 这里可将数据写入文件或做其他处理
                            break;
                        case 0x0: // 分片延续帧
                            // 需结合之前的帧状态处理,此处简化为直接输出
                            console.log('分片延续数据,长度:', fullPayload.length);
                            break;
                    }
                    // 重置状态,准备接收下一帧
                    frameState = {
                        opcode: null,
                        payloadChunks: [],
                        isFinal: false,
                        mask: null,
                        remainingPayloadLength: 0
                    };
                } else {
                    // 非最终分片,继续等待后续数据
                    // 注意:分片的后续帧opcode为0x0,需保持opcode不变
                    frameState.opcode = 0x0;
                }
            }
        }
    });
});

server.listen(8124, () => {
    console.log('server bound');
});

关键注意事项

  • 永远不要假设data事件传递的是完整WebSocket帧,必须按协议格式逐字节解析
  • 分片帧的首个帧携带真实opcode,后续分片opcode固定为0x0,需保留初始opcode直到FIN位为1
  • 客户端发送的所有帧都带掩码,必须解码后才能得到正确内容,否则必然出现乱码

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 06:27:51