
近期在调试一套工业环境监控系统时,遇到了关于 Modbus 连接保持的问题。现场部署了一批 恒湿净化设备(包括 @恒湿消毒净化一体机 等),主要用来维持特定环境的温湿度指标。在长时间运行后,发现设备偶尔会掉线。把 Modbus TCP 和 UDP 做成"双协议共存"之后,问题从"单协议轮询瓶颈"变成了"两个协议栈如何在同一颗 MCU 里互不干扰、报文如何抓、如何判"——这才是高并发机房监控里最硬的工程细节。

先说清楚场景:这不是"二选一"的纠结,而是物理层复用、传输层分工。
以太网温湿度变送器(W5500 / LAN8720 + STM32F407)
├── Modbus TCP Server(端口 502)
│ 用途:网关周期轮询,拉取全量寄存器
│ 特点:可靠、有序、有连接状态
│
└── UDP Server(端口 161 / 自定义 9000)
用途:SNMP Trap 推送 / 私有协议广播
特点:无连接、低开销、事件触发秒级上报共存不是重复造轮子,而是各干各的活:
两个协议跑在同一颗 MCU、同一块 PHY 芯片、同一个 IP 地址上,共享 MAC 层发送队列。这才是"共存设计"要解决的真正问题。
用 Wireshark 抓同一台 POE 温湿度变送器(IP: 10.10.10.15)的两种报文,逐层拆开看。
Ethernet II, Src: 00:08:dc:ab:cd:ef (W5500 MAC)
Dst: 00:0c:29:11:22:33 (Gateway MAC)
Type: 0x0800 (IPv4)两种协议在 MAC 层没有任何区别——都是标准的 IEEE 802.3 帧。区别从 IP 层开始。
Internet Protocol Version 4
0100 .... = Version: 4
.... 0101 = Header Length: 20 bytes
Differentiated Services Field: 0x00
Total Length: 60 (Modbus TCP) / 48 (UDP)
Identification: 0x1a2b
Flags: 0x40 (Don't Fragment)
Time to Live: 64
Protocol: 6 (TCP) / 17 (UDP) ← 唯一关键差异
Header Checksum: 0x3c4d [correct]
Source Address: 10.10.10.15 (变送器)
Destination Address: 10.10.10.100 (网关)Modbus TCP(端口 502):
Transmission Control Protocol
Source Port: 502
Destination Port: 49152 (网关临时端口)
Sequence Number: 12345
Acknowledgment Number: 67890
Header Length: 32 bytes (含 Options)
Flags: 0x018 (PSH, ACK)
Window Size: 8192
Checksum: 0x9a8b [correct]
Options:
Maximum segment size: 1460
Window scale: 2
SACK permittedUDP(端口 9000,私有协议):
User Datagram Protocol
Source Port: 9000
Destination Port: 9000
Length: 28
Checksum: 0x7f6e [correct]对比总结:
字段 | Modbus TCP | UDP |
|---|---|---|
头部大小 | 32 bytes(含 Options) | 8 bytes |
连接状态 | 有(SYN/ACK/FIN) | 无 |
序列号 | 有(防乱序、重排) | 无 |
确认机制 | ACK(可靠) | 无 |
流量控制 | 滑动窗口 | 无 |
拥塞控制 | 慢启动/拥塞避免 | 无 |
重传 | 自动(RTO) | 应用层自己做 |
Modbus TCP ADU(应用数据单元):
MBAP Header (7 bytes):
Transaction ID: 0x0001 ← 网关请求和传感器响应配对的关键
Protocol ID: 0x0000 ← 永远是 0
Length: 0x0006 ← 后面字节数
Unit ID: 0x01 ← 从站地址
PDU:
Function Code: 0x04 ← Read Input Registers
Starting Address: 0x0000
Quantity: 0x0005 ← 读 5 个寄存器
响应:
Transaction ID: 0x0001 ← 和请求一致
Protocol ID: 0x0000
Length: 0x000d
Unit ID: 0x01
Function Code: 0x04
Byte Count: 0x0a ← 10 bytes (5 registers × 2)
Registers:
0x00e2 → 22.6℃ (×0.1)
0x0216 → 53.4%RH (×0.1)
0x00c3 → 19.5℃ 露点
0x0001 → 状态正常
0x0000 → 质量 OKUDP 私有协议(自定义帧):
Header (8 bytes):
Magic: 0xAA55 ← 帧同步头
Version: 0x01
Sensor ID: 0x00010023 ← 设备唯一 ID
Payload Len: 0x0010 ← 16 bytes
Payload (16 bytes):
Timestamp: 0x5f3a_2c1b ← 采集时间(Unix 32-bit)
Temperature: 0x00e2 ← 22.6℃ (×0.1, int16)
Humidity: 0x0216 ← 53.4%RH (×0.1, int16)
Dew Point: 0x00c3 ← 19.5℃ (×0.1, int16)
Alert Flags: 0x0001 ← bit0=高温, bit1=高湿
CRC16: 0x8f4e ← 校验
总计:8 + 16 = 24 bytes 应用数据关键差异:Modbus TCP 的 Transaction ID 是"请求-响应配对"的唯一依据;UDP 没有这个机制,必须靠应用层 Magic + Sensor ID + Timestamp 来配对和去重。
# 在边缘网关上抓指定传感器的双向流量
sudo tcpdump -i eth0 host 10.10.10.15 -w capture.pcap
# 或者只抓 Modbus TCP
sudo tcpdump -i eth0 host 10.10.10.15 and port 502 -w modbus.pcap
# 只抓 UDP
sudo tcpdump -i eth0 host 10.10.10.15 and port 9000 -w udp.pcap# Modbus TCP 读请求
modbus.func_code == 4
# Modbus TCP 响应
modbus && !tcp.flags.syn && !tcp.flags.fin
# UDP 越限 Trap
udp.port == 9000 && data[0:2] == aa:55
# TCP 重传(排查连接问题)
tcp.analysis.retransmission
# 零窗口(接收方缓冲区满)
tcp.analysis.zero_window
# 连接重置(对端强制断开)
tcp.flags.reset == 1场景 1:正常轮询(Modbus TCP)
No. Time Source Destination Protocol Length Info
1 0.000000 10.10.10.100 10.10.10.15 TCP 74 49152 → 502 [SYN]
2 0.000218 10.10.10.15 10.10.10.100 TCP 74 502 → 49152 [SYN, ACK]
3 0.000312 10.10.10.100 10.10.10.15 TCP 66 49152 → 502 [ACK]
4 0.000401 10.10.10.100 10.10.10.15 Modbus 72 Read Input Registers
5 0.000892 10.10.10.15 10.10.10.100 TCP 66 502 → 49152 [ACK]
6 0.000901 10.10.10.15 10.10.10.100 Modbus 86 Response
7 0.001234 10.10.10.100 10.10.10.15 TCP 66 49152 → 502 [FIN, ACK]
8 0.001456 10.10.10.15 10.10.10.100 TCP 66 502 → 49152 [FIN, ACK]
9 0.001523 10.10.10.100 10.10.10.15 TCP 66 49152 → 502 [ACK]
往返时间:~0.9ms(局域网内)场景 2:UDP Trap 越限推送
No. Time Source Destination Protocol Length Info
10 5.230000 10.10.10.15 10.10.10.100 UDP 82 Source port: 9000
Data: aa 55 01 00 01 00 23 10 5f 3a 2c 1b 00 e2 02 16
00 c3 00 01 8f 4e场景 3:TCP 连接被重置(真实翻车)
No. Time Source Destination Protocol Length Info
15 30.500000 10.10.10.100 10.10.10.15 TCP 74 49153 → 502 [SYN]
16 30.500218 10.10.10.15 10.10.10.100 TCP 74 502 → 49153 [RST, ACK]→ 传感器 Modbus Server 连接池满了(低端 MCU 只支持 1–2 个并发 TCP 连接),拒绝新连接。这就是前面几篇提到的"三机抢资源"的底层原因。
// lwipopts.h
#define LWIP_TCP 1
#define LWIP_UDP 1
#define TCP_MSS 1460
#define TCP_SND_BUF 4096
#define TCP_WND 4096
#define MEMP_NUM_TCP_PCB 4 // TCP 连接数上限
#define MEMP_NUM_UDP_PCB 2 // UDP 监听数
#define PBUF_POOL_SIZE 16 // 内存池大小
#define PBUF_POOL_BUFSIZE 256 // 每个 pbuf 大小关键问题:TCP 和 UDP 共享 LWIP 的 pbuf 内存池。如果 UDP 广播风暴把池子占满,TCP 的 SYN 都分配不到内存,连接直接失败。
// 两个独立任务,不同优先级
void modbus_tcp_task(void *arg) {
struct netconn *conn = netconn_new(NETCONN_TCP);
netconn_bind(conn, IP_ADDR_ANY, 502);
netconn_listen(conn);
while (1) {
struct netconn *client;
netconn_accept(conn, &client);
handle_modbus_request(client); // 处理完后关闭
}
}
void udp_trap_task(void *arg) {
struct netconn *conn = netconn_new(NETCONN_UDP);
netconn_bind(conn, IP_ADDR_ANY, 9000);
while (1) {
struct netbuf *buf;
netconn_recv(conn, &buf); // 接收 UDP 数据
handle_udp_frame(buf); // 解析后触发 Trap 发送
netbuf_delete(buf);
}
}发送优先级(从高到低):
1. UDP Trap(越限事件,最高优先,不等 TCP)
2. Modbus TCP 响应(已建立的连接,必须及时回复)
3. UDP 心跳/广播(最低优先,可丢)
实现方式:
- LWIP 的 tcp_output() 和 udp_send() 共享底层 MAC 发送
- 在应用层给 UDP Trap 设独立发送缓冲区,绕过 pbuf 池竞争
- 或给 Trap 帧设 VLAN Priority Tag (802.1p, PRI=7)import asyncio
from pymodbus.client import AsyncModbusTcpClient
import socket
import struct
# Modbus TCP 轮询(已有实现,略)
# UDP Trap 接收(独立协程)
UDP_LISTEN_PORT = 9000
SENSORS = {} # ip → sensor_info
def parse_udp_frame(data):
"""解析 UDP 私有协议帧"""
if len(data) < 24:
return None
magic, version, sensor_id, payload_len = struct.unpack('>HBHI', data[:8])
if magic != 0xAA55:
return None
payload = data[8:8+payload_len]
ts, temp_raw, hum_raw, dew_raw, alert_flags, crc = struct.unpack(
'>IhhhHI', payload)
return {
'sensor_id': sensor_id,
'collect_ts': ts,
'temperature': temp_raw * 0.1,
'humidity': hum_raw * 0.1,
'dew_point': dew_raw * 0.1,
'alert_flags': alert_flags,
'crc_ok': verify_crc(data[8:8+payload_len-2], crc),
}
async def udp_trap_listener():
"""UDP Trap 监听协程"""
loop = asyncio.get_running_loop()
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('0.0.0.0', UDP_LISTEN_PORT))
sock.setblocking(False)
while True:
try:
data, addr = await loop.sock_recvfrom(sock, 1024)
frame = parse_udp_frame(data)
if frame and frame['crc_ok']:
# 立即上云,QoS=1
await publish_trap(frame, addr[0])
# 同时加速该传感器的 Modbus 轮询
accelerate_poll(addr[0])
except Exception as e:
print(f"UDP parse error: {e}")
async def main():
# 启动 UDP 监听
udp_task = asyncio.create_task(udp_trap_listener())
# 启动 Modbus TCP 轮询
modbus_task = asyncio.create_task(poll_all(SENSORS, interval=30))
await asyncio.gather(udp_task, modbus_task)# 写入 InfluxDB(asyncio + aiohttp)
import aiohttp
INFLUX_URL = "http://influx:8086/api/v2/write"
INFLUX_BUCKET = "archive_env"
INFLUX_ORG = "archive"
INFLUX_TOKEN = "xxx"
async def write_influx(point):
"""point: InfluxDB line protocol string"""
async with aiohttp.ClientSession() as session:
async with session.post(
INFLUX_URL,
params={"bucket": INFLUX_BUCKET, "org": INFLUX_ORG},
headers={"Authorization": f"Token {INFLUX_TOKEN}"},
data=point,
) as resp:
if resp.status != 204:
print(f"Influx write failed: {resp.status}")
def build_line_protocol(sensor_id, measurement, fields, tags=None, ts=None):
"""构建 InfluxDB 行协议"""
tag_str = ",".join(f"{k}={v}" for k, v in (tags or {}).items())
field_str = ",".join(f"{k}={v}" for k, v in fields.items())
ts_str = f" {ts}" if ts else ""
return f"{measurement},{tag_str} {field_str}{ts_str}"
# Modbus TCP 数据写入
point = build_line_protocol(
sensor_id="th-001",
measurement="environment",
tags={"zone": "room3", "protocol": "modbus_tcp"},
fields={
"temperature": 22.6,
"humidity": 53.4,
"dew_point": 19.5,
"quality": 0,
},
ts=int(time.time() * 1e9), # 纳秒时间戳
)
await write_influx(point)
# UDP Trap 数据写入(带 replay 标记)
point = build_line_protocol(
sensor_id="th-001",
measurement="alert_event",
tags={"zone": "room3", "protocol": "udp_trap", "source": "edge"},
fields={
"temperature": 22.8,
"humidity": 65.2,
"alert_flags": 1,
"replay": False,
},
ts=collect_ts_ns, # 用采集时间,不用到达时间
)
await write_influx(point)pbuf 池要够大,或者给 UDP Trap 开独立 DMA 缓冲区,防止 TCP 和 UDP 互抢内存导致连接重置。 collect_ts 是传感器本地时间。网关侧要做时间偏移校准(NTP 校时后计算偏移量),否则补传数据时间错乱。 Modbus TCP 和 UDP 在同一台以太网温湿度变送器里共存,不是简单的"两个端口同时监听",而是 MAC 层共享、传输层分工、应用层互补的工程平衡。 TCP 管全量可靠,UDP 管事件秒级,Wireshark 验证,InfluxDB 统一存储,边缘网关做时间对齐和去重——这才是高并发机房监控里经得起生产考验的双协议架构。
下一篇候选:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。