☰
Wireshark MQTT抓包实战:报文解析与故障排查指南
2026/10/5 15:37:06 网站建设 项目流程

做IoT设备调试的人,应该都经历过这种时刻:设备上报速度不对、消息偶尔丢失、或者客户端一连上就被服务端踢下线。手里没有好用的分析工具时,就像在暗房里找一只黑猫。我最常用的解决办法就是把Wireshark打开,把MQTT报文一封封拆开看。Wireshark是抓包工具里的老牌选手,对MQTT的支持是内置的,不需要装额外插件,只要报文里带的端口和协议特征能被识别,它就能把一条条二进制的TCP payload翻译成结构清晰的协议字段。这篇内容没有太多绕弯子的理论,重点是用Wireshark把MQTT的抓包、过滤、报文解析和故障排查串起来。不管你是做嵌入式开发、上位机通信、网关接入,还是只负责运维一套MQTT服务,照着这套思路走,基本都能快速上手。

1. 为什么建议把Wireshark当成MQTT调试的第一工具

1.1 一次让我印象深刻的排查

我之前调试过一批温湿度采集网关,现象很典型:设备上电后TCP能连上,但过不了几秒就自动断开,后台日志只显示一句connection closed,完全看不出原因。于是我在PC网卡上开了Wireshark,只过滤1883端口,抓到完整的CONNECT和CONNACK报文。CONNACK里的Return Code直接显示Server unavailable,说明broker根本没有接受这次连接,而不是网络链路问题。再回头查broker配置,发现是因为设备使用了一个奇怪的clientId触发了接入校验的规则。整个过程大概十五分钟,如果只看业务日志,可能还在排查网线和防火墙。

这个例子想说明一个道理:MQTT虽然是应用层协议,但大量问题都藏在协议字段里。TCP连接成功不等于MQTT连接成功,而Wireshark能把那些本来需要人工对着十六进制字节猜的字段直接摊开在界面上。消息发没发出去、broker有没有回ACK、订阅时写了哪个主题,全都能看见。很多时候问题不用猜,包放在那里,一眼就能定位。

1.2 Wireshark自带的MQTT解析器到底能做什么

Wireshark从很早的版本就内置了MQTT解析器,支持MQTT 3.1、3.1.1以及5.0。只要版本不是太老,抓包后通常会把1883端口自动识别成MQTT协议。它会把每个报文按照固定报头、可变报头、有效载荷三部分拆分,并且把消息类型、DUP标志、QoS等级、Retain标志、主题名、报文标识符、返回码这些关键信息全部解析成可读字段。同时还支持mqtt显示过滤器,以及通过tshark命令行做批量离线分析。

为什么推荐它而不是随便一个抓包工具?因为MQTT的包本身只是TCP payload里的一段二进制数据,如果不做解析,你看到的就只有一串十六进制。Wireshark的包详情窗口里,会直接显示Message Type: PUBLISH、Topic: home/room1/temp、QoS: 1这种内容,省去了查阅协议的繁琐过程。更关键的是,当你同时抓MQTT、HTTP、Modbus网关数据、WebSocket流量时,Wireshark可以按协议类型把不同流量区分开,后续排查效率完全不一样。这也是我把它当作MQTT调试第一工具的原因。

2. 搭一套随时能抓包的MQTT调试环境

2.1 用Mosquitto在本地起一个Broker

要反复做实验、验证各种异常行为,就不要直接拿生产上的broker来抓包,最好在自己电脑上跑一个本地的MQTT服务。我常用的是Eclipse Mosquitto,轻量、开源、跨平台,命令行一条命令就能启动。Linux下直接安装:

sudo apt install -y mosquitto mosquitto-clients mosquitto -v

Windows下可以下载官方安装包,或者用winget安装。装好之后在命令行执行mosquitto -v -p 1883,就能在前台启动一个监听1883端口的本地broker。如果想要配置用户名密码,可以在配置文件里指定,但测试阶段我基本都是先用匿名模式,避免引入额外变量。配置文件mosquitto.conf里可以这样写:

listener 1883 allow_anonymous true

需要注意的是,这里的allow_anonymous true只是为了本机测试方便,生产环境千万不要这么开。本地broker跑起来之后,MQTT报文会走回环接口,所以后面抓包时要选择loopback网卡,而不是物理网卡,这个细节很多人第一次都会踩。

2.2 准备最简单的发布订阅客户端

Mosquitto自带两个命令行工具:mosquitto_pub和mosquitto_sub,它们虽然简单,但足够用来制造干净的测试流量。开两个终端,一个订阅:

mosquitto_sub -h 127.0.0.1 -p 1883 -t 'test/topic' -v

另一个发布:

mosquitto_pub -h 127.0.0.1 -p 1883 -t 'test/topic' -m 'hello-mqtt'

执行完这条发布命令后,Wireshark里就能看到一条完整的PUBLISH消息。如果还想验证QoS和Retain的行为,可以分别加参数:mosquitto_pub -q 1 -r -t 'test/topic' -m 'retained-message'。-q控制QoS等级,-r表示保留消息。这样做的目的不是为了跑通一个demo,而是让你熟悉不同参数对报文内容的影响。比如同样一条消息,QoS 0和QoS 1在抓包里看到的交互过程完全不同,QoS 0只有一个PUBLISH,QoS 1会多一个PUBACK。纸上谈兵没有用,抓一次包就全都明白了。

2.3 设置Wireshark的抓包与过滤条件

启动Wireshark之后,第一步是选择正确的抓包接口。如果broker跑在本机,客户端也连本机,要选择loopback接口。Windows上安装Npcap时勾选支持loopback抓包,安装后会出现一个类似“Adapter for loopback traffic capture”的接口;Linux则是选择lo接口。选错接口的后果是抓半天什么都看不到。

为了避免抓到大量无关流量,可以在抓包选项里使用捕获过滤器,比如只抓发往1883端口的包:

tcp port 1883

如果本机同时跑了其他MQTT服务,也可以写成host 127.0.0.1 and tcp port 1883。开始抓包后,在显示过滤器里输入mqtt,Wireshark就会把识别为MQTT的报文都过滤出来。如果某些报文没有被自动识别,可以在报文列表里右键,选择Decode As,把对应TCP端口指定为MQTT。这个操作只是改变显示方式,不会修改原始抓包文件,可以放心尝试。

3. 手把手读懂MQTT报文:从连接到收发消息

3.1 固定报头:一个字节看出消息类型

每个MQTT报文都以固定报头开头,至少占两个字节。第一个字节的高四位表示消息类型,数字对应关系是:1是CONNECT,2是CONNACK,3是PUBLISH,4是PUBACK,5是PUBREC,6是PUBREL,7是PUBCOMP,8是SUBSCRIBE,9是SUBACK,10是UNSUBSCRIBE,11是UNSUBACK,12是PINGREQ,13是PINGRESP,14是DISCONNECT。低四位是标志位,其中PUBLISH会用到DUP、QoS、Retain这三个标志,其他报文的标志位大多固定,不需要多管。

第二个字节开始是剩余长度,它表示后面可变报头和有效载荷的总字节数。剩余长度不是简单的一个字节,而是采用1到4字节的变长编码,每个字节用7位存数据,最高位如果是1就表示后面还有延续字节。例如剩余长度300,编码出来就是0xAC 0x02。在Wireshark里这些都不需要人工去算,但理解这个结构还是有用的,因为当你用脚本解析原始payload时,必须知道如何跳过剩余长度字段才能正确读后面的内容。

实际在包详情窗口里,Wireshark会直接显示MQTT Message Type: CONNECT,下面还会展开Fixed Header相关的节点。我习惯先看一眼消息类型,因为只要类型对了,后续字段就八九不离十。比如调试时你预期客户端应该发PINGREQ,但抓到的却是DISCONNECT,那方向立刻就不一样了。

3.2 CONNECT/CONNACK:连接建立过程中的关键字段

CONNECT是客户端连接broker时发出的第一个报文。Wireshark展开后能看到协议名“MQTT”、协议级别4或者5,以及一组连接标志。连接标志位里比较关键的是Clean Session(MQTT 5.0中叫Clean Start)、Will Flag、Will QoS、Will Retain、Username Flag、Password Flag。Will遗嘱字段经常被忽略,但如果设备配置了遗嘱,异常掉线时broker会自动发布一条遗嘱消息。在Wireshark里可以直接看到Will Topic和Will Message的内容,这对排查“设备掉线后其他设备没收到通知”很有帮助。

Keep Alive字段是两字节的秒数,常见配置是60秒。它规定了客户端在没有业务数据时,最多隔多久必须发一条PINGREQ来保活。broker如果在一个半周期内没收到任何报文,就会判定客户端离线并断开连接。Wireshark会直接显示Keep Alive: 60这个字段,连换算都省了。

CONNACK是broker对CONNECT的回应。重点看两个地方:一个是Session Present标志,另一个是Return Code。Return Code为0表示连接成功;非0则对应不同的拒绝原因,比如4表示用户名或密码错误,5表示未授权。MQTT 5.0的CONNACK里还会带Reason String和属性,Wireshark同样能解析。我排查连接问题时基本上先看CONNACK,因为它直接告诉你是协议版本不匹配、用户名密码错误,还是服务端不可用,比对着日志一行行猜要快得多。

3.3 PUBLISH/SUBSCRIBE:主题、载荷和QoS怎么在包里体现

PUBLISH是MQTT里最核心的报文。Wireshark展开后最显眼的就是Topic字段,它是一个UTF-8字符串,显示成完整的主题路径。Packet Identifier只有在QoS大于0时才会出现,它用来关联PUBLISH和对应的ACK。Payload就是实际业务数据,Wireshark默认同时显示十六进制和ASCII。如果payload是JSON,可以右键选择“Show Packet Bytes”或者直接Follow TCP Stream,把完整内容复制出来看。

QoS等级不同,报文的交互方式也不同。QoS 0发出去就结束,抓包里只有一个PUBLISH,没有后续确认;QoS 1会有一个PUBACK;QoS 2的流程是PUBLISH、PUBREC、PUBREL、PUBCOMP四步,缺任何一步都可能导致消息卡住。如果你想从抓包里判断消息可靠性问题,直接看这些确认报文是否存在即可。显示过滤器可以这样写:只看PUBLISH用mqtt.msgtype == 3,只看某QoS等级用mqtt.qos == 1,按主题过滤用mqtt.topic contains "test"。

SUBSCRIBE报文里包含一个或多个Topic Filter,并且每个主题后面跟着一个请求QoS值。broker收到后会回SUBACK,SUBACK的返回码0、1、2表示成功且授予对应QoS,0x80表示失败。如果SUBACK返回失败,或者SUBSCRIBE报文根本没有到达broker,那客户端自然收不到消息。在抓包里把SUBSCRIBE的Topic和PUBLISH的Topic一对比,往往能发现很基础的主题不匹配问题。

3.4 补充:一个485设备通过MQTT上传数据的实际样本

很多工业项目里,485总线设备比如电表、温控器,会通过一个串口网关接入MQTT。网关做的事情有两件:一是把来自客户端的指令转换成Modbus RTU请求发到485总线上,二是把读取到的寄存器数据打包成MQTT消息发布到指定topic。抓到的PUBLISH payload经常是这样的十六进制:

01 03 02 01 2C 79 84

这串字节不是普通文本,而是一帧完整的Modbus报文。Wireshark会原样把它显示在Payload里,你需要自己对照Modbus协议手册去确认从站地址、功能码、寄存器数量、数据字节和CRC校验。这也是很多新手容易懵的地方:Wireshark能解析MQTT的协议头,但它不会帮你解析MQTT payload里的私有业务协议。解析到payload这层之后,剩下的还是要靠你对业务报文的理解。

我调试过一个电表采集项目,设备上报周期正常,但平台上的数据偶尔错位。抓包后发现发布报文的topic是对的,可payload里的寄存器值在不同的frame里对应关系反了。这种问题看业务日志很难发现,因为日志只会显示“读到数据”,但用Wireshark把所有topic下的payload摊开对比,很快就能看出数据被贴错了标签。再往深一层说,MQTT经常只是“搬运工”,真正要排查的往往是搬运工袋子里的Modbus、JSON或者自定义二进制协议。这也是为什么我建议在搞MQTT抓包的同时,至少能看懂一种常用业务载荷的格式。

4. 抓包实战:三种最常见的MQTT故障定位

4.1 设备上线又离线,先查Keep Alive心跳

现象很典型:设备状态在平台上反复跳“在线/离线”,后台日志只说连接断开了。遇到这种情况,我一般先把过滤器切到只看心跳报文:

mqtt.msgtype == 12 || mqtt.msgtype == 13

12是PINGREQ,13是PINGRESP。如果客户端持续发PINGREQ但broker不回PINGRESP,说明链路或broker侧异常。如果客户端压根没有发过PINGREQ,并且长时间的抓包里也没有其他业务报文,那问题就出在客户端的Keep Alive机制没有生效。broker会在1.5倍Keep Alive周期内没收到任何报文时,主动把连接关掉。

还有一种很隐蔽的情况:CONNECT里的Keep Alive被设成了0。0在MQTT里表示不启用保活机制,客户端不会主动发心跳,但很多云平台又要求设备在固定周期内必须有数据。平台侧超时判定离线后,设备端却完全不感知,于是反复重连。这种问题光看业务日志很难猜,但只要打开CONNECT报文看一眼Keep Alive字段,马上就能定位,是配置参数的问题,还是平台策略的问题,一目了然。

4.2 QoS 1/2消息出现重复,多半是ACK交互出了问题

有时候broker会收到重复的MQTT消息,不一定都是业务层面的重复上报。要区分这一点,需要抓包看PUBLISH报文里的DUP标志。如果同一个Packet Identifier的PUBLISH出现了两次,并且第二次的DUP等于1,说明是客户端在重发。重发的常见原因是没有收到对应的PUBACK或者PUBREC。此时可以过滤确认报文:

mqtt.msgtype == 4 || mqtt.msgtype == 5

4是PUBACK,5是PUBREC。如果Wireshark里只有PUBLISH,没有后续的ACK,那问题多半是broker端没有回应,可能被网络丢包、线程阻塞,或者防火墙拦掉了。如果broker已经回了ACK,但客户端仍然重发,那就需要去客户端的发送队列里找原因,通常是ACK和请求的Packet Identifier对不上,或者发送线程在收到ACK之前就超时了。

在Wireshark里处理这种连续报文时,有个很常用的小操作:右键任意一条MQTT报文,选择Conversation Filter -> TCP,就能只看当前这个TCP连接里的所有包。这样做的好处是可以把时间线压缩到一个连接里,肉眼很容易看出PUBLISH和ACK的先后顺序,不会因为同时抓到其他设备的流量而看花眼。

4.3 订阅成功却收不到数据,把Topic和Retain都查一遍

这个场景我遇到太多次了:客户端已经connect成功,SUBACK也返回0,但就是收不到任何数据。排查思路至少有两条线要同时走。

第一条线是看订阅是否正确。过滤mqtt.msgtype == 8,展开SUBSCRIBE报文里的Topic Filter,确认主题完全一致,包括大小写和斜杠位置。MQTT主题是区分大小写的,订阅了home/room1/temp,而发布方发的是home/Room1/temp,这两个根本不是一个主题,收不到数据才正常。还要注意通配符的用法,订阅a/#能收到a/b/c,但订阅a/b收不到a/b/c。在抓包里同时展开SUBSCRIBE和PUBLISH的Topic字段,一对比就能看出是否匹配。

第二条线是看发布是否真的发生。过滤mqtt.msgtype == 3,确认目标topic下有没有消息。如果业务逻辑是“设备自己订阅后马上能收到之前的状态”,那还要看PUBLISH报文里的Retain标志。RETAIN=1表示broker会保留这条消息,后续的新订阅者能立刻收到;RETAIN=0则表示消息只发给当前在线的订阅者。如果抓包里看到发布报文的RETAIN一直是0,但业务预期是“新设备上线就拿到最新状态”,那这就是设计问题,不是通信问题。

5. Wireshark解析MQTT的五个常见坑

5.1 抓到的MQTT包被显示成普通TCP

如果broker用的不是默认的1883端口,Wireshark可能不会自动把TCP payload识别成MQTT,报文列表里只会显示TCP而不是MQTT。解决办法有两种。第一种是在报文列表里右键任意一条TCP报文,选择Decode As,然后在当前端口上指定为MQTT。第二种是去Wireshark的偏好设置里,把自定义端口加进MQTT的TCP端口列表:Preferences -> Protocols -> MQTT -> TCP ports。我自己的习惯是填1883,8883,28883这样的常用端口,方便以后直接抓。

这个坑不算难,但很容易耽误时间。如果看到抓包里TCP payload是16进制,却一直没有MQTT解析树,先不要怀疑是协议问题,多半就是端口没有被识别。

5.2 本机Broker抓不到回环流量

很多人第一次抓本地MQTT,开了Wireshark发现怎么抓都是空的。原因通常是本机和本地broker之间的流量走的是loopback回环接口,不走物理网卡。Windows下需要安装Npcap时勾选“Support loopback traffic”选项,抓包接口里会有一个专门的回环适配器;Linux下要选择lo接口。如果已经选对接口但还是看不到,检查一下捕获过滤器是否写死了物理网卡的IP,比如host 192.168.1.100,但本地连接实际上走的是127.0.0.1,那当然啥都抓不到。这个坑和MQTT本身没关系,但确实是本地抓包最常见的卡点。

5.3 TLS加密后一屏乱码

MQTT over TLS通常用8883端口,此时Wireshark看到的TCP payload是密文,MQTT解析树完全是空的。想调试明文流量,两个合规范的思路:一是在测试环境临时关闭TLS,改成1883端口抓包,等业务问题复现后再恢复加密;二是如果客户端是自己写的,并且是自有设备、受控测试环境,可以在启动客户端时设置SSLKEYLOGFILE环境变量,导出TLS会话密钥,然后在Wireshark的Preferences -> Protocols -> TLS里配置这个日志文件,让Wireshark解密TLS流量并继续解析MQTT。

这里必须强调一个边界:解密TLS只能针对自己拥有和管理的设备、测试环境,并且要遵守相关合规要求。不要试图对别人的流量做解密,更不要在生产环境乱抓包。我自己的做法是,能用明文复现的问题尽量在测试环境明文抓包解决;实在需要分析TLS握手细节,只看TLS层,不做内容解密。

5.4 MQTT over WebSocket无法直接过滤

浏览器客户端或者一些前端设备会用WebSocket来承载MQTT,典型端口是8083或8084。抓包时Wireshark可能把上层协议识别成WebSocket,导致mqtt过滤器什么都过滤不出来。处理办法是找到WebSocket数据帧,右键Decode As,尝试把它设置为MQTT。新版Wireshark对MQTT over WebSocket的支持已经好了不少,但不同版本行为有差异。

实操上,我一般先过滤tcp.port == 8083,然后看WebSocket的payload开头。MQTT CONNECT报文的固定报头第一个字节是0x10,如果看到payload从10开始,后面跟着“MQTT”四个可见字符,那基本可以确定这就是MQTT over WebSocket。再用Decode As把它解析成MQTT,后面的字段就都能正常显示了。

5.5 只有TCP握手、没有MQTT报文

如果显示过滤器写的是mqtt,却只能看到TCP三次握手,看不到任何MQTT报文,通常有两种可能。第一种是服务端端口没有在监听,客户端连接后直接被RST或者超时。本地可以用netstat -an | findstr 1883或者Linux的ss -lntp确认端口状态。如果端口没监听,当然不会有MQTT。第二种是客户端连接到了一个中间代理或者负载均衡器,这个服务并不会转发MQTT流量。此时要看Wireshark里的目标IP和端口是不是预期地址,再用mosquitto_pub本地连一下broker,对比一下连接行为就能定位。

6. 把解析能力沉淀成离线脚本

6.1 用tshark批量提取主题和载荷

Wireshark的图形界面适合交互式排查,但当你手里有大量pcap文件时,手动一个个点会疯。这时候可以用Wireshark自带的命令行工具tshark。一条命令就能把MQTT报文里的关键字段批量导出:

tshark -r mqtt.pcap -Y "mqtt.msgtype == 3" -T fields \ -e frame.number -e mqtt.topic -e mqtt.qos -e mqtt.payload \ -E header=y -E separator=,

这条命令会列出文件中所有PUBLISH报文的帧号、主题、QoS和payload。输出成CSV之后,后续丢给Excel或者Python都很方便。如果没有某个字段,导出时会是空值,不影响整体。对比图形界面,这种方式的优势是稳定、可重复,同一份pcap可以反复跑不同的提取条件。

6.2 写一套简单的MQTT消息巡检脚本

如果你要处理的是周期性问题,比如设备每天凌晨掉线,手动盯屏幕抓包太累,可以把tshark放进自动化脚本里。思路很简单:定时抓一段时间的pcap,然后导出MQTT报文统计。比如在线抓包10分钟:

tshark -i any -f "tcp port 1883" -Y "mqtt" -T fields \ -e mqtt.msgtype -e mqtt.topic -e mqtt.clientid \ -E header=y -E separator=, > result.csv

拿到result.csv之后,可以在脚本里统计有没有CONNACK返回非0、有没有异常的重复PUBLISH、有没有长时间缺少PINGREQ。我建议不要为了用脚本而用脚本,日常单点问题直接开Wireshark更快。但当现象变成“每天凌晨三点设备集体掉线”这种定时问题时,脚本的价值就非常明显,它能帮你留下证据,也能在大批量终端的场景里快速缩小范围。

6.3 从MQTT协议解析延伸到Modbus/485指令调试

回到经常被问到的一个问题:MQTT如何给485设备发指令、读取数据。实际落地项目里,MQTT报文只是货物搬运工,真正的业务在payload里。用Wireshark看MQTT,只能判断消息有没有从A到B,但要知道指令内容对不对,还得结合Modbus协议文档逐字节去拆。操作思路是:抓包后过滤出指定topic的PUBLISH报文,复制payload的十六进制,按Modbus报文格式去拆:第一个字节是从站地址,第二个字节是功能码,后面是寄存器地址、寄存器数量、数据长度、数据和CRC校验。拆完之后和网关上实际发出的报文做对比。

这里有一个很实用的经验:网关把Modbus指令打包进MQTT时,一条指令对应的消息往往只有几十字节。用Wireshark的Follow TCP Stream,能看到这个TCP连接里完整的报文交互记录,比业务日志里截断的十六进制可靠很多。曾经有同事调485继电器控制,发现指令发出去了但设备没动作。抓包后看到MQTT publish的topic正确,payload看起来也正常,但发给目标设备的Modbus帧CRC校验总是不对。后来比对才发现网关在组帧时把校验字节和前面的数据弄混了。没有抓包,光看上层日志根本定位不了这种底层问题。

7. 写在最后

我自己的习惯是,项目里只要涉及MQTT调试,就先不急着改代码,而是把抓包环境固定下来:本地broker、预设显示过滤器、tshark导出模板都准备成一套固定流程。这样每次遇到问题,都可以在一个小时内有一个初步结论。有一次帮同事调无人值守设备,抓包后发现就是CONNECT里的Keep Alive被配置成0,导致平台侧判定离线,改完配置立刻正常。整个过程从抓包到定位不到十分钟,比逐行翻日志高效太多。

最后分享一个小技巧:在Wireshark里可以把自己常用的显示过滤器保存成按钮,比如下面这条:

(mqtt.msgtype == 1 || mqtt.msgtype == 2) && tcp.port == 1883

这样只要看到连接相关的报文,一键就能过滤出来。另外,强烈建议你动手搭一遍本地Mosquitto加上两个MQTT客户端的测试环境,亲手抓一次CONNECT、PUBLISH和SUBSCRIBE的完整报文。很多协议细节只看文档很难真正记住,但亲手抓过几次包之后,MQTT的交互过程就会在脑子里变得非常清晰。抓包这件事,最后拼的就是对报文结构的熟悉程度和你踩过的坑够不够多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询