为何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
修复方案
核心思路
- 为每个客户端连接维护帧状态,记录当前分片的累积数据、opcode、FIN状态等
- 按照WebSocket协议规范逐字节解析
data块,拆分出完整帧 - 对客户端发送的帧进行掩码解码
修复后的核心代码
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
相关产品推荐
相关产品推荐

