首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Wireshark 抓包实战:以太网温湿度传感器 Modbus TCP 异常重传与连接复位排查

Wireshark 抓包实战:以太网温湿度传感器 Modbus TCP 异常重传与连接复位排查

原创
作者头像
盛世宏博小可
发布于 2026-09-22 11:49:28
发布于 2026-09-22 11:49:28
970
举报

Wireshark 抓包实战:以太网温湿度传感器 Modbus TCP 异常重传与连接复位排查

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

前面几篇把采集引擎、协议转换网关、Docker 化部署、双供电切换测试讲透了。这一篇下沉到链路层——现场最磨人的一类故障:通信时好时坏,日志里偶发超时,重启后恢复,过几天又犯。靠 ping 和换网线解决不了时,Wireshark 抓包是唯一能定位根因的手段。

一、先明确:重传和 RST 是两件事,别混用

  • TCP Retransmission:发送方没收到 ACK,触发重传。说明报文丢了、或 ACK 丢了、或对端接收了但应用没取走(TCP 层已确认,应用层无响应时不会表现为重传,而是请求堆积)。
  • TCP Dup ACK:接收方收到失序段,发重复确认,触发快速重传。
  • TCP Zero Window / Window Full:接收端缓冲满或发送端受对端通告窗口限制,不是复位。
  • RST (reset):某端 TCP 栈强制中止连接。可能来自传感器固件、交换机/防火墙状态表回收、NAT 超时、采集端 socket 已关闭后继续写。

排查时别急着下结论,先分清是谁发的 RST、在哪个阶段发的。

二、抓包前的准备

1. 抓包点

  • 优先在采集网关侧抓,或在管理型交换机上做 SPAN/端口镜像,把传感器下行口流量镜像到抓包主机。
  • 容器化部署时,别在容器内抓——宿主机 host 网络模式下直接在宿主网卡抓;bridge 模式下抓宿主 veth/br0。
  • 双端对照:有条件时在传感器侧同时抓,对齐时间戳和 Transaction ID,判断请求是否到达、响应是否发出。

2. 抓包命令

代码语言:javascript
复制
# 宿主侧,按传感器 IP 过滤存盘,循环分片
sudo tcpdump -i eth0 -nn -s0 -G 3600 -W 4 \
  -w /data/pcap/modbus_%Y%m%d_%H%M%S.pcap \
  "host 192.168.10.21 and tcp port 502"

Wireshark GUI 直接抓也行,捕获过滤器:

代码语言:javascript
复制
host 192.168.10.21 and tcp port 502

3. 采集端复现

用 Python/pymodbus 跑稳定轮询,故意制造边界条件:拔 PoE、关交换机端口、改防火墙规则、拉长空闲期。保留日志与抓包时间轴对齐。

三、Wireshark 显示过滤器速查

代码语言:javascript
复制
# 基本聚焦
ip.addr == 192.168.10.21 and tcp.port == 502

# 重传 / 乱序 / 零窗口
tcp.analysis.retransmission
tcp.analysis.duplicate_ack
tcp.analysis.zero_window_probe || tcp.zero_window || tcp.window_size == 0

# 连接复位与异常关闭
tcp.flags.reset == 1
tcp.flags.fin == 1
tcp.analysis.connection_reset  # 专家信息辅助

# Modbus 层
modbus
mbtcp.trans_id == 5
modbus.func_code == 3
modbus.exception_code != 0

# 按流跟踪
tcp.stream eq 12

注意:显示过滤器 tcp.port == 502 会匹配任一端端口为 502 的段;客户端临时端口在对端,别误判。建议跟进 tcp.stream 或 Follow → TCP Stream。

四、典型抓包序列与解读

场景 1:链路丢包导致重传,最终超时

代码语言:javascript
复制
C->S  [PSH,ACK] MBAP/TCP Read Holding Registers 40001 len=6
S->C  [ACK]
... 无响应数据
C->S  [Retransmission] 同上段
C->S  [Retransmission]
C->S  [Retransmission]
C  RTO 退避后放弃,应用层超时,关闭 socket
C->S  [FIN,ACK] 或 close() 后无 FIN 直接新连接

判定:

  • 传感器侧抓包若无请求到达 → 网络/ACL/VLAN/路由问题。
  • 传感器侧收到请求但无响应 → 固件处理阻塞、寄存器地址错、从站任务队列溢出。
  • 两侧都有请求响应,但响应分片迟到 → 接收端未重组,应用超时阈值过低。

场景 2:持久连接空闲后被中间件回收

代码语言:javascript
复制
C->S  [ACK] 空闲 300s
中间设备(防火墙/NAT/交换机状态检测)老化会话
C 下次轮询发 [PSH,ACK]
S 或中间设备回 [RST,ACK]
C 应用层报 ConnectionResetError / WinError 10054

判定要点:

  • RST 的 MAC/IP 不一定是传感器——镜像抓包看以太网层,若回包 MAC 是交换机/防火墙,状态表回收所致。
  • 传感器原连接仍在固件 socket 表,但中间设备已丢状态,回包被丢弃或改写。
  • 解决:启用 TCP Keepalive,或应用层心跳轮询,别依赖默认 socket 行为。

场景 3:传感器固件主动断旧连接

代码语言:javascript
复制
C 持长连接轮询
S 固件会话超时(如 60–180s 无活动)或最大连接数限制
S->C [RST,ACK] 或 [FIN,ACK]
C pymodbus 复用旧 socket 继续写 -> EPIPE/Broken pipe/10054

判定:

  • RST 来自传感器 MAC、传感器 IP。
  • 多采集端/多客户端共用一台传感器,连接槽占满,新连接被拒或旧连接被踢。
  • pymodbus 未检测对端关闭,缓冲中仍认为 connected。

场景 4:半关闭 / CLOSE_WAIT

代码语言:javascript
复制
S->C [FIN,ACK] 固件重启或任务复位
C TCP 栈回 [ACK],进入 CLOSE_WAIT,应用未调用 close()
后续轮询继续在旧 fd 上 send(),返回错误或未报错但丢弃
新轮询逻辑新建连接,出现 TIME_WAIT 堆积

判定:ss -tno | grep 502、宿主机 netstat 看状态;Python 侧捕获异常后未销毁重建。

五、Wireshark 落地分析步骤

  1. 确认握手与连接生命周期
    • 过滤 tcp.stream,看每条流建立、保持、终止。
    • 关注是否每次轮询新建连接(connection-per-request)还是复用长连接。前者在抓包中表现为大量 SYN/FIN,后者表现为单流内持续请求。
  2. 看 MBAP 事务配对
    • Modbus TCP 头:Transaction ID (2B)、Protocol ID=0、Length、Unit ID。
    • Wireshark 列视图加自定义列:mbtcp.trans_id、mbtcp.unit_id、modbus.func_code。
    • 请求与响应 Transaction ID 应一致;不一致或响应 TID 错位,说明固件/网关代理/多客户端竞争。
  3. 看响应时间与异常码
    • 自定义列 frame.time_delta_displayed 或 I/O Graph,画请求→响应间隔。
    • 异常响应:功能码高位置 1(0x83/0x84/0x86),异常码 01/02/03/06/0B。
  4. 看 TCP 层指标
    • Statistics → Conversations → TCP:bytes、duration、retransmissions。
    • Statistics → TCP Stream Graphs → Round Trip Time / Throughput。
    • Expert Info(左下角):扫描 warnings/errors,定位重传簇、zero window、keep-alive。
  5. 双端对齐
    • 采集端 pcap + 传感器侧 pcap,按时间戳和 TID 对齐。请求离开采集端但未到传感器侧 → 网络路径;到传感器侧但无响应 → 设备侧;响应发出但未回采集端 → 回程/路由/ACL。

六、Python 侧健壮化:让故障可恢复,别把 RST 传成静默坏数据

pymodbus 3.x 异步客户端,关键在连接状态管理、失败后退避重建、不在旧 socket 上继续发。

代码语言:javascript
复制
import asyncio, logging, time
from pymodbus.client import AsyncModbusTcpClient
from pymodbus.exceptions import ModbusIOException, ConnectionException

log = logging.getLogger("modbus_poller")

class Poller:
    def __init__(self, host, port=502, slave=1, poll=5.0, timeout=2.0):
        self.host, self.port, self.slave = host, port, slave
        self.poll, self.timeout = poll, timeout
        self.client = None
        self._conn_lock = asyncio.Lock()

    async def _ensure_conn(self):
        # 惰性建连 + 健康探测
        if self.client is None or not self.client.connected:
            async with self._conn_lock:
                if self.client is not None:
                    try: self.client.close()
                    except Exception: pass
                self.client = AsyncModbusTcpClient(
                    self.host, port=self.port, timeout=self.timeout,
                    retry_on_empty=True, retries=2,
                )
                # pymodbus 3.x 走 asyncio 事件循环
                ok = await self.client.connect()
                if not ok:
                    raise ConnectionException("connect failed")
                # 可选:设置 sockopt keepalive(见下)

    async def read(self):
        for attempt in range(3):
            try:
                await self._ensure_conn()
                resp = await self.client.read_holding_registers(0, count=2, slave=self.slave)
                # pymodbus: resp None / isError / 异常响应都要处理
                if resp is None or resp.isError():
                    raise ModbusIOException(f"bad resp: {resp}")
                temp = resp.registers[0] * 0.1
                humid = resp.registers[1] * 0.1
                return {"temp": temp, "humid": humid, "quality": 0}
            except (ConnectionException, ModbusIOException, OSError) as e:
                log.warning("poll fail attempt=%d: %s", attempt, e)
                # 强制销毁,下一轮重建
                try: self.client.close()
                except Exception: pass
                self.client = None
                await asyncio.sleep(min(2 ** attempt, 8))   # 退避,别自旋
        return {"quality": 2}

    async def run(self):
        while True:
            t0 = time.monotonic()
            res = await self.read()
            # 上报/缓存/补传逻辑略
            dt = time.monotonic() - t0
            await asyncio.sleep(max(0.0, self.poll - dt))

注意点:

  • AsyncModbusTcpClient.connect() 在 pymodbus 3.x 返回 bool/协程行为随版本变化,务必按所用版本验证;包装 try/except,失败后置 None。
  • 不要在异常分支继续复用旧 client 对象,显式 close + 新建。
  • 长连接保活:pymodbus 暴露底层 transport/socket 有限,更稳妥的是应用层心跳——每轮询周期读一次存活寄存器,空闲时兜底轮询,使会话不老化。
  • 多设备并发:每设备独立连接或连接池,别在单 socket 上交错发请求导致 TID 竞争;如需并发,用 asyncio.gather 但各自持有连接。

设置 TCP Keepalive(在自建 socket 时更可控,下面给参考):

代码语言:javascript
复制
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
# Linux
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 30)
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10)
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3)

pymodbus 未暴露该钩子时,用应用层心跳更可移植。

七、现场根因清单(按出现频率)

抓包证据

根因

处置

重传簇 + CRC/PHY 错误

网线/水晶头/屏蔽/EMI/超距

换六类屏蔽线、独立桥架、PoE 分离供电,固定双工 100M/1G 全双工

RST 来自防火墙 MAC

状态表老化、NAT 超时

长连接保活、静态会话、白名单,或改为受控重连

RST 来自传感器 IP,多客户端

固件连接数限制/会话超时

单主采集、收敛客户端数、固件升级、延长固件超时

FIN 后 CLOSE_WAIT 在采集端

应用未关 socket

异常路径显式 close + 重建,别缓存旧对象

Dup ACK + 分片迟到

固件 TCP 发送缓冲/延迟分片

提高应用超时、批量读寄存器、升级固件

TID 错配

并发请求/旧响应到达

串行化或每连接隔离,校验 TID 匹配

UDP 旁路通道未处理

部分设备报警用 UDP 推,采集端只轮询 TCP

另起 UDP 接收线程,合并事件总线,别混用连接模型

PoE 端口节能关闭

交换机节能策略

静态 PoE、禁止端口休眠、上行链路监测

补充:标题里带 UDP,现场别忽略——部分网口变送器除 Modbus TCP 轮询外,还走 UDP 主动上报报警/心跳。抓包时一并过滤 udp port <n>,确认上报目标 IP/端口、是否需要响应,避免边缘网关只听 TCP 而漏事件。

八、验证闭环

排障后做回归:

  1. 连续运行 24h,采集侧记录每次重连、超时、质量位;Wireshark/I/O Graph 确认无重传增长。
  2. 注入故障:断 PoE 5s/30s、断网 4h、并发轮询压力,验证自愈与补传。
  3. InfluxDB 侧核对采样连续性:原始 5s/1s 采集,聚合层保留 count/越限次数,中断窗口 quality=bad 标记,非均值平滑掩盖。
  4. 腾讯云侧:边缘网关断网自治,恢复后增量同步,下行控制指令走独立通道,不经由异步解耦队列回写。

九、常见返工点

  • 只在笔记本直连时抓包,现场经过交换机/VLAN 未镜像,误判为设备问题——抓生产路径。
  • 捕获过滤器写错,抓不到回程;显示过滤器过度过滤,漏看 RST/FIN。
  • 用 Follow TCP Stream 看明文时,Modbus 多事务流拼接误导,按 message 边界和 TID 核对。
  • pymodbus 异常分支未销毁连接,重连风暴打满固件连接表,交换机端口阻塞。
  • 把固件踢旧连接当网络闪断,未收敛采集端连接模型。
  • 调大超时掩盖问题,未消除根因,验收时复现。

十、一句话总结

抓包的价值不在"看到重传",而在还原连接生命周期:请求是否发出、是否到达、响应是否返回、谁先关闭、RST 来自哪一层。定位后,修复在采集端连接管理 + 网络路径加固 + 固件/部署约束三方协同,而不是换网线碰运气。

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

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

目录
  • Wireshark 抓包实战:以太网温湿度传感器 Modbus TCP 异常重传与连接复位排查
  • 物联网 #Modbus #TCP/IP #UDP #POE供电 #腾讯云 #Wireshark #Python #InfluxDB #以太网温湿度传感器 #网口温湿度变送器 #机房监控
    • 一、先明确:重传和 RST 是两件事,别混用
    • 二、抓包前的准备
      • 1. 抓包点
      • 2. 抓包命令
      • 3. 采集端复现
    • 三、Wireshark 显示过滤器速查
    • 四、典型抓包序列与解读
      • 场景 1:链路丢包导致重传,最终超时
      • 场景 2:持久连接空闲后被中间件回收
      • 场景 3:传感器固件主动断旧连接
      • 场景 4:半关闭 / CLOSE_WAIT
    • 五、Wireshark 落地分析步骤
    • 六、Python 侧健壮化:让故障可恢复,别把 RST 传成静默坏数据
    • 七、现场根因清单(按出现频率)
    • 八、验证闭环
    • 九、常见返工点
    • 十、一句话总结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档