news 2026/3/15 14:43:13

TCP 报文错乱的根本原因

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TCP 报文错乱的根本原因

您遇到的问题是非常典型的TCP 粘包/拆包问题。让我详细解释原因和解决方案:

问题原因分析

  1. TCP 是流式协议,不是消息协议

    • TCP 把数据看作连续的字节流,没有消息边界

    • 发送方发送:"报文1" + "报文2"

    • 接收方可能收到:"报文1报文2"(粘包)或 "报" + "文1"(拆包)

  2. 您的代码问题分析

// 您的原始 TCP 代码可能类似这样: socket.on('data', (data) => { // 问题:这里可能收到不完整的报文或多个报文合并 console.log('收到数据:', data); // 直接发送响应 const response = messageFactory(type, deviceAddress, data); socket.write(response); // 如果前一次响应还没发完,就会和这次合并 });
  1. 缓冲区问题

    • Node.js 的socket.write()异步非阻塞

    • 快速连续调用write()会导致数据在缓冲区合并

    • 操作系统 TCP 缓冲区也会合并小数据包(Nagle 算法)

Socket.IO 为什么没有这个问题?

Socket.IO 在底层做了这些工作:

  1. 消息帧封装

    // Socket.IO 的消息格式 { type: 2, // 消息类型 data: [...], // 实际数据 id: 123, // 消息ID nsp: '/' // 命名空间 }
  2. 自动分包/组包

    • 添加消息长度前缀

    • 添加消息边界标识

    • 保证消息完整性

  3. 消息队列管理

    • 有序发送,避免并发写入冲突

    • 流量控制,避免缓冲区溢出

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 服务器启动'); });

总结

  1. Socket.IO 解决了 TCP 粘包问题是因为它实现了完整的消息协议

  2. 如果要支持原始 TCP,必须自己处理消息边界

  3. 推荐使用长度前缀法,这是最可靠的方式

  4. 避免并发写入,使用队列控制发送顺序

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/13 10:37:38

SpringBoot+Vue 网站平台完整项目源码+SQL脚本+接口文档【Java Web毕设】

💡实话实说:CSDN上做毕设辅导的都是专业技术服务,大家都要生活,这个很正常。我和其他人不同的是,我有自己的项目库存,不需要找别人拿货再加价,所以能给到超低价格。摘要 随着互联网技术的快速发…

作者头像 李华
网站建设 2026/3/14 4:44:15

SpringBoot+Vue 大创管理系统管理平台源码【适合毕设/课设/学习】Java+MySQL

摘要 在当今信息化快速发展的时代,高校大学生创新创业项目(大创)的管理需求日益增长,传统的人工管理方式效率低下且容易出错。大创项目涉及学生、导师、项目申报、评审、经费管理等多个环节,亟需一套高效、智能的管理系…

作者头像 李华
网站建设 2026/3/13 10:08:46

企业级车辆管理系统管理系统源码|SpringBoot+Vue+MyBatis架构+MySQL数据库【完整版】

摘要 随着城市化进程加快和物流行业迅猛发展,企业对车辆管理的需求日益增长。传统的人工管理方式效率低下,难以应对复杂的车辆调度、维护和数据分析需求。企业亟需一套高效、智能的车辆管理系统,以实现车辆信息的实时监控、资源优化配置和成本…

作者头像 李华
网站建设 2026/3/13 16:20:35

零基础学习proteus8.16下载安装教程的完整步骤

从零开始搭建电路仿真环境:手把手教你安装 Proteus 8.16 你是不是也曾在搜索框里输入“ proteus8.16下载安装教程 ”,结果跳出来一堆广告、病毒链接和残缺不全的步骤?明明只想安安心心学个单片机仿真,怎么连软件都装不上&#…

作者头像 李华
网站建设 2026/3/13 19:25:41

电路设计入门准备:Multisim14.3环境搭建手把手教程

从零搭建电路仿真环境:手把手带你装好 Multisim 14.3 你是不是也遇到过这种情况——老师刚布置了一个放大电路作业,要求仿真验证波形,结果一查才发现自己电脑上连个像样的电路仿真工具都没有?或者好不容易下载了 Multisim&#xf…

作者头像 李华
网站建设 2026/3/13 9:48:06

ModbusTCP报文格式说明与Wireshark抓包对照详解

深入 Modbus TCP 报文结构:从协议解析到 Wireshark 实战抓包对照在工业自动化现场,你是否曾遇到过这样的场景?PLC 数据突然不更新了,HMI 显示异常,而 SCADA 系统报“通信超时”。排查了一圈硬件、网线、IP 地址&#x…

作者头像 李华