Modbus TCP这事儿,看着比RTU简单——不用算CRC,不用管3.5字符间隔,网线一插就能通。但真到现场跑起来,坑一点不比串口少。从PLC到DCS,从网关到边缘控制器,这些年用TCP跟各种设备打过架,今天把心得全倒出来。
先定个调:Modbus TCP不是把RTU报文塞进TCP包那么简单。虽然数据部分长得像,但前面多了个MBAP头,后面少了CRC校验,工作机制也完全不一样。如果拿RTU那套思维去套TCP,迟早栽跟头。
一、端口502是死规矩,别乱改
IANA分配的就是502,几乎所有支持Modbus TCP的设备都监听这个端口。但实际工程中,502经常被防火墙封死或者被别的服务占掉,这时候可以用非标端口,比如5020、5021。只是非标端口在组态软件里要手动填,没有默认选项那么方便。
还有一点要注意:一台设备可以同时监听多个端口,但标准502端口得留着,因为很多上位机扫描工具默认只扫502。你改了端口,上位机扫不到,现场调试的人第一反应是设备坏了。
二、MBAP报文头:7个字节,一个都不能少
这是Modbus TCP跟RTU最本质的区别。MBAP全称是Modbus Application Protocol,7个字节固定长度:
事务标识符(Transaction Identifier):2字节,主站自己维护,每发一个请求+1,从站回复时原样带回。这是用来匹配请求和响应的,尤其在多请求并发时特别重要。
协议标识符(Protocol Identifier):2字节,Modbus协议固定填0x0000。你要是填别的,有些设备直接不回。
长度域(Length):2字节,表示后续数据的总字节数,从单元标识符开始算到数据结束。这个值必须算准,算少了从站收不全,算多了TCP粘包时解析错位。
单元标识符(Unit Identifier):1字节,相当于RTU里的从站地址。但TCP里这玩意儿的意义变了——它用来区分网关后面的串口子设备。如果直接连的是纯TCP设备,填0xFF或者1都行,看设备要求。
MBAP头必须整明白,不然报文解析全是乱码。
三、报文对比:TCP少了个CRC
先看请求帧:
RTU:地址 + 功能码 + 起始地址 + 寄存器数量 + CRC
TCP:事务标识符 + 协议标识符 + 长度 + 单元标识符 + 功能码 + 起始地址 + 寄存器数量
看到了吧?功能码和数据部分跟RTU几乎一样,但地址挪到了单元标识符,CRC直接砍掉——因为TCP/IP协议栈自带校验和重传机制,Modbus层面再加CRC纯属冗余。
但有个细节容易忽略:TCP报文里没有帧边界标记。RTU靠3.5字符静默判断帧结束,TCP靠的是TCP协议栈的流式传输。接收端得靠MBAP里的长度域来截取完整一帧。很多人按RTU那套字节超时来收TCP数据,结果粘包了都不知道。
四、并发与事务标识符:这地方最容易翻车
串口是半双工,一发一收,主站发完等回复,总线上同一时间只有一个请求,天然串行。
但TCP是全双工,以太网可以同时跑多个请求。上位机可以连续发多个请求出去,不用等前一个回复再发下一个。这时候怎么区分哪个回复对应哪个请求?
事务标识符就是干这个的。
主站发请求1,事务ID填0x0001;发请求2,填0x0002。从站回复时把对应的事务ID原样带回来。主站收到回复,看事务ID就知道是回哪个请求的。
这个机制听起来简单,但实现时有个坑:事务ID的维护必须原子操作。多线程环境下,两个线程同时发请求,事务ID自增出现竞争,结果两个请求用了同一个ID,回复回来全乱套。这事我亲眼见过——一个国产网关,固件里事务ID没加锁,上位机并发一高就频繁超时,抓包一看事务ID重复的报文一堆。
正确做法是加互斥锁,或者用原子自增指令。如果系统不支持,就老老实实用单线程轮询,别搞并发。
五、连接管理:保持长连接还是短连接?
TCP是面向连接的,建链需要三次握手,拆链需要四次挥手。工业现场有个共识:长连接优先。
每个请求都重新建链,开销大不说,502端口连接数有限,频繁建拆链容易把从站的TCP栈搞崩溃。我见过一个老款PLC,连接数上限只有8个,上位机每扫一次建一个新连接不释放,扫到第9次设备直接拒绝服务。
标准做法:建立连接后保持住,心跳包定期维持。Modbus没有专门的心跳机制,但可以每隔几秒发一个读线圈的请求(比如读0地址),设备有回复就证明链路还在。
断线重连也得有策略:检测到TCP连接断开后,延时1~2秒重连,别马上重试,否则从站还没来得及释放端口,新的连接又被拒绝。
六、粘包与拆包:TCP流式传输的老问题
Modbus TCP跑在TCP之上,而TCP是流式协议,没有消息边界。如果你连续发两帧,接收端有可能一次收到两帧粘在一起,也可能一帧被拆成两次收。
解决思路只有一个:靠MBAP头里的长度域拆帧。
标准接收流程:
收到数据先攒到缓冲区。
检查缓冲区字节数,够不够7个(MBAP头固定长度)。
够了就解析头,读第5-6字节的长度域。
检查缓冲区总长度是否 >= 7 + 长度域的值。
够就截取一完整帧,从缓冲区移除,继续处理下一帧。
不够就继续收,等下一包数据。
千万别用RTU那套定时器超时判断帧结束,TCP上这么干不稳,网络抖动时误报率高得一塌糊涂。
七、响应超时:跟RTU完全两码事
RTU超时设200ms~500ms就够了,但TCP场景完全不一样。
局域网内:ping值<1ms,从站响应通常几十毫秒,超时设200ms绰绰有余。
跨网段或者走4G/5G:延迟可能上百毫秒甚至更高。这时候超时得放开到1~3秒,不然频繁超时重试,反而让本就拥堵的网络雪上加霜。
还有个容易被忽略的点:TCP本身的超时。connect()系统调用默认超时时间可能很长(几十秒到几分钟),代码里必须主动设置socket超时,否则连接一个不存在的IP时程序直接卡死。setsockopt设SO_RCVTIMEO和SO_SNDTIMEO,这是基本功。
八、防火墙与NAT:现场常踩的坑
很多工业现场,Modbus TCP设备部署在子网里,上位机在外网通过VPN或者端口映射访问。
这时候有几个常见问题:
端口映射:公网IP的某个端口映射到内网设备的502。但有些设备在回复时会校验目的IP和端口,如果映射前后端口不一致,设备直接丢包。解决办法是保持内外端口一致,或者用代理模式转发。
防火墙的TCP状态跟踪:有些工业防火墙开启了状态检测,长连接如果长时间没有数据交互,防火墙会认为连接已超时而主动清掉会话表。这时候应用层没感知,等到下一个请求发出去就石沉大海。解决办法是在应用层加心跳,或者关掉防火墙的状态检测功能。
NAT穿透:如果设备在NAT后面主动向上位机建立连接(反向连接),设备端的MBAP事务ID和源端口都得处理好,不然上位机回复时路由不回去。
九、性能:一次读多点,比多次读一点强得多
Modbus TCP本身没有限制单帧数据量,但受TCP最大报文段长度(MSS)和从站缓冲区限制,实际单帧能读的最大寄存器数量通常在125个左右(跟RTU一样)。
性能优化的黄金法则:批量读写。
需要读20个连续寄存器?别发20次读单个寄存器的请求,一次把20个全读回来。帧数量从20降到1,网络开销直接少两个数量级。
写操作也一样,功能码0x10可以一次写多个寄存器。但要注意:有些设备对多寄存器写支持得不好,要么返回异常码,要么只写了前几个。这种情况没辙,只能退回到单个写。
十、安全:502端口暴露在公网就是找死
Modbus TCP没有任何安全机制,没有加密没有认证没有授权。谁连上502端口都能读寄存器,都能写线圈。
千万千万别把502端口直接暴露在公网上。这不是危言耸听,Shodan上一搜一堆暴露的Modbus设备,被人恶意写个停机指令,生产线直接趴窝。
正确做法:
局域网隔离,Modbus设备放在独立的工业网段,不跟办公网混在一起。
远程访问走VPN或者专用的工业安全网关,做协议深度检测。
如果真的需要公网访问,前面加一个Modbus/TCP转MQTT或者OPC UA的网关,把协议层隔断。
十一、最后说个真事儿
前年帮一个光伏电站调系统,逆变器厂家提供的Modbus TCP接口,文档里写着支持10个并发连接。结果并发一超过5个,逆变器就开始丢包,事务ID对不上,频繁超时。
折腾了两天才搞明白:厂家说的"支持10个连接"指的是TCP建链能建10条,但协议栈处理能力只能同时处理3~4个请求。最后改成单线程轮询,周期拉长到500ms,问题解决。
这事儿给我的教训是:厂商标称的参数是理想值,实际性能打折是常态。设计系统时预留余量,别跑在极限边缘。
Modbus TCP技术层面就这些。协议本身简单,但工程应用里牵涉到网络、并发、超时、防火墙,事儿就多了。拿Wireshark抓个包,看事务ID怎么跳、长度域怎么填,比看一百页手册都管用。
如果需要,我还能单独写一篇Modbus TCP与RTU的网关转换,那个坑更多,尤其是时序映射和异常码转换,全是经验。