1. 放着现成的Modbus不用为什么非要用socket从站先说一个我自己的项目经历。去年做一条产线的数据采集改造现场已经有十几台汇川EASY系列PLC在跑工艺上位机是MES那边统一开发的。按常规方案走Modbus TCP通信地址表能列出来但问题出在MES要的数据不是简单的寄存器读写——它要求PLC主动上报设备状态、报警事件并且每次上报的数据帧格式是MES自定义的头部有设备ID和帧序号尾部有CRC校验。Modbus TCP这种一问一答的主从模式完全应付不了这种“PLC需要主动往外推数据”的场景。这时候socket从站方案就顶上来了。所谓“socket做从站”翻译成大白话就是把PLC当成一个网络服务端通过以太网口监听一个端口上位机或者其他设备作为客户端主动连上来双方用TCP或UDP协议按照自定义的报文格式自由收发数据。它跟Modbus TCP这种标准协议最大的区别在于报文内容完全由你自己定义不受功能码和寄存器地址空间的约束。EASY系列PLC本身定位是小设备但它的以太网口能力并不弱做socket服务端完全够用。很多工程师有一个误解觉得做从站就是要跑Modbus服务器做主站才需要折腾socket。实际上不是这样。socket从站解决的是“PLC作为数据源主动组帧推送”的问题而Modbus TCP解决的是“上位机按地址逐一读取”的问题。两者面向的场景有交叉但socket的自由度要高得多。这篇文章适合三类人看一是项目上被非标协议卡住、需要PLC跟第三方设备直接做TCP通信的二是想彻底搞懂EASY系列网络通信底层逻辑不满足于只会拖指令块的三是在做socket从站时遇到像bind: only one usage of each socket address (protocol/network address/port)这种报错、不知道怎么排查的。后面这部分我会用一整章来讲因为这个坑我踩过一次之后彻底记住了。2. EASY系列做socket从站之前先搞清楚硬件与固件的边界2.1 EASY系列网络能力的底子汇川EASY系列是小型PLC家族不同型号的以太网口配置不完全一样。做方案规划前我建议你先确认三件事第一你的CPU本体是否自带以太网口。EASY系列里有些入门型号是没有网口的需要在左侧扩展或者用其他方式转接。自带网口的型号通常是一个RJ45口支持10/100M自适应。第二固件版本对socket指令的支持情况。socket通信在汇川的中大型PLC里非常成熟而在EASY系列里不同批次的固件对socket指令的支持有差异。最稳妥的做法是拿到设备后在编程软件里看“PLC信息”确认固件版本再去汇川官网查对应的指令手册。如果手头设备固件比较老建议先升级固件再开发不然有些指令块根本编译不过去。第三同一时间能建立的socket连接数上限。这个参数决定了你设计的系统能同时接几个客户端。我在EASY系列上实测作为服务器端时同时建立的TCP连接数并不是无限的一般根据型号不同是几个到十几个不等具体数值以对应型号手册为准。做方案时不要把连接数往高了估如果有10台上位机同时要连建议用UDP或者分组错峰。2.2 编程软件里需要提前确认的三件事用汇川的编程软件打开EASY系列工程后在写socket从站程序之前有三个地方必须确认好IP地址配置在PLC参数里的“以太网端口”设置静态IP。EASY系列一般默认是DHCP或者一个固定出厂IP工程应用里必须改成和现场网络同一网段的固定IP。改完之后要重新下载硬件配置并断电重启才生效这个顺序千万别记反。socket指令库是否可用在指令树里展开“通信指令”相关的库找到socket类指令有的版本叫MBSocket有的叫SocketServer名称以当前软件为准。如果看不到要在库管理里手动添加通信库。通信超时时间与端口号规划写程序前先想好端口号建议选1024以上的高位端口避开常用端口。同时确认PLC端的防火墙或路由器端口转发规则如果跨网段通信。表格列一下我通常做的规划项规划项推荐做法说明IP网段与上位机同网段如192.168.1.x避免跨网段广播风暴和路由问题端口号10000以上的自定义端口避开系统保留端口和常用端口协议选择默认TCP需要PLC主动推送时TCP更可靠连接数按手册最大值的70%设计预留冗余防止热连接占用报文格式帧头长度类型数据校验自定义格式必须有长度字段和校验这些规划看着简单但每一项目后面都对应了具体的socket行为。比如端口号如果用了系统保留端口bind的时候直接被操作系统拒绝后面讲踩坑的时候会细说。3. 从约定到代码socket从站的完整落地过程3.1 第一步把通信角色和报文格式定死写socket程序之前先别急着拖指令块先把通信契约定了。我的习惯是画一张报文格式表把帧头、设备ID、帧序号、命令字、数据长度、数据域、校验字节全部列出来。这一步在梯形图编程里比在高级语言里更重要因为PLC里的字节处理和数组操作比电脑上麻烦得多格式设计不好后面解析程序能写到怀疑人生。以我常用的一个从站上报帧为例字节偏移内容长度说明0帧头2字节固定为0xAA 0x552设备ID2字节标识设备4帧序号2字节循环递增用于查重6命令字2字节0x0001状态上报8数据长度2字节数据域字节数10数据域N字节工艺数据10NCRC校验2字节从帧头到数据域的CRC16这里有一个关键设计数据长度字段绝对要保留。因为socket收到的字节流是连续的没有标准协议帮你切帧只有靠“帧头长度”来切包。如果你只发固定长度的帧那确实可以不用长度字段但一旦数据域变成变长的没有长度字段根本没法解析。3.2 第二步socket从站程序的四段式骨架EASY系列里写socket从站程序不管指令名称怎么变逻辑骨架都一样。我用结构化文本的伪代码描述一下你对应到自己软件版本里的实际指令名即可// 第一步创建服务器套接字并绑定地址端口 // 这里对应的是“打开/创建”服务端Socket指令 // 参数一般包括协议类型(TCP)、端口号、连接资源ID bOpenResult : SocketServer_Open( enable : TRUE, protocol : TCP, port : 20000, ipAddr : 192.168.1.10, socketID : 1 ); // 第二步监听并接受客户端连接 // 只有客户端连上来之后收发数据才有意义 bListenResult : SocketServer_Listen( enable : bOpenResult, socketID : 1, timeout : 5000 ); // 第三步循环收发数据 // 收数据放入接收缓冲区同时把需要上报的数据发送出去 bRecvResult : SocketServer_Recv( enable : bListenResult, socketID : 1, recvBuffer : byRecvBuf, recvSize : 256, timeout : 1000 ); bSendResult : SocketServer_Send( enable : bRecvResult, socketID : 1, sendBuffer : bySendBuf, sendSize : nDataLen, timeout : 1000 ); // 第四步关闭连接释放资源 bCloseResult : SocketServer_Close( enable : bSendResult, socketID : 1 );这个骨架最核心的思路是所有的数据收发都通过一个socketID与远端关联。所以当你有多个客户端连接时不能只开一个socketID得给每个客户端分配独立的socketID。否则就会出现数据串线A客户端发来的数据被B客户端收到了。3.3 第三步编写客户端验证程序PLC端的服务端程序写完只是第一步必须有一个客户端来做联调。很多人没有合适的调试工具我推荐一个思路先用PC上的网络调试助手或者Python脚本模拟客户端验证报文格式和时序等PC端完全调通了再去对接真实的上位机。一个最简的Python客户端验证程序用来测试EASY作为服务端是否正常import socket import struct import time client socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect((192.168.1.10, 20000)) # 填PLC的IP和端口 # 构造一个请求帧帧头 0xAA55 设备ID 帧号 命令字 长度 数据 CRC frame bytearray() frame b\xAA\x55 frame struct.pack(H, 1) # 设备ID frame struct.pack(H, 1) # 帧序号 frame struct.pack(H, 0x0001) # 命令字 frame struct.pack(H, 2) # 数据长度 frame b\x01\x02 # 数据域 # CRC略 client.send(bytes(frame)) resp client.recv(1024) print(收到响应:, resp.hex()) client.close()用Python测试的好处是你能完全掌握发送的字节内容可以精准构造各种异常报文来测试PLC的容错能力。比如故意发一个长度字段和数据域不一致的包看看PLC端会不会被卡死。4. 踩坑实录bind报错的完整排查链路4.1 报错现场还原有次在现场联调上位机PC端是Python写的socket客户端运行时报了这么一段错OSError: [WinError 10048] bind: only one usage of each socket address (protocol/network address/port)这个错误从字面上理解是每个套接字地址协议/网络地址/端口只能被绑定一次而现在有重复绑定了。很多第一次遇到这个报错的人会懵以为是PLC端的程序不对其实问题大概率出在PC客户端这边。我用一个生活类比的解释帮大家理解一个酒店门牌号IP端口只能对应一个房间你开房时前台登记这个门牌号房间钥匙交给你。当你还占着这间房没退又跑去前台要求登记同一个门牌号前台就会拒绝——报错就是“这个门牌号已经被占用了”。4.2 三类根因逐一排查遇到这个报错先别急着改程序按下面顺序排查第一类本机上其他进程占了端口。在PC端命令行执行netstat -ano | findstr 20000看看20000端口被哪个PID占用然后在任务管理器里找到对应进程。很多时候是你不小心开了两个客户端实例第一个没关掉第二个再去bind同一个本地端口就报错了。第二类socket没有正常关闭进入TIME_WAIT状态。TCP连接关闭时主动关闭方会进入TIME_WAIT状态端口要保持一段时间Linux默认60秒Windows也类似。你上一次测试程序崩溃或者强退端口实际上还没完全释放立刻重启程序bind同一个端口就会撞上。解决办法是设置socket的SO_REUSEADDR选项client.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)或者连接时不要手动绑定端口让系统自动分配一个空闲端口用client.connect()而不是先bind()再connect()。第三类PLC端的socket没有释放。EASY系列服务端程序如果只是停止了运行或者下载了新程序而上次建立的连接尚未完全断开理论上一般不会阻塞其他客户端的connect但有些固件版本在异常情况下会出现连接资源耗尽、新的客户端连接不上的问题。遇到这种情况把PLC重新上电基本能恢复。下表是排查步骤的速查排查步骤命令/操作定位方向检查端口占用netstat -ano | findstr 20000本机端口被其他进程占用检查进程tasklist | findstr PID是否为残留的旧客户端进程查看连接状态netstat -an | findstr 20000TIME_WAIT状态说明端口未释放测试PLC端口telnet 192.168.1.10 20000检查PLC是否在监听重启PLC通信PLC断电重启释放异常连接资源抓包分析Wireshark过滤tcp.port20000查看握手过程是否正常4.3 我的修复经验与例行措施那次排查最终的根因是调试过程中开了一个带自动重连的客户端后台线程这个线程一直占着端口没退出我又手动开了一个新的客户端实例去bind同一个端口直接撞车。把后台线程彻底结束掉之后问题就消失了。经过这次教训我给自己定了两条规矩一是客户端调试程序一律不手动bind端口交给系统自动分配。只有服务端PLC才需要固定端口。二是PLC端程序里一定要有专门处理“上一连接未释放”的逻辑。也就是当收到close信号、通信异常超时时要立刻调用释放指令把这个socketID复位。同时程序启动时先尝试关闭之前可能残留的连接再开始listen。EASY系列里具体做法是在程序初始化段先对用的socketID调用一次close哪怕返回错误也无所谓继续往下执行Open逻辑。这样可以避免PLC重新上电后旧的连接状态还没被系统回收的情况。4.4 一个容易忽视的细节TCP是四元组标识既然说到bind就多说一嘴。TCP连接的标识是四元组源IP、源端口、目的IP、目的端口。也就是说同一个本地端口是可以被多个连接共用的只要远端地址不同就行。这跟UDP不一样UDP是二元组标识同一个本地端口只能被一个socket绑定。所以当PC端做客户端时有多个连接要出去不需要给每个连接分配不同的源端口。真正要小心的是像上面这种“服务端程序重复bind同一个端口”的场景。而你用EASY做socket从站本质是服务端服务端在监听端口时也要记住这个区别多个客户端连同一个监听端口服务端会为每个连接分配不同的连接资源底层会区分四元组不会串数据但前提是你在PLC程序里给每个连接分配了独立连接ID。5. 联调阶段最容易翻车的三个隐藏问题5.1 问用TCP但没做拆包粘包处理上位机发来连续的几帧数据时socket收数据是字节流不保证一次recv刚好是一帧。最常见的情况是对方发了两帧但你一次recv全收进来了或者对方发了一帧大的你分两次recv才收完。这就是TCP流式协议的粘包/拆包问题在PLC这种资源受限的设备上尤其容易出问题因为缓冲区有限、扫描周期又不固定。EASY系列自带的socket接收指令一般有最大长度限制超出部分会留在内核缓冲区等下一次接收。所以做拆包逻辑时必须在收到的字节流缓冲区里做“找帧头、读长度、截帧、剩余数据缓办”的循环处理。我见过最典型的翻车场景是PLC程序简单地从接收缓冲区取固定长度然后判断帧头、校验结果因为一次收了两帧导致判断失败后续所有数据全部错位整个通信瘫痪。5.2 问把重连逻辑交给了上位机PLC不做心跳还有一个高频问题上位机软件崩溃后重启重新连接PLC时发现连不上。多半原因就是PLC作为服务端长期没有收到对端数据却没有主动关闭死掉的连接。合理的设计是PLC端服务端做心跳超时监测。我的习惯做法是每个扫描周期检查一次最近一次接收数据的时间戳如果距离当前时间超过设定阈值比如15秒或30秒视业务而定就主动close这个连接释放连接资源重新回到listen状态。这样上位机无论怎么崩溃重启只要重新连接PLC这边都有空闲资源可以接受。5.3 问把PLC程序的扫描周期和通信实时性混为一谈EASY系列是小型PLC程序扫描周期一般在毫秒级到几十毫秒级取决于程序量和通信负载。socket收发指令是在扫描周期里执行的意味着实时性不会像中大型PLC的专用通信任务那么好。设计时要把心跳间隔、数据上报频率放在100ms以上量级不要试图做到跟Socket中断一样的毫秒级响应。如果你确实需要毫秒级甚至更快的响应那就要考虑用支持中断或专门通信任务的系列EASY这种小型设备更适合做秒级或百毫秒级的非实时上报。这个认知早点建立能帮你少走很多弯路。6. 把demo变成能交付的方案多客户端管理与业务分级6.1 为每个客户端分配独立性真实项目中PLC做socket从站往往不是只接一个上位机。我做过一个项目EASY系列PLC同时接了触摸屏、MES客户端和一台第三方视觉系统。三方都通过socket连到PLC上但数据需求完全不同。触摸屏要看状态MES要收报警视觉系统要下发OK/NG判定结果。一开始简单粗暴地用同一个socketID收发结果三个客户端的数据全混在一起切都切不开。后来改成多socketID方案每个客户端连接分配独立的socketID每个socketID对应独立的发送缓冲区、接收缓冲区和状态字。程序里用一系列socketID相关的状态位来判断谁连上了、谁上次通信是什么时候、最近一帧数据是谁发来的。这样业务就清楚多了。EASY系列在编程时这种多连接的管理建议用数组下标的思路。用变量wSocketIdx做循环对每个连接ID执行一遍Listen、Recv、Send、超时检查逻辑代码量会小很多而且不会漏掉某个连接。6.2 数据收发与业务逻辑分离这是一个非常实用的架构思想主程序里不要直接去处理socket收发而是分三层层级职责说明通信层负责socket的Listen/Recv/Send/Close和拆包只做字节搬运不解析业务协议层负责帧校验、帧类型识别、数据与全局变量的互相拷贝把原始字节翻译成结构化的PLC变量业务层根据协议层给的变量做工艺逻辑、报警判断只关心PLC变量不关心网络细节我早期做socket从站是直接在收发指令后面跟着一大堆业务判断梯形图写到后面根本没法维护改一个通信响应逻辑整页程序都要动。后来改成三层分离通信层只负责把收到的字节流放到一个全局数组里协议层把数组里的有效载荷解析到PLC变量业务层全都是普通逻辑清晰了很多。EASY系列的指令块通用性在这个架构下也体现出来了——换指令库版本甚至换PLC型号只要保留通信层的对接变量业务层的代码完全不用动。6.3 一个完整的小案例视觉系统对接举个具体例子。产线上视觉系统通过TCP连接EASY系列PLC视觉系统作为socket客户端PLC作为从站服务端。视觉系统每检测完一个产品主动上报一条结果报文格式如下帧头(0xAA 0x55) 产品编号(4字节) OK/NG(1字节) 检测项数量(2字节) 检测项列表(N*2字节) 时间戳(4字节) CRC16(2字节)PLC端收到后协议层把产品编号解析到dwProductID把OK/NG解析到bVerdict把检测项数量解析到wItemCount检测项列表放到数组awItemData[0..19]。业务层根据bVerdict决定是否放行下一产品同时把结果通过另一个socketID送给MES系统。这里最值得注意的一点是视觉系统一个扫描周期可能连续发几条结果PLC要是每条处理完才去Recv下一条很容易出现接收缓冲区溢出。因此我常常把接收缓冲区设大一些比如256字节甚至更大然后拆包循环里一次把缓冲区里所有的完整帧都解析出来而不是每扫描周期只处理一帧。这个细节直接影响丢包率。7. 我在EASY系列socket从站上积累的三条经验写到这里最后分享几个我在实际项目中坚持的做法供参考。第一EASY系列做socket从站能稳定运行但它毕竟不是一台通用计算机不要拿PC上socket网络编程的思路生搬硬套。PC上处理粘包拆包是一个独立的线程缓冲区几十KB甚至更大而PLC是一个扫描周期一个周期地轮询处理缓冲区也小得多。理解了这个差异之后很多问题都能解释通。第二通信程序一定要做fail-safe设计。我见过很多工程师觉得PLC端的TCP连接是“永远在线”的根本不处理断线情况。实际上网络电缆被老鼠咬断、交换机重启、上位机蓝屏这种事天天都有。通信程序里必须考虑收不到数据怎么办、发不出去怎么办、对端突然断开怎么办。我个人会在PLC程序里加两个全局变量wCommStatus和dwLastRecvTime把通信健康状态暴露给触摸屏或MES让现场操作员能看到“通信已断开”而不是默默无声地停产。第三做好日志。EASY系列的掉电保持区或者存储区可以记录最近N条通信异常事件包括异常代码和时间戳。当现场半夜打电话说通信故障时把日志拷出来一看就能判断是上位机发疯了还是PLC侧断开了。这套办法帮我处理了不少售后问题比远程一帧一帧抓包省事多了。EASY系列做socket从站这件事本身不复杂难点在于对通信细节的把握和对异常情况的预判。按照上面的步骤走下来从选型规划到程序落地再到现场稳定运行基本不会跑偏。如果你也在做类似的事欢迎交流你的踩坑经验。
阅读完成 · 觉得有帮助?