2026/3/29 15:39:03
网站建设
项目流程
律师事务所 网站建设,温州专业网站建设推广,做动画合成的视频网站,a4网站建设您遇到的问题是非常典型的 TCP 粘包/拆包问题。让我详细解释原因和解决方案#xff1a;
问题原因分析 TCP 是流式协议#xff0c;不是消息协议 TCP 把数据看作连续的字节流#xff0c;没有消息边界 发送方发送#xff1a;报文1 报文2 接收方可…您遇到的问题是非常典型的TCP 粘包/拆包问题。让我详细解释原因和解决方案问题原因分析TCP 是流式协议不是消息协议TCP 把数据看作连续的字节流没有消息边界发送方发送报文1 报文2接收方可能收到报文1报文2粘包或 报 文1拆包您的代码问题分析// 您的原始 TCP 代码可能类似这样 socket.on(data, (data) { // 问题这里可能收到不完整的报文或多个报文合并 console.log(收到数据:, data); // 直接发送响应 const response messageFactory(type, deviceAddress, data); socket.write(response); // 如果前一次响应还没发完就会和这次合并 });缓冲区问题Node.js 的socket.write()是异步非阻塞的快速连续调用write()会导致数据在缓冲区合并操作系统 TCP 缓冲区也会合并小数据包Nagle 算法Socket.IO 为什么没有这个问题Socket.IO 在底层做了这些工作消息帧封装// Socket.IO 的消息格式 { type: 2, // 消息类型 data: [...], // 实际数据 id: 123, // 消息ID nsp: / // 命名空间 }自动分包/组包添加消息长度前缀添加消息边界标识保证消息完整性消息队列管理有序发送避免并发写入冲突流量控制避免缓冲区溢出TCP 解决方案对比方案原理优缺点长度前缀法在消息前添加长度字段最可靠但需要解析分隔符法用特殊字符分隔消息简单但分隔符不能出现在数据中固定长度法所有消息等长简单但不够灵活Socket.IO完整的消息协议功能强大但依赖特定库为您的 Modbus 应用实现可靠 TCP 通信方案1基于 Modbus TCP 协议标准const net require(net); // Modbus TCP 报文头事务标识(2) 协议标识(2) 长度(2) 单元标识(1) const MBAP_HEADER_LENGTH 7; class ModbusTCPServer { constructor() { this.buffers new Map(); // 存储每个连接的缓冲区 } createServer() { return net.createServer((socket) { const connectionId ${socket.remoteAddress}:${socket.remotePort}; this.buffers.set(connectionId, Buffer.alloc(0)); socket.on(data, (chunk) { this.handleData(socket, connectionId, chunk); }); socket.on(close, () { this.buffers.delete(connectionId); }); }); } handleData(socket, connectionId, chunk) { // 1. 追加到缓冲区 let buffer Buffer.concat([this.buffers.get(connectionId), chunk]); this.buffers.set(connectionId, buffer); // 2. 循环解析完整报文 while (buffer.length MBAP_HEADER_LENGTH) { // 读取报文长度第5-6字节 const length buffer.readUInt16BE(4); const fullMessageLength MBAP_HEADER_LENGTH length; // 3. 检查是否收到完整报文 if (buffer.length fullMessageLength) { // 提取完整报文 const message buffer.slice(0, fullMessageLength); // 剩余数据保留在缓冲区 buffer buffer.slice(fullMessageLength); this.buffers.set(connectionId, buffer); // 4. 处理报文 this.processMessage(socket, message); } else { // 等待更多数据 break; } } } processMessage(socket, message) { // 这里处理 Modbus 报文逻辑 console.log(收到完整报文:, message); // 发送响应确保一次只处理一个请求 const response this.createResponse(message); this.sendResponse(socket, response); } sendResponse(socket, response) { // 确保发送完整性 return new Promise((resolve, reject) { socket.write(response, (err) { if (err) reject(err); else resolve(); }); }); } }方案2请求-响应队列避免并发写入class TcpConnection { constructor(socket) { this.socket socket; this.sendQueue []; this.isSending false; this.buffer Buffer.alloc(0); } // 接收数据处理防粘包 onData(chunk) { this.buffer Buffer.concat([this.buffer, chunk]); // 基于 Modbus TCP 长度字段解析 while (this.buffer.length 7) { // MBAP 头长度 const pduLength this.buffer.readUInt16BE(4); const frameLength 6 pduLength; if (this.buffer.length frameLength) { const frame this.buffer.slice(0, frameLength); this.buffer this.buffer.slice(frameLength); this.handleFrame(frame); } else { break; // 等待更多数据 } } } // 发送数据防并发写入 send(data) { return new Promise((resolve, reject) { this.sendQueue.push({ data, resolve, reject }); this.processQueue(); }); } async processQueue() { if (this.isSending || this.sendQueue.length 0) return; this.isSending true; const { data, resolve, reject } this.sendQueue.shift(); try { // 使用 drain 事件确保缓冲区清空 if (!this.socket.write(data)) { await new Promise(resolve this.socket.once(drain, resolve)); } resolve(); } catch (err) { reject(err); } finally { this.isSending false; this.processQueue(); } } }方案3使用现有库推荐const Modbus require(jsmodbus); const net require(net); // 使用成熟的 Modbus 库处理协议细节 const server net.createServer((socket) { const modbusServer new Modbus.server.TCP(socket, { // 设备寄存器 0: [0, 0, 0, 0], // 保持寄存器 1: [1, 1, 1, 1] // 输入寄存器 }); // 自动处理粘包拆包问题 modbusServer.on(readCoils, (request) { // 处理请求 }); }); server.listen(502, () { console.log(Modbus TCP 服务器启动); });总结Socket.IO 解决了 TCP 粘包问题是因为它实现了完整的消息协议如果要支持原始 TCP必须自己处理消息边界推荐使用长度前缀法这是最可靠的方式避免并发写入使用队列控制发送顺序