

近期在调试一套工业环境监控系统时,遇到了关于 Modbus 连接保持的问题。现场部署了一批 恒湿净化设备(包括 @恒湿消毒净化一体机 等),主要用来维持特定环境的温湿度指标。在长时间运行后,发现设备偶尔会掉线。把 Modbus TCP、UDP 和 SNMP 做成"三协议并行"之后,问题从"单协议轮询瓶颈"变成了"三个协议栈如何在同一颗 MCU 里互不干扰、端口如何分配、报文如何抓、如何判"——这才是高并发机房监控里最硬的工程细节。
先说清楚:这不是炫技,是物理层复用、传输层分工、应用层互补。
以太网温湿度变送器(W5500 / LAN8720 + STM32F407)
├── Modbus TCP Server(端口 502)
│ 用途:网关周期轮询,拉取全量寄存器
│ 特点:可靠、有序、有连接状态
│ 负载:温度、湿度、露点、状态、质量戳
│
├── UDP Server(端口 9000,私有协议)
│ 用途:事件 Trap 推送、越限秒级上报
│ 特点:无连接、低开销、不等轮询
│
└── SNMP Agent(端口 161 UDP)
用途:标准网管查询、Trap 推送(端口 162)
特点:标准化 MIB、与 NMS 集成、读/写/通知三协议各司其职:
协议 | 角色 | 数据特征 | 可靠性要求 |
|---|---|---|---|
Modbus TCP | 全量基线 | 周期性、结构化、多寄存器 | 高(必须读到) |
UDP 私有 | 事件快报 | 瞬时、小包、越限/故障 | 中(可丢,但尽量快) |
SNMP | 标准管理 | 标量、表、Trap | 中(Get 需响应,Trap 可丢) |
变送器 IP: 10.10.10.15
TCP 502 → Modbus TCP(RW,全量寄存器)
UDP 161 → SNMP Agent(RO,标准 MIB)
UDP 162 → SNMP Trap 发送(主动推送)
UDP 9000 → 私有事件 Trap(越限/故障)关键设计决策:
LWIP 协议栈资源(STM32F407 + 192KB RAM):
TCP PCB: 2 个(Modbus TCP,限制连接数)
UDP PCB: 4 个(SNMP 161、SNMP Trap 162、私有 UDP 9000、DNS)
pbuf 池: 32 个 × 512 bytes(Modbus TCP 大帧 + SNMP 小帧 + UDP Trap)为什么 UDP PCB 给 4 个?
用 Wireshark 抓同一台变送器(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)Internet Protocol Version 4
Protocol: 6 (TCP) / 17 (UDP) ← TCP 用于 Modbus,UDP 用于 SNMP 和私有协议
Source: 10.10.10.15
Destination: 10.10.10.100 (网关/NMS)字段 | Modbus TCP (502) | SNMP (161) | 私有 UDP (9000) |
|---|---|---|---|
协议 | TCP | UDP | UDP |
头部 | 32 bytes (含 Options) | 8 bytes | 8 bytes |
连接 | 有 (SYN/ACK/FIN) | 无 | 无 |
可靠性 | 确认+重传 | 无 | 无 |
流量控制 | 滑动窗口 | 无 | 无 |
Modbus TCP(读 5 个输入寄存器):
MBAP Header:
Transaction ID: 0x0001
Protocol ID: 0x0000
Length: 0x0006
Unit ID: 0x01
PDU:
Function Code: 0x04 (Read Input Registers)
Starting Address: 0x0000
Quantity: 0x0005
响应:
Transaction ID: 0x0001
Length: 0x000d
Unit ID: 0x01
Byte Count: 0x0a
Registers: [0x00e2, 0x0216, 0x00c3, 0x0001, 0x0000]SNMP GetRequest(读 sysDescr):
SNMPv2c
Version: 2c
Community: public
PDU Type: GetRequest
Request ID: 0x7a3f
Error Status: 0 (noError)
Error Index: 0
VarBind List:
sysDescr.0 (1.3.6.1.2.1.1.1.0)
响应:
PDU Type: GetResponse
sysDescr.0 = "EnvSensor-V2.1.3, STM32F407, LWIP-2.1.2"私有 UDP Trap(越限事件):
Header (8 bytes):
Magic: 0xAA55
Version: 0x01
Sensor ID: 0x00010023
Payload Len: 0x0010
Payload:
Timestamp: 0x5f3a_2c1b
Temperature: 0x00e2 (22.6℃)
Humidity: 0x0216 (53.4%RH)
Dew Point: 0x00c3 (19.5℃)
Alert Flags: 0x0001 (bit0=高温)
CRC16: 0x8f4e# 在边缘网关上抓指定传感器的全协议流量
sudo tcpdump -i eth0 host 10.10.10.15 -w capture_3proto.pcap
# 或分协议抓
sudo tcpdump -i eth0 host 10.10.10.15 and port 502 -w modbus.pcap
sudo tcpdump -i eth0 host 10.10.10.15 and port 161 -w snmp.pcap
sudo tcpdump -i eth0 host 10.10.10.15 and port 9000 -w udp_trap.pcap# Modbus TCP 读请求
modbus.func_code == 4
# SNMP GetRequest
snmp.pdu_type == 0 # GetRequest
# SNMP Trap
snmp.pdu_type == 4 # Trap
# UDP 私有 Trap
udp.port == 9000 && data[0:2] == aa:55
# TCP 重传
tcp.analysis.retransmission
# TCP 零窗口
tcp.analysis.zero_window
# UDP 丢包(无直接标志,看 IO Graph 速率)场景 1:正常三协议并行(时间轴)
No. Time Source Destination Protocol Info
1 0.000000 10.10.10.100 10.10.10.15 TCP 49152 → 502 [SYN]
2 0.000218 10.10.10.15 10.10.10.100 TCP 502 → 49152 [SYN, ACK]
3 0.000312 10.10.10.100 10.10.10.15 TCP 49152 → 502 [ACK]
4 0.000401 10.10.10.100 10.10.10.15 Modbus Read Input Registers
5 0.000892 10.10.10.15 10.10.10.100 Modbus Response
6 0.001234 10.10.10.100 10.10.10.15 TCP 49152 → 502 [FIN, ACK]
7 0.001456 10.10.10.15 10.10.10.100 TCP 502 → 49152 [FIN, ACK]
8 0.001523 10.10.10.100 10.10.10.15 TCP 49152 → 502 [ACK]
9 5.230000 10.10.10.100 10.10.10.15 SNMP GetRequest sysDescr.0
10 5.230456 10.10.10.15 10.10.10.100 SNMP GetResponse
11 5.230789 10.10.10.15 10.10.10.100 UDP 9000 → 9000 Trap (越限)
12 30.000000 10.10.10.15 10.10.10.100 SNMP Trap (湿度越限)场景 2:端口冲突导致 Modbus 连接被拒
No. Time Source Destination Protocol Info
15 30.500000 10.10.10.100 10.10.10.15 TCP 49153 → 502 [SYN]
16 30.500218 10.10.10.15 10.10.10.100 TCP 502 → 49153 [RST, ACK]→ MCU TCP 连接池满(只有 2 个 PCB),被 SNMP/NMS 的 TCP 连接(如果有)或 Web 管理占用。
场景 3:SNMP 和私有 UDP 抢 pbuf
No. Time Source Destination Protocol Info
20 10.000000 10.10.10.100 10.10.10.15 SNMP GetBulkRequest
21 10.000100 10.10.10.15 10.10.10.100 TCP [ZeroWindow]
22 10.000200 10.10.10.15 10.10.10.100 UDP 9000 → 9000 Trap (丢失)→ LWIP pbuf 池耗尽,Modbus TCP 响应发不出(ZeroWindow),私有 UDP Trap 被丢弃。
症状:Modbus 间歇性 RST,SNMP 正常
原因:网关 A(新系统)+ 网关 B(旧 SCADA)+ Web 管理同时连 502
解决:限制 TCP PCB 数量,或旧系统改 RTU 轮询症状:SNMP Get 超时,私有 UDP Trap 丢包
原因:LWIP pbuf 池满,两个 UDP 协议栈互相阻塞
解决:调大 pbuf 池,或 SNMP 和私有 UDP 用不同端口但共享 PCB(需固件支持)症状:两个 Trap 同时发,NMS 只收到一个
原因:MCU 发送队列满,后到的 Trap 被丢弃
解决:Trap 合并策略,或优先级排序(SNMP Trap 优先)症状:传感器 MAC 被交换机封锁
原因:端口安全策略限制 MAC 数量(1 个),但传感器可能有多个 IP?
解决:检查传感器是否有多个 IP 或 IPv6 链路本地地址症状:Modbus 通,SNMP 不通
原因:交换机 ACL 只允许 502 端口,拦截 161/162
解决:调整 ACL 规则,放行 SNMP 端口// lwipopts.h
#define LWIP_TCP 1
#define LWIP_UDP 1
#define MEMP_NUM_TCP_PCB 2 // Modbus TCP 连接数
#define MEMP_NUM_UDP_PCB 4 // SNMP + Trap + 私有 UDP + DNS
#define PBUF_POOL_SIZE 32 // 内存池
#define PBUF_POOL_BUFSIZE 512 // 每 pbuf 大小
#define TCP_WND 2048 // TCP 窗口
#define TCP_SND_BUF 2048
#define TCP_MSS 256// 三个独立任务,不同优先级
void modbus_tcp_task(void *arg) {
// 处理 Modbus TCP 请求,优先级中
}
void snmp_task(void *arg) {
// 处理 SNMP Get/Set,优先级低
}
void udp_trap_task(void *arg) {
// 处理私有 UDP Trap,优先级高(事件驱动)
}发送优先级(从高到低):
1. UDP Trap(越限事件,最高优先)
2. Modbus TCP 响应(已建立的连接)
3. SNMP Trap(标准 Trap)
4. SNMP GetResponse(可延迟)
5. 私有 UDP 心跳(最低优先)import asyncio
from pymodbus.client import AsyncModbusTcpClient
from pysnmp.hlapi import *
import socket
import struct
# Modbus TCP 轮询
async def poll_modbus(ip, interval=30):
client = AsyncModbusTcpClient(ip, port=502)
await client.connect()
while True:
result = await client.read_input_registers(0, 5)
if not result.isError():
temp = result.registers[0] * 0.1
hum = result.registers[1] * 0.1
dew = result.registers[2] * 0.1
print(f"Modbus: T={temp}, H={hum}, D={dew}")
await asyncio.sleep(interval)
# SNMP 查询
async def poll_snmp(ip, interval=60):
while True:
for (errorIndication, errorStatus, errorIndex, varBinds) in getCmd(
SnmpEngine(),
CommunityData('public'),
UdpTransportTarget((ip, 161)),
ContextData(),
ObjectType(ObjectIdentity('1.3.6.1.2.1.1.1.0'))
):
if errorIndication:
print(f"SNMP error: {errorIndication}")
else:
for varBind in varBinds:
print(f"SNMP: {varBind}")
await asyncio.sleep(interval)
# UDP Trap 监听
async def udp_trap_listener(port=9000):
loop = asyncio.get_running_loop()
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('0.0.0.0', port))
sock.setblocking(False)
while True:
data, addr = await loop.sock_recvfrom(sock, 1024)
frame = parse_udp_frame(data)
if frame:
print(f"UDP Trap from {addr}: {frame}")
async def main():
tasks = [
asyncio.create_task(poll_modbus('10.10.10.15')),
asyncio.create_task(poll_snmp('10.10.10.15')),
asyncio.create_task(udp_trap_listener(9000)),
]
await asyncio.gather(*tasks)# 三协议数据统一写入 InfluxDB
def write_to_influx(protocol, sensor_id, measurement, fields, tags=None, ts=None):
point = build_line_protocol(
sensor_id=sensor_id,
measurement=measurement,
tags={**(tags or {}), 'protocol': protocol},
fields=fields,
ts=ts or int(time.time() * 1e9)
)
await write_influx(point)
# Modbus TCP 数据
write_to_influx('modbus_tcp', 'th-001', 'environment', {
'temperature': 22.6,
'humidity': 53.4,
'dew_point': 19.5,
})
# SNMP 数据
write_to_influx('snmp', 'th-001', 'environment', {
'sys_uptime': 123456,
'poe_voltage': 48.2,
})
# UDP Trap 数据
write_to_influx('udp_trap', 'th-001', 'alert_event', {
'temperature': 22.8,
'humidity': 65.2,
'alert_flags': 1,
})Modbus TCP、UDP、SNMP 三协议并行,不是简单的"三个端口同时监听",而是物理层共享、传输层分工、应用层互补的工程平衡。 TCP 管全量可靠,UDP 管事件秒级,SNMP 管标准管理,Wireshark 验证,InfluxDB 统一存储,边缘网关做时间对齐和去重——这才是高并发机房监控里经得起生产考验的三协议架构。
下一篇候选:
敬请关注!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。