
简介基于Modbus TCP的C服务端实现面向工业自动化、设备联网及上位机通信开发者。项目将Modbus报文结构迁移到TCP/IP之上代码展示监听502端口、接收客户端连接、解析功能码与寄存器地址、执行读写操作并返回响应同时包含超时与错误码处理适合具备C基础、希望深入协议栈的读者。资源共14个文件主体为5个.h和5个.cpp源码另含vcproj工程文件、sln解决方案、md说明及txt备注压缩包大小仅13KB结构紧凑便于逐文件阅读。已有678人浏览学习源码规模不大适合作为学习Modbus TCP协议和C网络编程的入门范例。通过阅读项目可理解Modbus寄存器映射表如何组织、多客户端请求如何并发处理以及调试与日志模块如何辅助定位网络异常工程还提供完整构建配置方便在Visual Studio中直接打开快速体验服务端运行流程。 先说说我为什么要折腾这个modbus-TCP-server。做自动化项目或者上位机开发的人应该都有这种体会PLC程序写完了、HMI画面做好了、相机的触发逻辑也编好了结果现场设备还没装完或者生产线不允许你随便动真机联调一拖就是好几天。后来我养成了一个习惯——先在一台普通电脑上跑一个Modbus TCP服务器把要对接的寄存器区、线圈区全部虚拟出来让PLC、组态软件、工业相机先跟这个“假设备”通讯。所有逻辑调通了再去现场接真实设备基本一遍过。这篇内容就是围绕这个标题展开的。我会从Modbus TCP服务器的角色模型讲起然后给一份可以直接抄的pymodbus实现代码再拆开讲讲地址映射、字节序、异常响应这些最容易踩坑的点最后结合信捷PLC做服务器接海康相机、三菱FX5U做主站连自建服务器这两个热门场景说说实际联调时的思路。适合正在做PLC通讯、上位机开发、视觉联动项目的工程师也适合刚接触Modbus TCP、被“主站从站”“客户端服务器”这类概念绕晕的新手。1. Modbus TCP里“服务器”到底是个什么角色1.1 主从关系和客户端/服务器的对应很多从PLC转过来的工程师第一次听到“Modbus TCP服务器”都会愣一下PLC里不是一直叫主站从站吗怎么到了TCP又冒出个服务器其实说的是一回事只是换了套叫法。在原始的Modbus协议里角色分主站Master和从站Slave主站主动发起读写请求从站被动响应。到了Modbus TCP时代协议栈不再叫Master/Slave改叫客户端Client和服务器Server。服务器监听在502端口等着客户端来连客户端主动建立TCP连接然后发Modbus请求。所以Modbus TCP服务器传统意义的从站是提供数据、被读写的一方。搞清楚这层对应关系配置信捷PLC、三菱FX5U时就不会被菜单里的“客户端/服务器”选项搞晕。我自己调试时习惯用下面这个对应关系来理解概念传统叫法角色行为常见设备Modbus TCP Server从站被动等待连接监听502端口响应读写请求自建服务器、信捷PLC设为服务器模式Modbus TCP Client主站主动发起连接发送读写请求上位机组态软件、三菱FX5U做主站、海康相机视觉软件还要提一个经常被忽略的参数Unit ID单元标识符。Modbus RTU时代这个值是挂在总线上的从站地址比如1到247。到了TCP里它依然存在于MBAP头中但实际用途已经很弱通常填1或者255。如果你自建的服务器用的是singleTrue不管Unit ID是多少都返回同一份数据如果做多从站模拟就得靠它来区分不同设备。联调时如果PLC那边配了一个跟服务器端不匹配的Unit ID最常见的表现是TCP连接能建立但请求一直没响应或被拒。1.2 MBAP头 PDU的报文结构想真正把Modbus TCP服务器跑明白最好还是理解一下报文。Modbus TCP的报文结构是MBAP头加PDUMBAP是Modbus Application Protocol的缩写一共7个字节。事务处理标识符占2字节用来匹配请求和响应协议标识符占2字节Modbus固定填0后续字节数占2字节表示后面还有多少数据Unit ID占1字节。紧跟着的PDU才是功能码和数据区。TCP是基于字节流的协议不像串口一发一收那么天然隔离所以必须靠“长度”这个字段来切分报文边界。而事务标识符的存在保证了同一条TCP连接上可以并发发出多个请求响应返回时一比对事务ID就知道对应哪个请求。这点在写服务器端异步处理逻辑时特别重要。还有个从RTU转过来的工程师常问的问题Modbus RTU报文末尾有CRC校验Modbus TCP怎么没有因为TCP/IP协议栈自己就把传输可靠性管了底层有校验和重传Modbus TCP再套一层CRC属于浪费干脆去掉。如果你用Wireshark抓包看到的不是一串带CRC的完整帧而是TCP载荷里的MBAP加功能码这个区别很直观。2. 用pymodbus搭一个最小可用的modbus-TCP-server2.1 为什么选pymodbus而不是自己造轮子实现一个Modbus TCP服务器理论上你可以直接用socket监听502端口自己解析MBAP头、功能码自己维护寄存器数组。但实际写起来你就会发现光是处理功能码的边界条件就够写几百行还要考虑异常响应格式、并发连接、多从站模拟这些都是重复劳动而且自己写的协议栈还不一定严格符合规范。pymodbus是目前Python生态里最主流的Modbus库同时提供客户端和服务器端实现。服务器端可以创建ModbusSlaveContext来管理4类数据区一行代码就能启动TCP监听还自带日志功能配了串口模块后也能拿来跑RTU。我自己在Windows、Ubuntu、树莓派上都跑过跨平台很干净。这里有个坑要提醒pymodbus 3.x版本的API相比2.x改动很大网上大量教程还是老写法。你如果在2.x的代码里看到ModbusServerContext的构造方式跟新版不同先查一下自己装的版本别直接照抄。2.2 最小服务器代码与数据区定义下面这段代码是我常用的最小模板基于pymodbus 3.x启动一个监听502端口的Modbus TCP服务器保持寄存器提供100个地址初始值为0同时把线圈、离散输入、输入寄存器也各建了一小块。直接在机器上安装pymodbus后就能跑。from pymodbus.server import StartTcpServer from pymodbus.datastore import ModbusSlaveContext, ModbusSequentialDataBlock from pymodbus.datastore import ModbusServerContext def run_server(): # 保持寄存器0x0000 开始的 100 个寄存器初始值 0 hr_block ModbusSequentialDataBlock(0x0000, [0] * 100) # 线圈0x0000 开始的 10 个线圈初始值 False co_block ModbusSequentialDataBlock(0x0000, [False] * 10) # 离散输入0x0000 开始的 10 个点 di_block ModbusSequentialDataBlock(0x0000, [False] * 10) # 输入寄存器0x0000 开始的 50 个寄存器 ir_block ModbusSequentialDataBlock(0x0000, [0] * 50) store ModbusSlaveContext( didi_block, coco_block, hrhr_block, irir_block ) # singleTrue 表示所有 Unit ID 都共享同一份数据区 context ModbusServerContext(slavesstore, singleTrue) # 监听本机所有网卡的 502 端口 StartTcpServer(contextcontext, address(0.0.0.0, 502)) if __name__ __main__: run_server()这段代码里最重要的就是ModbusSlaveContext的4个数据块参数它们对应Modbus协议里的4张表di是离散输入只读co是线圈可读可写hr是保持寄存器可读可写ir是输入寄存器只读。实际项目里90%的联调都用保持寄存器就够了因为像D区、数据寄存器这种业务参数协议上都落在保持寄存器里。线圈通常用来映射M区或者中间继电器离散输入和输入寄存器用得相对少。启动时有个容易被忽略的环境问题502端口在Linux下默认1024以下端口需要root权限在Windows下会被防火墙拦截。开发阶段为了省事可以把端口改成5020这种高位端口但很多PLC和组态软件的从站端口不能自定义默认就是502。真接PLC的时候要么用管理员/root权限启动要么提前把防火墙入站规则里TCP 502加白。2.3 “又读又写”是数据区自带的不用额外写逻辑热词里有个“modbus tcp怎么实现又读又写”估计问这个问题的人是被“读”和“写”两个方向搞怕了以为要写两套逻辑。其实Modbus服务器天然就是可读可写的而且不需要你写任何额外代码。关键在功能码的划分保持寄存器既支持读功能码03也支持写单个寄存器功能码06、写多个寄存器功能码16线圈也一样既支持读功能码01也支持写单个线圈05、写多个线圈15。主站发来“读”指令服务器就从数据区把当前值返回主站发来“写”指令服务器就把新值写进数据区。同一个地址谁都可以读谁都可以写不存在“只能读不能写”或者“只能写不能读”的天然限制除非你在数据块实现里故意把写入接口屏蔽掉。如果你需要在写入时触发业务逻辑比如某个寄存器被写了1之后要让相机拍一张照那就不能只依赖ModbusSlaveContext的默认实现了而是要自定义DataBlock的setValues方法在写入动作发生时拦一道先执行业务代码再更新寄存器值。pymodbus允许你继承ModbusSequentialDataBlock并重写setValues这个点是进阶玩法等基础通讯通了再研究。3. 寄存器地址映射、字节序和异常响应的坑3.1 组态软件里的40001和协议地址0x0000差一个“连接建立了但读出来的数据全是0或者干脆报地址错误。”这是我被问到最多的问题十有八九是地址偏移搞错了。这里有个很绕人的地方Modbus TCP报文里实际传输的地址是从0开始的协议地址但你打开PLC编程软件、组态软件、HMI配置工具看到的却是一套“从1开始的人性化地址”。保持寄存器在软件里通常显示为40001、40002分别对应协议地址0x0000和0x0001线圈对应00001、00002离散输入对应10001、10002输入寄存器对应30001、30002。所以你在服务器代码里写ModbusSequentialDataBlock(0x0000, [0] * 100)组态软件那边要访问的第一个保持寄存器就是40001不是40000。如果配置工具里把起始地址写成了0实际上访问的可能是协议地址0xFFFF这通常是非法地址服务器端会回异常客户端就会报错。联调之前先把这张对应表写清楚能省掉一小时的抓狂时间。数据区功能码读/写组态软件地址示例协议地址示例线圈01 / 05、15000010x0000离散输入02只读100010x0000输入寄存器04只读300010x0000保持寄存器03 / 06、16400010x00003.2 32位整数和浮点数的字序问题Modbus协议里每个寄存器固定16位但工程上要传的数据动不动就是32位整数、32位浮点数。一个float必须拆到两个寄存器里于是字节序的问题就来了。Modbus协议规定寄存器内部是大端字节序也就是高字节在前低字节在后但它没有规定寄存器之间的排列顺序。于是不同PLC和不同上位机软件之间就分成了两派有的先放高16位就是高字在前对应顺序AB有的先放低16位是低字在前对应顺序BA。如果你服务器端按AB方式组了两个寄存器客户端按BA方式解析读出来的浮点数就是一个天文数字。举个例子浮点数1.5的十六进制是0x3FC00000。A方案下第一个寄存器存0x3FC0、第二个存0x0000B方案下则是第一个存0x0000、第二个存0x3FC0。你在组态软件里配置数据长度时通常会看到“高位在前”“低位在前”或“Big Endian”“Little Endian”之类的选项改一下重新读取数值就正常了。我在自建服务器的时候会把字序做成一个可配置参数方便不同品牌PLC对接时切换实测下来这个设计特别实用。3.3 请求越界的异常响应把服务器理解成一台“会拒绝的机器”也很重要。主站请求的起始地址加寄存器数量超出服务器定义的数据区范围时服务器不能静默吞掉而是要返回Modbus异常响应。最常见的异常码是0x02非法数据地址也就是告诉主站你要读的地址不在我的数据区里。pymodbus对越界请求的默认处理已经做好了一般不用你操心。但有一种特殊情况值得注意有些PLC组态软件在初始化时会自动用广播或连续的请求去扫描从站能力范围比如一次性读125个保持寄存器。如果你的服务器只定义了100个寄存器而这个扫描请求从地址100开始读125个就会直接越界。这时候客户端可能会报“通讯异常”但你的服务器数据区本身是好的。建议给数据区留一点余量比如实际用100个寄存器就分配128个或者在服务器日志里看一眼请求的起始地址和数量定位起来非常快。4. 对接真实设备信捷PLC、海康相机、三菱FX5U4.1 信捷PLC作为Modbus TCP服务器与海康相机通讯最近热词里有一条“配置信捷PLC作为modbus tcp服务器与海康相机进行通讯”这个场景我太熟了信捷PLC负责产线逻辑海康工业相机或视觉控制器需要从PLC获取触发信号和当前工件的工艺参数检测完再把结果和OK/NG信号写回PLC。两边不在同一个通讯主从模型上信捷PLC做成服务器非常合适。配置思路分三步。第一步在信捷PLC的编程软件里把以太网口IP设好比如192.168.1.10然后找到Modbus TCP相关配置把服务器功能打开。第二步建立软元件和Modbus地址的映射关系信捷的D区通常对应保持寄存器M区对应线圈具体映射范围要查对应型号的手册不同系列差异挺大。第三步在海康相机的视觉软件或SDK里配置Modbus TCP客户端目标IP填PLC的IP端口默认502再把需要读写的地址按映射关系填进去。联调时我一般会给相机单独配一个固定IP跟PLC在同一网段比如192.168.1.20。很多通讯问题都不是程序逻辑问题而是IP不在同一网段或者被路由器隔离了。如果相机软件里能看到“连接成功”但数据一直不对先别动相机用Modbus Poll这个小工具从PC上连接PLC逐个地址读一遍确认是PLC侧映射的问题还是相机侧配置的问题。4.2 三菱FX5U作为Modbus TCP主站连接自建服务器反过来拿三菱FX5U当主站去连我们自建的modbus-TCP-server也是联调利器。FX5U本体自带以太网口GX Works3里配置起来比较直观。新建工程后在“内置以太网端口”里设置IP然后找到智能设备模块或Modbus TCP相关的功能配置入口新建一个连接。目标IP填运行服务器的PC地址目标端口填502单元标识符要和服务器端匹配。我自己跑通这套链路时的顺序是先在PC上用Modbus Poll连自建服务器确认寄存器的读写都正常再把FX5U的客户端连过来。第一次启动时客户端报了“无响应”后来查了服务器日志发现三菱发过来的Unit ID填了255而我服务器端默认不处理255这个地址。把服务器改成singleTrue后问题直接消失。如果你用的是多从站模拟模式记得检查Unit ID的对应关系。三菱FX5U读取写入的指令在GX Works3里可以选用Modbus专用的MBWR、MBRD这类功能指令也可以直接配置成软元件联动。不同系列指令叫法有差异但配置入口基本都在这几个地方。如果你手头没有PLC只靠自建服务器和Modbus Poll也能把整个流程模拟出来这也是为什么我说这是联调利器。4.3 分层联调的方法论自建Modbus TCP服务器最大的价值是让联调变成可以分层的工程而不是一次拼运气。我的联调习惯永远是三层分开走。第一层服务器端自测启动modbus-TCP-server用Modbus Poll作为客户端把配置文档里每个寄存器地址都读一遍、写一遍确认数据区范围和初始值符合预期。第二层第三方接入让PLC或相机的工程师先把跟真实设备的通讯协议调通哪怕只是读一段测试数据也要把两边的IP、端口、Unit ID、功能码都锁定下来。第三层真实业务联调等前两层都干净了再上真机、走真实工艺流程。这套方法论救了我很多次。很多联调问题表面上看是“通讯库坏了”或者“设备不正常”实际上就是地址映射表有错、字节序配反了。用虚拟服务器把这两类问题提前筛掉现场只处理纯粹的IO逻辑和节拍问题效率高很多。5. 硬件接线和网络排障经验5.1 硬件电路不是RS485是以太网热词里有“modbus tcp硬件电路”估计是习惯了Modbus RTU的工程师找A/B线和终端电阻。这里必须说清楚Modbus TCP没有硬件电路这回事A/B线只存在于RS485物理层而Modbus TCP跑在以太网上物理层就是网线、网卡、交换机那一套。你要做的接线和网络规划其实很朴素。一端连PLC或自建服务器的网口另一端连相机/PC的网口中间没有交换机的时候现在网卡基本都支持自动翻转普通直通网线也能点对点直连。如果设备多了加一台工业交换机把PLC、相机、上位机都接到同一台交换机上然后规划好IP地址在同一个网段网关看情况填不跨网段的话可以留空。需要现场排查的时候电脑上先ping一下对方的IP能通不通比拿万用表量电压靠谱得多。5.2 防火墙、端口占用和多客户端连接自己电脑上跑Modbus TCP服务器最大的敌人是操作系统防火墙。Windows下首次运行Python监听502端口经常弹防火墙提示手滑点了取消客户端那边就是TCP连接超时。处理办法是去防火墙入站规则里手动加一条TCP 502的放行规则或者干脆开发时临时关掉防火墙联调完再打开。Linux下如果绑定502失败大概率是权限问题用sudo运行或者选一个大于1024的开发端口。502端口被占用也经常发生。排查时用netstat -ano | findstr :502Windows或ss -lntp | grep 502Linux看看是哪个进程占着端口。很多定时软件、工控软件会偷偷占502遇到“服务器起不来”先查这一步。另外要注意Modbus TCP允许一条服务器同时接入多个客户端连接pymodbus的默认实现支持多连接处理。但有的PLC客户端不会维持长连接而是每次读写都新建连接再断开。如果你在服务器日志里看到大量连接建立断开并且偶尔出现请求超时可以尝试减少客户端的请求频率或者检查交换机的端口协商是否正常。多数情况下直连测试能排除一大部分网络问题。5.3 常见症状排查表下面这张表是我这么多年调试Modbus TCP服务器过程中遇到频率最高的几类问题基本涵盖了从“连不上”到“数据乱”的常见情况。现象大概率原因处理办法TCP连接不上IP不通、端口被防火墙挡ping对方IPtelnet IP 502测试端口检查防火墙连接已建立但请求无响应Unit ID不匹配、服务器数据区异常查看服务器日志确认MBAP里的Unit ID连上但读到全0寄存器类型选错、地址偏移用Modbus Poll分别用03和04读确认起始地址读到的数值明显不对字节序/字序配置反了切换大端小端、高字低字选项偶尔超时或掉线网络质量、连接数限制、网线松动直连测试观察日志检查交换机指示灯写入后数据不变化写入了只读数据区确认目标地址是保持寄存器或线圈不是输入区排查时我总建议新手养成一个习惯遇到问题先看服务器日志再多用Modbus Poll这种工具做对照实验最后才去翻业务代码。Modbus TCP是极度成熟的协议出错的地方往往不在协议本身而在配置细节上。还有一个小技巧做完这个modbus-TCP-server之后把所有对接寄存器整理成一张Excel映射表包括寄存器地址、数据类型、读写权限、缩放系数、单位发给PLC工程师、上位机工程师、相机工程师各一份。先让大家都按这张表联调发现问题就在表上改版本而不是在各自代码里猜。我实际用下来这个习惯比任何调试工具都省时间也是我自己能从“通讯难调”的坑里跳出来的关键。本文还有配套的精品资源点击获取