Skip to content

工业通信协议

工业通信协议(Industrial Communication Protocol)是工业 AI 系统的数据采集层——它定义了上位机(host PC)与 PLC、传感器、变频器等现场设备之间”如何说话”的规则。如果把工业 AI 比作一个人,那么视觉算法是眼睛、控制逻辑是大脑,而通信协议就是连接它们的神经系统。本页从 TCP/IP 底层原理出发,逐步覆盖 Socket 编程、Modbus TCP、OPC UA、MQTT 以及 PLC 专有协议,帮你打通”从设备到 AI 模型”的数据链路。

本页是 工业 AI 系统 的前置知识,同时与 数据库与中间件 中的时序数据库、消息队列章节紧密衔接——采集到的数据最终需要落盘存储和流式处理。

理解工业通信,只需抓住三个层次:

层次核心问题代表协议
应用层读什么寄存器?数据格式是什么?Modbus、OPC UA、MQTT
传输层数据怎么可靠送达?TCP/IP
物理层信号怎么在线缆上跑?RS-485、工业以太网

一句话直觉:应用层协议解决”读什么”,TCP 解决”怎么送达”,物理层解决”怎么跑线”。工业通信的复杂性主要在应用层。


TCP(Transmission Control Protocol,传输控制协议)是几乎所有工业以太网协议的底层载体。理解 TCP 的行为,是调试工业通信问题的基本功。

TCP 是面向连接的协议——数据传输前必须先建立连接,传输结束后要优雅断开。

三次握手(Three-Way Handshake) 建立连接的过程:

客户端 服务端
| ──── SYN (seq=x) ────────→ |
| ←── SYN-ACK (seq=y, ack=x+1) |
| ──── ACK (ack=y+1) ──────→ |
| 连接建立 ✓ |
  • SYN(Synchronize,同步):客户端发起连接请求,携带初始序列号 x。
  • SYN-ACK(Synchronize-Acknowledge):服务端确认并附上自己的初始序列号 y。
  • ACK(Acknowledge,确认):客户端最终确认,连接进入 ESTABLISHED 状态。

四次挥手(Four-Way Handshake) 断开连接:

客户端 服务端
| ──── FIN ────────────────→ |
| ←── ACK ─────────────────── |
| ←── FIN ─────────────────── |
| ──── ACK ────────────────→ |
| 连接关闭 ✓ |

TCP 是全双工(full-duplex,即双方可同时收发)的,所以每个方向的关闭需要单独完成 FIN→ACK,共四个报文。

TCP 是流式协议(stream protocol)——它没有”消息边界”的概念。发送方调用两次 send() 各发 100 字节,接收方可能一次性 recv() 收到 200 字节;反之,一次 send() 发 2000 字节也可能被拆成两个包到达。这就是经典的粘包/拆包问题。

工业通信中解决此问题的常见方案:

方案原理代表协议
固定长度每条消息恰好 N 字节部分串行协议
长度前缀(length prefix)消息头前几个字节标注后续负载长度Modbus TCP(MBAP header 含 length 字段)
分隔符(delimiter)用特殊字节序列标记消息结尾HTTP(\r\n\r\n)、MQTT(剩余长度编码)

关键理解:“粘包”不是 TCP 的 bug,而是流式协议的固有特性。应用层协议必须自行定义消息边界。

import struct
def send_message(sock, payload: bytes):
"""用 4 字节大端长度前缀封装消息,解决 TCP 粘包问题。"""
length_prefix = struct.pack('>I', len(payload)) # 4-byte big-endian
sock.sendall(length_prefix + payload)
def recv_message(sock) -> bytes:
"""先读 4 字节长度,再按长度读取完整消息。"""
header = _recv_exactly(sock, 4) # 必须收够 4 字节
(length,) = struct.unpack('>I', header)
return _recv_exactly(sock, length)
def _recv_exactly(sock, n: int) -> bytes:
"""循环 recv 直到收满 n 字节——recv 不保证一次返回所需长度。"""
buf = bytearray()
while len(buf) < n:
chunk = sock.recv(n - len(buf))
if not chunk:
raise ConnectionError('连接已关闭')
buf.extend(chunk)
return bytes(buf)
  • 短连接:每次请求建立 TCP 连接→发数据→关闭连接。优点是简单,缺点是三次握手开销大(典型局域网 RTT 约 0.5 ms,握手就占 1.5 ms)。早期 HTTP/1.0 就是短连接。
  • 长连接(keep-alive):建一次连接,多次复用。工业场景几乎都用长连接——PLC 与上位机之间的连接可能保持数月不断开。

主动关闭连接的一方会进入 TIME_WAIT 状态,持续 2 MSL(Maximum Segment Lifetime,默认 60 秒 × 2 = 120 秒)。如果服务端频繁重启用同一端口,会因大量 TIME_WAIT 占用而 bind() 失败。

Terminal window
# 查看系统当前的 TIME_WAIT 数量
ss -ant | grep TIME-WAIT | wc -l
# 解决方案:设置端口复用
# Python 中在 bind() 之前设置 SO_REUSEADDR
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

Socket(套接字)是操作系统提供的网络通信接口,是所有上层网络库的基石。

# server.py —— 一个最简 TCP echo server
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('0.0.0.0', 502)) # Modbus 默认端口 502
server.listen(5) # backlog = 5 个等待队列
print('等待连接...')
conn, addr = server.accept() # 阻塞,直到有客户端连入
print(f'客户端 {addr} 已连接')
while True:
data = conn.recv(1024)
if not data:
break
conn.sendall(data) # echo 回去
conn.close()
# client.py —— 对应的客户端
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('192.168.1.100', 502))
client.sendall(b'\x00\x01\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0a')
response = client.recv(1024)
print(f'收到响应: {response.hex()}')
client.close()

当服务端需要同时处理数百上千个连接时,为每个连接分配一个线程是不可行的。I/O 多路复用(I/O multiplexing)允许单线程同时监听多个文件描述符(file descriptor, fd)的状态变化。

机制最大连接数事件检测方式性能
selectFD_SETSIZE(默认 1024)每次调用遍历所有 fdO(n),最差
poll无上限同 select,遍历所有 fdO(n)
epoll无上限内核回调通知,只返回就绪 fdO(1),最佳

epoll 是 Linux 独有的高效机制,也是 Python asyncio、Nginx、Redis 等高性能框架的底层引擎。

epoll 有两种触发模式:

  • Level-Triggered(LT,水平触发):只要 fd 的缓冲区有数据可读,epoll_wait 就会持续通知。使用简单,但若不读完数据会反复唤醒。select/poll 都是 LT。
  • Edge-Triggered(ET,边缘触发):仅在新数据到达的那一瞬间通知一次,后续不再通知,除非又有新数据写入。性能更高(减少系统调用),但要求程序必须一次读完所有数据(用非阻塞 I/O + 循环读至 EAGAIN)。
import select, socket
# epoll 示例(Linux 专属)
epoll = select.epoll()
server.setblocking(False)
epoll.register(server.fileno(), select.EPOLLIN)
connections = {}
while True:
events = epoll.poll(timeout=1) # 返回就绪 fd 列表
for fd, event in events:
if fd == server.fileno():
conn, addr = server.accept()
conn.setblocking(False)
epoll.register(conn.fileno(), select.EPOLLIN)
connections[conn.fileno()] = conn
elif event & select.EPOLLIN:
data = connections[fd].recv(1024)
if data:
# 处理数据...
pass
else:
epoll.unregister(fd)
connections[fd].close()

Python asyncio 的底层就是 epoll——asyncio 将 epoll 封装为事件循环(event loop),配合 async/await 语法,让你用同步风格的代码获得 epoll 级别的并发性能。详见 Python 工程进阶。


Modbus 由 Modicon 公司(现施耐德电气)于 1979 年发明,是工业领域使用最广泛的通信协议——没有之一。它极其简单:一个请求,一个响应,没有复杂的握手。Modbus TCP 是其以太网版本(Modbus RTU 是串行版本)。

Modbus 将设备内存抽象为四张表:

类型读写数据类型功能码
线圈(Coil)读/写单 bit01 读 / 05 写单个 / 0F 写多个
离散输入(Discrete Input)只读单 bit02 读
保持寄存器(Holding Register)读/写16 bit03 读 / 06 写单个 / 10 写多个
输入寄存器(Input Register)只读16 bit04 读

记忆技巧:Coil 和 Discrete Input 是”开关量”(1 bit),Holding Register 和 Input Register 是”数值量”(16 bit)。带”Input”的都是只读。

Modbus TCP 报文前 7 字节是 MBAP(Modbus Application Protocol)header:

Byte: 0-1 2 3-4 5 6 7...
字段: 事务ID | 协议ID | 长度 | 单元ID | 功能码 | 数据...
长度: 2 B | 2 B | 2 B | 1 B | 1 B | ...
  • 事务 ID(Transaction ID):2 字节,用于请求/响应配对,客户端自增即可。
  • 协议 ID:固定 0x0000(表示 Modbus)。
  • 长度:后续字节数(含 Unit ID)。
  • 单元 ID(Unit ID):从站地址,串行网络中标识具体设备。
from pymodbus.client import ModbusTcpClient
# 连接 Modbus TCP 设备(如变频器、PLC 网关)
client = ModbusTcpClient('192.168.1.50', port=502, timeout=3)
client.connect()
# —— 读取保持寄存器(功能码 03)——
# 从地址 0 开始读 10 个寄存器,从站地址 = 1
result = client.read_holding_registers(address=0, count=10, slave_id=1)
if not result.isError():
print(f'寄存器值: {result.registers}')
# 例如 [2300, 0, 500, ...] —— 可能是温度×10、状态码、频率等
# —— 写单个保持寄存器(功能码 06)——
client.write_register(address=0, value=2500, slave_id=1)
# —— 读线圈(功能码 01)——
coils = client.read_coils(address=0, count=8, slave_id=1)
print(f'线圈状态: {coils.bits}') # [True, False, False, ...]
# —— 写线圈(功能码 05)——
client.write_coil(address=0, value=True, slave_id=1)
client.close()

工程提示:工业现场寄存器的物理含义由设备厂商的”寄存器映射表”(register map / communication manual)定义。Modbus 协议本身不知道地址 0 是”温度”还是”压力”,这需要你查手册。常见陷阱:大端/小端字节序、32 位浮点数需读两个寄存器再拼接、不同厂商的寄存器偏移量差异(PLC 地址 vs 协议地址可能差 1)。


OPC UA(OLE for Process Control Unified Architecture,工业统一架构)是 OPC 基金会推出的平台无关的工业通信标准。如果说 Modbus 是”功能机时代的短信”,那 OPC UA 就是”智能手机时代的微信”——功能强大,但复杂度也高得多。

概念说明
节点(Node)OPC UA 中一切皆节点——传感器、变量、方法、视图,每个节点有唯一 NodeID
NodeID节点标识符,格式为 ns=<namespace>;i=<numeric> 或 ns=<namespace>;s=<string>
命名空间(Namespace)命名空间编号,用于区分不同供应商的节点集合
地址空间(Address Space)所有节点组成的树状结构,类似文件系统
订阅(Subscription)客户端订阅节点变化,服务端主动推送,替代轮询
安全模式None / Sign / SignAndEncrypt,基于 X.509 证书

Modbus 的工作方式是”轮询”(polling)——客户端每隔 100 ms 问一次”温度是多少?“。如果数据没变化,这就是浪费带宽。OPC UA 引入了订阅机制:客户端只需说”温度变化超过 0.5°C 就通知我”,之后服务端会在条件满足时主动推送(publish/notify),大幅降低网络负载。

from opcua import Client
client = Client('opc.tcp://192.168.1.60:4840/freeopcua/server/')
# client.set_security(...) # 可配置证书加解密
try:
client.connect()
# 通过 NodeID 获取节点
temp_node = client.get_node('ns=2;s=Temperature')
temperature = temp_node.get_value()
print(f'当前温度: {temperature} °C')
# 创建订阅,数据变化时回调
from opcua import Subscription
sub = client.create_subscription(period=500, handler=MySubHandler())
handle = sub.subscribe_data_change(temp_node)
finally:
client.disconnect()
class MySubHandler:
"""订阅回调处理器。"""
def datachange_notification(self, node, val, data):
print(f'温度更新: {val} °C (来自 {node.node_id})')
维度Modbus TCPOPC UA
复杂度极简,几十行代码上手复杂,概念多
数据模型扁平寄存器表树状地址空间 + 类型系统
数据变化通知❌ 只能轮询✅ 订阅推送
安全性无加密(明文)证书签名 + 加密
语义信息无(靠手册猜含义)内含变量名、单位、范围
适用场景简单设备、成本敏感复杂系统、需要信息模型

选型直觉:设备少、协议简单→Modbus;设备多、厂商杂、需要统一数据模型→OPC UA。很多工业网关支持同时做 Modbus master 和 OPC UA server,充当协议转换器。


MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)是 IBM 于 1999 年发明的轻量级发布/订阅协议,专为带宽受限、网络不稳定的物联网场景设计。它已成为工业 IoT 数据上云的事实标准。

与 Modbus 的”点对点请求-响应”不同,MQTT 采用发布/订阅(publish/subscribe)模式,引入一个中间角色——Broker(代理服务器):

  • 发布者(Publisher)只管把消息发到某个 topic,不关心谁接收。
  • 订阅者(Subscriber)向 Broker 注册感兴趣的 topic,有消息时 Broker 自动推送。
  • 解耦:发布者和订阅者之间不需要知道彼此的存在,系统扩展性极强。

Topic 用 / 分隔层级,类似文件路径:

factory/floor1/lineA/temperature
factory/floor1/lineA/pressure
factory/floor1/lineB/temperature

订阅时支持通配符:

通配符含义示例
+匹配单层factory/+/temperature 匹配所有楼层的温度
#匹配多层(末尾)factory/# 匹配 factory 下所有子层级

MQTT 定义了三个 QoS(Quality of Service)等级:

等级名称交付保证握手次数适用场景
QoS 0At most once(最多一次)不保证送达,不重传1 次(fire and forget)高频遥测数据,丢一两条无所谓
QoS 1At least once(至少一次)保证送达,可能重复2 次(PUBLISH + PUBACK)常用默认级别
QoS 2Exactly once(恰好一次)保证送达且不重复4 次(PUBLISH→PUBREC→PUBREL→PUBCOMP)计费、指令等关键消息

工程权衡:QoS 2 虽然最可靠,但 4 次握手的开销在低带宽网络下显著。工业遥测数据通常用 QoS 0 或 1;控制指令(如”紧急停机”)才用 QoS 2。

  • Retain(保留消息):发布时设置 retain=True,Broker 会保存最后一条消息。新的订阅者上线后立即收到这条保留消息,而不需要等下一次数据更新。非常适合”当前状态”类数据(如开关状态、当前温度)。

  • Last Will and Testament(遗嘱机制):客户端连接时可以注册一条”遗嘱消息”。如果客户端异常断开(非正常 disconnect,如断电、网络中断),Broker 会自动发布这条遗嘱消息。订阅者据此得知设备离线。

Mosquitto 是最流行的开源 MQTT broker:

/etc/mosquitto/mosquitto.conf
# 允许匿名访问(生产环境应关闭,配合 password_file)
allow_anonymous true
# MQTT 标准端口
listener 1883
# WebSocket 端口(供浏览器前端直接连接)
listener 9001
protocol websockets
# 持久化——将消息和订阅关系保存到磁盘,重启不丢失
persistence true
persistence_location /var/lib/mosquitto/
# 最大保持消息数(-1 不限制)
max_queued_messages 2000
# 日志
log_dest file /var/log/mosquitto/mosquitto.log
import paho.mqtt.client as mqtt
import json, time
BROKER = '192.168.1.100'
PORT = 1883
# —— 连接回调 ——
def on_connect(client, userdata, flags, rc, properties=None):
if rc == 0:
print('已连接 Broker')
client.subscribe('factory/+/temperature', qos=1) # 通配符订阅
else:
print(f'连接失败,返回码 {rc}')
# —— 消息到达回调 ——
def on_message(client, userdata, msg):
payload = json.loads(msg.payload)
print(f'[{msg.topic}] 温度={payload["value"]}°C 时间={payload["ts"]}')
client = mqtt.Client(
callback_api_version=mqtt.CallbackAPIVersion.VERSION2,
client_id='ai-collector-01',
)
client.on_connect = on_connect
client.on_message = on_message
# 设置遗嘱:设备掉线时通知其他系统
client.will_set(
topic='device/ai-collector-01/status',
payload=json.dumps({'online': False}),
qos=1,
retain=True,
)
client.connect(BROKER, PORT, keepalive=60)
client.loop_start() # 后台线程处理网络事件
# —— 模拟发布数据 ——
while True:
temp = read_sensor() # 假设的传感器读取函数
client.publish(
topic='factory/floor1/lineA/temperature',
payload=json.dumps({'value': temp, 'ts': time.time()}),
qos=1,
retain=False,
)
time.sleep(1.0)

MQTT 采集的数据通常直接写入时序数据库或消息队列进行流处理,详见 数据库与中间件。


Modbus 和 OPC UA 虽好,但很多主流 PLC 厂商使用专有协议来暴露更丰富的功能(如直接访问数据块 DB、定时器、计数器等内部资源)。

西门子(Siemens)S7 系列 PLC(S7-200/300/400/1200/1500)使用 S7comm/S7comm-Plus 协议,底层走 TCP 端口 102(ISO-on-TCP, RFC 1006)。开源库 snap7 提供了完整的 S7 通信实现。

import snap7
from snap7.util import get_real, get_int
# 连接西门子 S7-1200/1500 PLC
plc = snap7.client.Client()
plc.connect('192.168.0.1', rack=0, slot=1)
# 读取数据块 DB1 的前 4 字节(一个 REAL 浮点数)
# 参数:db_number, start, size
data = plc.db_read(db_number=1, start=0, size=4)
temperature = get_real(data, 0) # 解析为 IEEE 754 float
print(f'DB1.DBD0 (温度): {temperature} °C')
# 读取 DB1 的第 4 字节开始的 INT(2 字节有符号整数)
data2 = plc.db_read(db_number=1, start=4, size=2)
speed = get_int(data2, 0)
print(f'DB1.DBW4 (转速): {speed} rpm')
# 写入数据——将设定值写入 DB1.DBW8
from snap7.util import set_int
write_buf = bytearray(2)
set_int(write_buf, 0, 1500)
plc.db_write(db_number=1, start=8, data=write_buf)
plc.disconnect()

注意事项:西门子 S7-1200/1500 默认关闭了”PUT/GET 通信”功能,需要在 TIA Portal 工程中勾选启用,否则 snap7 连接会报权限错误。同时需要在 PLC 属性中勾选”Permit access with PUT/GET communication”。

三菱(Mitsubishi)PLC 使用 MC Protocol(MELSEC Communication Protocol),支持二进制和 ASCII 两种格式。报文以特定命令帧开头(如 5000 为二进制模式的 MC 请求),支持按设备名(D寄存器、M继电器等)读写。

# 使用 mcprotocol 库(第三方开源)
from mcprotocol import MelsecMcA7EBinary
plc = MelsecMcA7EBinary(host='192.168.1.10', port=5000)
plc.open()
# 读取 D100 开始 10 个 16 位寄存器
values = plc.read_words('D100', 10)
print(f'D100-D109: {values}')
plc.close()
维度Modbus TCPOPC UAS7/MC 专有协议
厂商中立✅ 开放标准✅ 开放标准❌ 厂商绑定
实施难度低中-高低(有库)
数据丰富度寄存器级别带语义的完整信息模型可直接访问 PLC 内部全部资源
通信效率高中(开销较大)高
实时性毫秒级依赖配置毫秒级
安全性无证书+加密部分支持
推荐场景简单设备采集多厂商统一数据平台同厂商 PLC 深度集成

下图为三种主流工业协议在典型 100 Mbps 工业以太网环境下、单连接、约 100 字节小报文场景的延迟与吞吐对比:

工业协议延迟与吞吐对比

解读:MQTT 延迟最低、吞吐最高,但这是因为其报文极轻量且 broker 转发高效;OPC UA 的安全握手和 XML/二进制编码带来额外开销;Modbus 居中,胜在简单稳定。实际性能受网络质量、报文大小、并发连接数等影响。


  1. 先用 Wireshark 抓包再写代码:Wireshark 内置 Modbus、OPC UA、MQTT 协议解析器(dissector),可以逐字节查看报文。遇到通信问题时,抓包是第一排查手段。
  2. 超时设置一定要配:工业网络可能因设备重启、线缆松动而中断。务必为每个 socket 设置 timeout,避免线程永久阻塞。
  3. 断线自动重连:工业场景的连接终会断开(设备维护、网络抖动),程序必须实现指数退避重连逻辑。
  4. 字节序陷阱:Modbus 使用大端(big-endian),但很多 PLC 默认小端(little-endian)。32 位数据需跨两个寄存器时,注意字交换(word swap)。
  5. OPC UA 证书管理:生产环境务必启用 SignAndEncrypt 模式,妥善管理客户端/服务端 X.509 证书,切勿用 None 安全模式。

趋势说明
OPC UA over TSN时间敏感网络(Time-Sensitive Networking)为以太网增加确定性延迟保障,使 OPC UA 可用于硬实时控制场景(≤1 ms 周期),逐步替代传统现场总线
MQTT 5.0 普及MQTT 5.0 引入共享订阅(Shared Subscription,多消费者负载均衡)、消息过期、主题别名等功能,2025 年已成为主流 broker(EMQX 5.x、HiveMQ 4.x)的默认选项
Sparkplug BEclipse Sparkplug B 规范为工业 MQTT 定义了统一的数据载荷格式和状态管理机制,解决原生 MQTT 缺少设备生命周期管理的问题
OPC UA FXOPC UA Field eXchange(FX)定义了控制器之间的对等通信标准(PLC-to-PLC),无需中央 OPC UA server,支持去中心化的控制器协同
pymodbus 3.x 重构2024-2025 年 pymodbus 3.x 系列进行了全面异步化重构(asyncio 原生支持),同步/异步双 API 并行,性能提升显著
OPC UA over MQTT (PubSub)OPC UA 的 PubSub 模式允许通过 MQTT 或 AMQP 作为传输层,融合了 OPC UA 的语义能力和 MQTT 的轻量传输,成为工业 IoT 云端集成的热点方案

术语英文释义
套接字Socket操作系统提供的网络通信端点抽象
多路复用I/O Multiplexing单线程同时监听多个 I/O 事件的技术
水平触发Level-Triggered (LT)只要条件满足就持续通知的模式
边缘触发Edge-Triggered (ET)仅在状态变化瞬间通知一次的模式
线圈CoilModbus 中可读写的单 bit 数据,源于继电器线圈
保持寄存器Holding RegisterModbus 中可读写的 16 bit 数据单元
地址空间Address SpaceOPC UA 中所有节点构成的树状结构
订阅Subscription客户端注册兴趣点,服务端数据变化时主动推送
发布/订阅Publish/Subscribe发布者与订阅者通过中间代理间接通信的解耦模式
代理BrokerMQTT 中的消息转发中心
服务质量QoS (Quality of Service)MQTT 定义的交付保证等级(0/1/2)
遗嘱Last Will and TestamentMQTT 客户端异常断线时由 broker 代发的通知消息
保留消息Retained MessageBroker 缓存的最后一条消息,新订阅者上线即收到
现场总线Fieldbus工业现场设备级的数字通信网络
时间敏感网络TSN (Time-Sensitive Networking)IEEE 802.1 系列标准,为以太网提供确定性实时通信能力