做物联网通信后台的哥们,应该都有过这种经历:设备端报“连接被关闭”,服务端日志却什么异常都没有,两边各说各话,就是找不到问题在哪。我早期排查 MQTT 问题主要靠看代码和打日志,后来一次本地联调,客户端日志只说 DISCONNECTED,broker 也没记录到主动断开,卡了一下午。最后用 Wireshark 抓回环口,一眼就看到 PINGREQ 和 PINGRESP 的间隔已经超过 keepalive,broker 判定客户端失联,直接踢掉了连接。从那以后,Wireshark 就成了我分析 MQTT 通信流程的常规工具。
这篇文章把整套流程完整梳理一遍:从环境搭建、抓包过滤器配置,到 CONNECT、PUBLISH、SUBSCRIBE 这些核心报文的逐条拆解,再到掉线、丢消息这类典型问题怎么通过抓包定位。内容偏实操,适合刚接触协议分析的开发者,也适合想深入理解 MQTT 行为细节的技术人员。你照着走一遍,就能自己搭出一个小型测试环境,把 MQTT 通信的每个环节看得明明白白。
1. 抓包前必须补齐的 MQTT 与 Wireshark 基础知识
1.1 一句话理解 MQTT 的发布订阅模型
MQTT 采用发布/订阅模型,通信双方不是点对点直连,而是通过一个 broker(消息代理)中转数据。生产数据的客户端向某个主题(Topic)发布消息,需要接收数据的客户端订阅对应主题,broker 根据主题匹配关系完成转发。
打个比方,broker 就是邮局,主题就是地址。发件人把信投到邮局,邮局根据收件地址分发,订了同一地址的收件人就能拿到信。发布者、订阅者彼此不需要知道对方 IP,也不用同时在线,这也是 MQTT 适合物联网场景的核心原因。分析抓包时也要带着这个模型去看:客户端之间的消息不会直接到达对方网卡,你看到的是“客户端A到 broker”和“broker 到客户端B”两段独立的报文。
1.2 MQTT 报文的通用结构与协议层次
MQTT 跑在 TCP 之上,明文端口默认 1883,TLS 加密端口默认 8883。TCP 负责可靠传输和分段重组,MQTT 在业务层面定义自己的报文格式。Wireshark 抓到的本质上是 TCP 包,能识别为 MQTT,是因为它在 TCP 负载里看到了符合 MQTT 报文格式的头部特征,并且端口符合默认规则。
MQTT 报文由三部分构成:固定报头、可变报头、负载。固定报头第一个字节的高 4 位是报文类型,低 4 位是各类标志位(比如 PUBLISH 报文的 DUP、QoS、Retain 标志)。整个报文类型对照表新手值得常备:
| 报文类型 | 数值 | 中文含义 | 方向 |
|---|---|---|---|
| CONNECT | 1 | 客户端请求连接 | 客户端到 broker |
| CONNACK | 2 | 连接确认 | broker 到客户端 |
| PUBLISH | 3 | 发布消息 | 双向 |
| PUBACK | 4 | QoS 1 消息确认 | 双向 |
| PUBREC | 5 | QoS 2 第一阶段确认 | 双向 |
| PUBREL | 6 | QoS 2 第二阶段释放 | 双向 |
| PUBCOMP | 7 | QoS 2 完成确认 | 双向 |
| SUBSCRIBE | 8 | 订阅主题 | 客户端到 broker |
| SUBACK | 9 | 订阅确认 | broker 到客户端 |
| PINGREQ | 12 | 心跳请求 | 客户端到 broker |
| PINGRESP | 13 | 心跳响应 | broker 到客户端 |
| DISCONNECT | 14 | 断开连接 | 客户端到 broker |
固定报头里还有个“剩余长度”字段,它采用变长编码,最多 4 个字节,用于表示后面可变报头和负载的总长度。Wireshark 会把这一层自动解析好,你展开报文详情面板就能直接看到 Length、Message Type 这些字段,不用手工去算偏移量。
1.3 Wireshark 分析 MQTT 的核心价值与客观局限
Wireshark 分析 MQTT 的优势在于它能同时看到连接两端的行为。客户端日志只记录自己发出去和收到的东西,broker 日志也只记录服务端视角的消息,两边一对不上就抓瞎。抓包拿到的是一份“上帝视角”的完整记录,时间戳精确到微秒,还能结合 TCP 层信息,把应用层确认与传输层重传放在一起看。
客观局限也要说清楚。如果 MQTT 走的是 8883 端口并且启用了 TLS 加密,Wireshark 默认只能看到 TLS 握手和密文,看不到明文 payload。想要在加密场景下分析,需要额外配置会话密钥。这个后文展开讲。还有一点,如果 broker 部署在你本机之外,比如在远端服务器上,你只能在客户端一侧抓到发往服务器的包,broker 往其他订阅者转发的行为就看不到了,必要时得在 broker 侧抓包或使用端口镜像。
2. 分析环境搭建与抓包过滤器设置
2.1 本地测试环境怎么快速搭起来
分析 MQTT 通信流程,强烈建议先在本地搭一个完全可控的测试环境。一个 broker、一个发布客户端、一个订阅客户端,就够了。broker 用开源的 MQTT broker 程序即可,这类软件很多,选你熟悉的、能跑在本地系统上的就行。默认配置下它监听 1883 端口,不需要额外改配置就可以直接联调。
客户端工具有两类。图形化的调试工具(如 MQTTX)适合边看界面边操作,新手比较容易上手;命令行形式的订阅/发布工具(如 mosquitto_pub、mosquitto_sub)适合写脚本批量自动化。我这里最常用的是图形工具加命令行组合:订阅端用图形工具保持连接,发布端用命令行脚本反复发不同主题、不同 QoS 的消息,抓包数据会非常丰富。
启动 broker 后,先做一次连通性验证。订阅端订阅 test 主题,发布端向 test 主题发一条 hello。如果订阅端能收到,说明 broker、客户端、网络链路都是通的,再开始抓包。我建议把 broker 和客户端都跑在本机,抓 loopback 回环接口。这样不会有局域网防火墙、路由器 NAT 之类的干扰因素,定位问题最干净。
2.2 选择抓包接口与基础过滤器的要点
本机测试抓的是回环接口。Wireshark 默认可能不显示回环接口,需要在捕获接口列表里勾选“显示环回接口”之类的选项,不同版本的菜单位置略有差异,找不到就搜一下版本对应界面。如果 broker 跑在局域网另一台机器上,就选本机实际出网的网卡,可以是以太网接口或无线接口。
抓包前设置好抓包过滤器(Capture Filter),只保留和 MQTT 相关的流量。语法是 tcp port 1883,运行在环回口时再叠加 host 条件也可以,如 ip.addr 需要放在显示过滤器里,抓包过滤器阶段用 tcp port 1883 就足够。加上这个过滤条件,整个抓包文件干净很多,不会有一堆 DNS、广播背景流量干扰。
显示过滤器(Display Filter)落实到工具栏的绿框里,经常组合使用的有这几条:
- mqtt:只看被识别为 MQTT 的包
- mqtt && tcp.port == 1883:限定端口
- mqtt.topic == "test/topic":只看指定主题
- mqtt.qos == 1:只看 QoS 1 的报文
- mqtt.msgtype == 3:只看 PUBLISH 报文
这里注意区分抓包过滤器和显示过滤器。一个在抓包阶段把否丢弃不需要的包,一个只在显示阶段隐藏不相关的包。新手常混为一谈,实际两者语法不同、作用时机不同,抓包过滤器写错会造成包根本进不来,显示过滤器写错只是看不见。
2.3 让 Wireshark 正确识别 MQTT 报文的关键设置
Wireshark 默认根据 TCP 端口识别应用层协议,如果你的 broker 跑在标准 1883 端口,协议列会自动显示 MQTT。识别成功的标志是协议列里明确写着 MQTT,而不是 TCP。如果只显示 TCP,说明解析器没有启用或者端口不在默认规则里。
非标准端口场景下,需要手动指定解码。具体操作是右键任意一条 TCP 报文,选择 Decode As,在弹出窗口的 Current 列把默认协议改成 MQTT。设置完以后,这条 TCP 连接上的后续报文都会按 MQTT 解析。这个操作对临时排查很有用,比如客户端把 MQTT 端口改成了 2883,没有这个步骤就只能看到一堆裸 TCP 数据。
还有一个常见情况需要注意:同一条 TCP 连接上,前半段报文正常显示 MQTT,中间某几段突然显示 TCP。这通常不是解析器抽风,而是多个 MQTT 报文被合并到了同一个 TCP 分段里,或者一个 MQTT 报文被拆分成了多个 TCP 分片。Wireshark 大多数时候能自动重组,少数情况需要右键选择 Follow TCP Stream 从完整字节流里确认内容。
3. 完整通信流程的核心报文逐条拆解
3.1 连接建立阶段:CONNECT 与 CONNACK
客户端和 broker 建立 TCP 连接之后,第一对 MQTT 报文就是 CONNECT 和 CONNACK。抓包里定位起来很直观:源端口是客户端高位随机端口,目标端口是 1883,协议列显示 MQTT。展开 CONNECT 报文,重点看 Client ID、Clean Session、Keep Alive、User Name、Password 这几个字段。
以我调试时抓的一段报文为例,固定报头第一字节是 0x10,表示 CONNECT,剩余长度后面跟着协议名、协议级别、连接标志、保活时间。Wireshark 详情面板会完整展开这些字段:
Message Type: CONNECT (1) Protocol Name: MQTT Protocol Version: 4 Client ID: demo_client Keep Alive: 60 Clean Session: 1CONNACK 报文相对简单,核心看两个字段。Session Present 标识 broker 之前是否保留了该客户端的会话,为 1 说明可以接着用之前的订阅状态;Return Code 是连接结果,0 代表成功,其他值分别代表协议版本不支持、标识符被拒绝、服务器不可用、认证失败等。调试的时候如果 Return Code 不是 0,优先去查用户名密码和客户端标识符。
3.2 心跳保活阶段:PINGREQ 与 PINGRESP
连接建立后,如果一段时间内没有其他报文往来,客户端会在 keepalive 间隔内发送 PINGREQ,broker 收到后回复 PINGRESP。这是应用层的心跳机制,作用在于探测连接是否仍然有效。
为什么有 TCP 的保活机制还要 MQTT 自己做心跳?因为 TCP 层 Keep-Alive 默认间隔太长,而且代理、负载均衡、NAT 设备可能随时静默丢弃连接,TCP 半开连接很难被及时发现。MQTT 的应用层心跳让双方都能主动感知对方是否“活着”,这是分析掉线问题时必须理解的一条原理。
抓包中看到的状态序列大概是这样:
- 客户端发 PINGREQ,Wireshark 信息栏显示 Ping Request
- broker 回 PINGRESP,信息栏显示 Ping Response
- 两者时间差通常在几毫秒到几十毫秒
如果抓包发现 PINGREQ 发出去后迟迟没有 PINGRESP,然后 TCP 层出现重传或者 RST,说明链路要么半开、要么 broker 已经卡死。如果 PINGREQ 从来没有发出去,说明客户端的心跳线程出了问题。这两种现象指向完全不同的排查方向,靠日志很难区分,抓包一眼就能定位。
3.3 消息发布阶段:PUBLISH 与 PUBACK
PUBLISH 是承载业务数据的核心报文。展开后能看到 Topic、Payload、QoS、Retain、Message Identifier 这些关键字段。QoS 0 的场景最简单,PUBLISH 发出后没有任何确认报文,broker 尽力转发,不保证送达。高频传感器数据经常用 QoS 0,丢了可以接受,或者由应用层的周期上报来兜底。
QoS 1 引入确认机制,流程是发送方发 PUBLISH,接收方回 PUBACK,发送方收到 PUBACK 认为消息送达。这个“至少一次”语义意味着接收方可能收到重复消息,因为发送方在超时未收到 PUBACK 时会使用相同 Message Identifier 重发。抓包中辨识重发很直观:两条 PUBLISH 除了时间戳,其余字段几乎完全相同,DUP 标志位置为 1。
实际示例我贴一个典型序列:
- 发布端向主题 temp/room1 发布一条 JSON 数据
- 报文详情中 QoS 显示为 1,Message Identifier 为 12345
- broker 广播完这条消息给订阅者后,向发布端回一条 PUBACK
- PUBACK 中的 Message Identifier 也是 12345
如果抓包中只有 PUBLISH 没有 PUBACK,并且发布端持续重发,优先怀疑 broker 的处理线程或订阅者消费能力出现问题,而不是单纯看应用日志。
3.4 QoS 2 的四步握手过程
QoS 2 的流程比 QoS 1 复杂得多,要保证“恰好一次”。抓包中的报文序列是 PUBLISH、PUBREC、PUBREL、PUBCOMP,四条报文构成一个完整事务。理解这套握手,关键是明白每一跳的目的:PUBREC 表示接收方已经收到消息并记录消息标识;PUBREL 表示发送方确认收到 PUBREC,允许接收方释放消息;PUBCOMP 表示接收方完成全部处理。
只看名字容易绕晕,我当初也是对着抓包一点点捋清的。实际展开观察时要注意,四条报文的 Message Identifier 必须一致,整个 QUORUM 期间 broker 和客户端各自维护状态,如果中途丢了一环,双方会按规则重发对应报文。Wireshark 的时间戳列在这里很好用,你直接看 PUBLISH 到 PUBREC、PUBREC 到 PUBREL 之间的耗时,就能判断是哪一端响应慢。
为什么很少见有人用 QoS 2?因为四步握手的开销比 QoS 1 大一倍,多数物联网场景下 QoS 1 加应用层幂等处理已经足够。但高频金融支付、设备控制命令这类场景,重复执行会造成业务事故,就必须用 QoS 2。理解归理解,生产环境选型还是看业务容忍度。
3.5 订阅阶段:SUBSCRIBE 与 SUBACK
订阅行为本身也是一对报文。客户端发 SUBSCRIBE,主题过滤器字段里可以写出多个主题,比如 device/+/status 这种通配符写法,每个主题带一个请求的 QoS。broker 处理完回 SUBACK,里面是主题过滤器数组对应的返回码列表。
返回码 0、1、2 分别表示 QoS 0、1、2 被授予,0x80 表示失败。抓包里经常出现的坑是:客户端订阅时报错,但报错信息里不写具体是哪个主题失败。展开 SUBACK 一看,两个请求 QoS 的主题一个成功一个 0x80,问题立刻收敛到那个失败主题的合法性或者权限上。
还有一条重要规则要在抓包前明确:实际收到的消息 QoS 级别等于发布时 QoS 和订阅时 QoS 的较小值。发布端用 QoS 2 发消息,订阅端用 QoS 0 订阅,实际收到的消息就是 QoS 0,抓包里出现的行为也按 QoS 0 理解,不会看到 PUBACK。这个规则很多入门资料提过,但真正抓包时才会发现错位有多让人困惑。
3.6 断开连接:主动 DISCONNECT 与异常离线
正常结束连接时,客户端发 DISCONNECT,broker 收到后关闭 TCP 连接,不再为该客户端转发消息。如果设备在发送 DISCONNECT 前网络突然中断,抓包里就不会出现这个报文,broker 只能等 keepalive 超时后把设备标记为离线。
这两种离线方式在抓包里的区分非常明显。主动断开后 TCP 层是正常的 FIN 挥手序列,异常离线则很可能是 TCP 重传数次后无响应,随后 broker 发出 FIN 或者 RST。如果你在设备端配了遗嘱消息(Will Message),可以在 CONNECT 报文里看到 Will Topic 和 Will Message 字段。broker 在客户端异常离线时会代发遗嘱消息,而正常 DISCONNECT 则不会触发。想验证这个机制,抓包时故意断掉客户端的有线网络或者杀掉客户端进程,观察 broker 是否发出遗嘱消息,就能把理论和对包行为连起来。
4. 常见连接故障的抓包定位方法
4.1 客户端频繁掉线的定位套路
掉线问题最常见,也最容易被日志误导。客户端日志往往只写了 reconnect,broker 日志也只记录 TCP 断开,具体为什么断开没有下文。抓包后的第一步是找到最后一次收发的时间点,然后把时间显示切换成相对上一包模式,看掉线前最后几条报文是什么。
我遇到过不止一次的场景:客户端 CONNECT 之后一切正常,中间有数据交互,隔一段时间发 PINGREQ,broker 回 PINGRESP,再过一段时间,TCP 层出现 FIN。对照 keepalive 配置计算一下,从最后一次 PINGREQ 到 FIN 之间的间隔如果超过 keepalive 的两倍,基本可以断定 broker 等心跳等空了,主动断线。这类问题根源多半是客户端心跳线程被阻塞,或者系统休眠导致 socket 长时间没有读写。
另一种掉线现象是 TCP 层出现大量重传,然后直接 RST。这通常是网络本身不稳定,或者中间设备防火墙硬超时。定位时加一个过滤器 mqtt && (tcp.flags.reset == 1 || tcp.flags.fin == 1),把断开相关报文单独筛出来看,比在几百个包里翻效率高得多。
4.2 消息“丢了”但实际没丢的排查顺序
订阅端没收到消息,第一直觉是 broker 丢消息,但抓包结果往往打脸。排查这个问题一定要把发布端、broker、订阅端三段都纳入视野。先看发布端有没有真正发出 PUBLISH;再看 broker 有没有收到并回 PUBACK;最后看 broker 是否向订阅端转发了 PUBLISH。这三段在抓包里对应不同 IP 对的流量,需要区分开。
老司机都知道优先检查订阅时机和订阅主题。客户端先发布了一条 QoS 0 消息,之后才发起 SUBSCRIBE,而主题上又没有保留消息(Retained Message),订阅端收不到是正常行为,不叫丢消息。抓包中 SUBSCRIBE 的时间戳晚于 PUBLISH,问题一目了然。
还有一类“丢消息”是订阅端其实收到了,但是 QoS 0 消息在某个中转环节被静默丢弃。抓包中可以看到 broker 发出 PUBLISH,但订阅端 TCP 层出现重传,重传几次后 broker 放弃。这个现象说明订阅端网络接收能力出了问题,或者 socket 缓冲区满了。别急着甩锅给 broker。
4.3 MQTT 报文完全看不到怎么办
抓完包一张 MQTT 报文都没有,不用慌,按下面四点排查基本能解决。第一,确认端口与解码方式,非 1883 端口需要手动 Decode As;第二,确认显示过滤器语法是否正确,mqtt 过滤不出结果时先清掉所有过滤器,看协议列手机会不会显示 MQTT;第三,确认抓包时机,订阅发布操作和暂停抓包之间有时间差,注意先点开始捕获再做业务动作;第四,确认客户端工具不会把所有流量都加密,有些 SDK 强制 TLS,明文 MQTT 解码当然看不到业务数据。
加密场景有个通用解法。Wireshark 支持 TLS 会话密钥导出,客户端所在的系统里设置 SSLKEYLOGFILE 环境变量,然后在 Wireshark 的首选项中选择 TLS 协议,指定这个密钥文件路径。有密钥之后,8883 端口上的 TLS 数据也能解密成明文 MQTT 报文。测试环境这么做非常方便,生产环境出于安全考虑一般不导出密钥,但掌握了这个方法,本地复现问题时可以省很多事。
4.4 用 Follow TCP Stream 还原完整会话
TCP 分段和重组是定位疑难问题的一大障碍。一个 MQTT 报文可能被拆成两个 TCP 包,也可能多个 MQTT 报文合并在一个 TCP 包里。逐条看零散包,拼不出完整故事。这时右键任意一条 MQTT 报文,选择 Follow,再选 TCP Stream,Wireshark 会把这条 TCP 连接上的全部负载按顺序拼接出来,以原始字节流形式展示。
字节流窗口默认用 ASCII 显示负载,如果业务数据是二进制或者压缩后的内容,窗口里会出现乱码,这是正常现象,切换到 Hex Dump 模式就能看到十六进制数据。利用这个功能,你可以完整核对一条 MQTT 连接里从 CONNECT 到 DISCONNECT 的全部业务交互,对测试客户端行为是否规范特别有用。
5. 把报文看“厚”:统计、时间与留存经验
5.1 用统计功能观察消息速率与连接用量
Wireshark 不只是看单条报文的放大镜,它自带的统计功能适合观察整体模式。Statistics 菜单下的 Conversations 可以按 TCP 连接聚合流量,看哪条连接传输量最大、哪些连接只发不收。IO Graph 更直观,按时间轴画出报文数量曲线,业务压测时能直接看出消息是否突发、是否有周期性空档。
实际排障时,IO Graph 对于发现消息风暴特别有效。比如某主题的发布频率突然从每秒 1 条变成每秒几百条,靠逐条数是数不过来的,曲线上一眼就能看出来。再配合时间列做精确的时间点定位,效率比翻包高很多。
5.2 时间显示格式切换与延迟计算
排查性能类问题时,Wireshark 的时间显示格式是个隐藏加速器。默认显示的是相对文件开始的时间,要算两条报文之间的间隔得做减法。在 View 菜单下找到 Time Display Format,改为 Seconds Since Previous Displayed Packet,每一条报文都会直接显示与上一条报文的时间差。
这个改动对 QoS 链路检查特别有用。比如 PUBLISH 发出后,隔多久收到 PUBACK;SUBSCRIBE 发出去,隔多久收到 SUBACK。如果间隔从平时的几十毫秒涨到几百毫秒,继续往下拆 TCP 层就能找到瓶颈在哪。实测下来,这个操作比用外部脚本解析 pcapng 快得多。
5.3 抓包文件的安全留存与分享
保存抓包文件时,pcapng 格式最合适,它保留完整元数据,重新打开还能继续用显示过滤器。导出为 CSV 或 JSON 时要注意,MQTT 的 CONNECT 报文里用户名密码字段在明文连接下直接可读,业务 payload 可能包含设备唯一标识、定位信息等敏感数据。分享抓包文件前最好先清掉敏感字段,或者只保留脱敏后的报文摘要。
这也是生产环境安全审查的一个重要话题。早年间很多 IoT 设备用 1883 明文端口,抓包等于明文看数据。现在设备端如果仍然无法启用 TLS,至少要在网络边界做好隔离,避免被链路抓包直接获取业务数据。记住,Wireshark 能看到的东西,别人在链路里也能看到。
我个人的习惯是,每次调试 MQTT 相关功能都会顺手存一个最小复现的 pcapng,命名带上日期和关键字,比如 mqtt-qos2-20250115.pcapng。下次再遇到类似问题,拿旧包和现网包对比,经常能快速发现问题模式是旧病复发还是新问题。
用 Wireshark 分析 MQTT 并不复杂,核心思路是先看应用层的 MQTT 报文是否正常,再逐层往下看 TCP,而不是一上来就被重传包淹没。抓包前把过滤器和时间显示设置好,抓包后按连接建立、心跳、发布、订阅、断开这几个阶段去梳理,绝大多数问题都能在这个过程中现出原形。希望这篇文章对你排查 MQTT 通信问题能有实际帮助。