首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Modbus TCP 与 UDP 在以太网温湿度变送器中的共存设计:报文结构对比与抓包分析

Modbus TCP 与 UDP 在以太网温湿度变送器中的共存设计:报文结构对比与抓包分析

原创
作者头像
盛世宏博科技
发布于 2026-09-22 11:07:33
发布于 2026-09-22 11:07:33
880
举报

Modbus TCP 与 UDP 在以太网温湿度变送器中的共存设计:报文结构对比与抓包分析

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


一、为什么要在同一台变送器里同时跑两个协议

先说清楚场景:这不是"二选一"的纠结,而是物理层复用、传输层分工。

代码语言:javascript
复制
以太网温湿度变送器(W5500 / LAN8720 + STM32F407)
  ├── Modbus TCP Server(端口 502)
  │     用途:网关周期轮询,拉取全量寄存器
  │     特点:可靠、有序、有连接状态
  │
  └── UDP Server(端口 161 / 自定义 9000)
        用途:SNMP Trap 推送 / 私有协议广播
        特点:无连接、低开销、事件触发秒级上报

共存不是重复造轮子,而是各干各的活:

  • Modbus TCP 管"全量基线"——温度、湿度、露点、状态、质量戳,30s/60s 一轮
  • UDP 管"事件快报"——越限瞬间、探头断开、设备重启,不等轮询,毫秒级推送

两个协议跑在同一颗 MCU、同一块 PHY 芯片、同一个 IP 地址上,共享 MAC 层发送队列。这才是"共存设计"要解决的真正问题。


二、报文结构对比:从 MAC 头到应用数据

用 Wireshark 抓同一台 POE 温湿度变送器(IP: 10.10.10.15)的两种报文,逐层拆开看。

2.1 MAC 层(完全相同)

代码语言:javascript
复制
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 层开始。

2.2 IP 层(几乎相同,TTL 和 Checksum 不同)

代码语言:javascript
复制
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 (网关)

2.3 传输层:TCP vs UDP(核心差异)

Modbus TCP(端口 502):

代码语言:javascript
复制
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 permitted

UDP(端口 9000,私有协议):

代码语言:javascript
复制
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)

应用层自己做

2.4 应用层数据

Modbus TCP ADU(应用数据单元):

代码语言:javascript
复制
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 → 质量 OK

UDP 私有协议(自定义帧):

代码语言:javascript
复制
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 来配对和去重。


三、Wireshark 抓包实战

3.1 抓包命令

代码语言:javascript
复制
# 在边缘网关上抓指定传感器的双向流量
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

3.2 Wireshark 显示过滤器

代码语言:javascript
复制
# 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

3.3 典型抓包场景分析

场景 1:正常轮询(Modbus TCP)

代码语言:javascript
复制
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 越限推送

代码语言:javascript
复制
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 连接被重置(真实翻车)

代码语言:javascript
复制
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 连接),拒绝新连接。这就是前面几篇提到的"三机抢资源"的底层原因。


四、MCU 侧共存设计(STM32F407 + LWIP)

4.1 LWIP 协议栈配置

代码语言:javascript
复制
// 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 都分配不到内存,连接直接失败。

4.2 接收线程分离

代码语言:javascript
复制
// 两个独立任务,不同优先级
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);
    }
}

4.3 发送队列优先级

代码语言:javascript
复制
发送优先级(从高到低):
  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)

五、Python 侧双协议并发采集

代码语言:javascript
复制
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 时序存储:双通道数据如何存

代码语言:javascript
复制
# 写入 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)

七、共存设计的 5 个关键经验

  1. 内存池隔离:LWIP 的 pbuf 池要够大,或者给 UDP Trap 开独立 DMA 缓冲区,防止 TCP 和 UDP 互抢内存导致连接重置。
  2. 发送带宽仲裁:UDP 广播/ Trap 不能无限制发包。设速率限制(如每秒最多 10 个 Trap 帧),避免 UDP 风暴把交换机端口堵死,间接影响 Modbus TCP。
  3. TCP 连接数限制:低端 MCU 的 Modbus Server 通常只支持 1–2 个并发 TCP 连接。网关侧要做好连接池管理,不要同时开多个连接。
  4. Wireshark 是你的眼睛:上线前必做抓包验证——确认 TCP 三次握手正常、Transaction ID 不重复、UDP 帧 CRC 正确、没有 RST 包。
  5. 时间一致性:UDP Trap 里的 collect_ts 是传感器本地时间。网关侧要做时间偏移校准(NTP 校时后计算偏移量),否则补传数据时间错乱。

八、一句话总结

Modbus TCP 和 UDP 在同一台以太网温湿度变送器里共存,不是简单的"两个端口同时监听",而是 MAC 层共享、传输层分工、应用层互补的工程平衡。​ TCP 管全量可靠,UDP 管事件秒级,Wireshark 验证,InfluxDB 统一存储,边缘网关做时间对齐和去重——这才是高并发机房监控里经得起生产考验的双协议架构。

物联网 #Modbus #TCP/IP #UDP #POE供电 #腾讯云 #Wireshark #Python #InfluxDB #以太网温湿度传感器 #网口温湿度变送器 #机房监控


下一篇候选:

  1. 《LWIP 协议栈调优实战:pbuf 池大小、TCP 窗口与 UDP 缓冲区在温湿度变送器中的参数配置》
  2. 《InfluxDB 时序数据去重与补传合并:双通道数据在云端如何按采集时间对齐》
  3. 《Wireshark 抓包排错手册:Modbus TCP 连接重置、UDP 丢包、交换机风暴的现场定位方法》

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • Modbus TCP 与 UDP 在以太网温湿度变送器中的共存设计:报文结构对比与抓包分析
    • 一、为什么要在同一台变送器里同时跑两个协议
    • 二、报文结构对比:从 MAC 头到应用数据
      • 2.1 MAC 层(完全相同)
      • 2.2 IP 层(几乎相同,TTL 和 Checksum 不同)
      • 2.3 传输层:TCP vs UDP(核心差异)
      • 2.4 应用层数据
    • 三、Wireshark 抓包实战
      • 3.1 抓包命令
      • 3.2 Wireshark 显示过滤器
      • 3.3 典型抓包场景分析
    • 四、MCU 侧共存设计(STM32F407 + LWIP)
      • 4.1 LWIP 协议栈配置
      • 4.2 接收线程分离
      • 4.3 发送队列优先级
    • 五、Python 侧双协议并发采集
    • 六、InfluxDB 时序存储:双通道数据如何存
    • 七、共存设计的 5 个关键经验
    • 八、一句话总结
  • 物联网 #Modbus #TCP/IP #UDP #POE供电 #腾讯云 #Wireshark #Python #InfluxDB #以太网温湿度传感器 #网口温湿度变送器 #机房监控
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档