☰
EASY系列PLC Socket从站实战:自定义报文与通信踩坑指南
2026/10/3 5:46:30 网站建设 项目流程

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从站程序之前,有三个地方必须确认好:

  1. IP地址配置:在PLC参数里的“以太网端口”设置静态IP。EASY系列一般默认是DHCP或者一个固定出厂IP,工程应用里必须改成和现场网络同一网段的固定IP。改完之后要重新下载硬件配置并断电重启才生效,这个顺序千万别记反。

  2. socket指令库是否可用:在指令树里展开“通信指令”相关的库,找到socket类指令(有的版本叫MBSocket,有的叫SocketServer,名称以当前软件为准)。如果看不到,要在库管理里手动添加通信库。

  3. 通信超时时间与端口号规划:写程序前先想好端口号,建议选1024以上的高位端口,避开常用端口。同时确认PLC端的防火墙或路由器端口转发规则(如果跨网段通信)。

表格列一下我通常做的规划项:

规划项推荐做法说明
IP网段与上位机同网段,如192.168.1.x避免跨网段广播风暴和路由问题
端口号10000以上的自定义端口避开系统保留端口和常用端口
协议选择默认TCP需要PLC主动推送时TCP更可靠
连接数按手册最大值的70%设计预留冗余,防止热连接占用
报文格式帧头+长度+类型+数据+校验自定义格式必须有长度字段和校验

这些规划看着简单,但每一项目后面都对应了具体的socket行为。比如端口号,如果用了系统保留端口,bind的时候直接被操作系统拒绝,后面讲踩坑的时候会细说。

3. 从约定到代码:socket从站的完整落地过程

3.1 第一步:把通信角色和报文格式定死

写socket程序之前,先别急着拖指令块,先把通信契约定了。我的习惯是画一张报文格式表,把帧头、设备ID、帧序号、命令字、数据长度、数据域、校验字节全部列出来。这一步在梯形图编程里比在高级语言里更重要,因为PLC里的字节处理和数组操作比电脑上麻烦得多,格式设计不好,后面解析程序能写到怀疑人生。

以我常用的一个从站上报帧为例:

字节偏移内容长度说明
0帧头2字节固定为0xAA 0x55
2设备ID2字节标识设备
4帧序号2字节循环递增,用于查重
6命令字2字节0x0001=状态上报
8数据长度2字节数据域字节数
10数据域N字节工艺数据
10+NCRC校验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.port==20000查看握手过程是否正常

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从站这件事,本身不复杂,难点在于对通信细节的把握和对异常情况的预判。按照上面的步骤走下来,从选型规划到程序落地再到现场稳定运行,基本不会跑偏。如果你也在做类似的事,欢迎交流你的踩坑经验。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询