部门技术分享
IoT 应用开发必知网络协议与抓包
「连不上」是某一层先失败了:按层拆开,用抓包把它找出来
协议:TCP/IP · TCP · UDP · TLS · HTTP · WebSocket · MQTT
工具:tcpdump 在设备或路由器上抓包 · Wireshark 在 PC 上分析
amass · 2026.10.14
AGENDA / 目录
内容提要
分层与四关
七个协议各在哪层,一次上线要过哪四关
TCP 与 UDP
建连、分帧、断开:TCP 只管到字节为止
TLS 安全通道
公私钥谁持有,数据怎么加密,证书怎么校验
HTTP 与 WebSocket
OTA 断点续传,WebSocket 升级握手与帧
MQTT 与物联网平台
SDK 屏蔽了什么,物模型与设备影子
抓包排查实战
设备或路由器上 tcpdump 抓,Wireshark 按症状读包
01 · 分层与四关
「连不上」其实是某一层先失败了
分层 · 本次协议的位置
七个协议各在四层模型的哪一层
- 链路层与网络层:以太网、Wi-Fi、IP,应用只碰到地址与 MTU
- 传输层:TCP 是可靠字节流,UDP 是无连接数据报,端口在这一层
- TLS 不属于四层中的任何一层:对下依赖 TCP,对上给应用加密
- 应用层:HTTP、WebSocket、MQTT 各自定义消息格式与语义
- WebSocket 由 HTTP 升级而来;MQTT 也能跑在 WebSocket 上
- 结论:每层只解决一个问题,排查也从下往上一层一层查
↑ 本次涉及的协议栈(示意)
分层 · 每一关对应的 C 接口
一次 MQTT 上线要过的四关
- DNS 解析
getaddrinfo - TCP 连接
connect - TLS 握手
mbedtls_ssl_handshake - MQTT 连接
CONNECT / CONNACK
- 每关落在不同的层:DNS 通常走 UDP,connect 在传输层,握手是 TLS,CONNACK 在应用层
- 失败的样子各不相同:解析不出地址、连接超时或被拒、证书校验失败、CONNACK 拒绝
- 走 WebSocket 承载时,TLS 之后还多一关 HTTP 升级握手
- 每关失败都要单独打日志并带上返回码,否则现场只剩一句「连不上」
- 结论:先找出哪一关先失败;日志可能漏记,抓包是另一份独立证据
02 · TCP 与 UDP
TCP 只管把字节送到,边界和存活归应用
传输层 · 两种服务
UDP 与 TCP:应用各要补什么
UDP · 无连接数据报
- 发出即完成:不保证到达,不保证顺序
- 保留边界:一次 sendto 对应一次 recvfrom
- 以太网下负载控制在 1472 字节内,免 IP 分片
- 用在 DNS、NTP、DHCP、mDNS 设备发现
TCP · 可靠字节流
- 三次握手建连,确认、重传、按序交付
- 字节流:一次 send 不对应一次 recv
- 自带流量控制与拥塞控制
- 承载 HTTP、WebSocket、MQTT 与 TLS
- 选 UDP 要自己补确认与重传;选 TCP 要自己补消息边界与存活检测
连接的一生 ① · tcpdump 输出(示意,字段有删节)
建连:读懂 [S]、[S.] 与 [R.]
# 正常建连:设备 10.0.0.2 → 服务端 10.0.0.1:1883
IP 10.0.0.2.40312 > 10.0.0.1.1883: Flags [S], seq 0, length 0
IP 10.0.0.1.1883 > 10.0.0.2.40312: Flags [S.], seq 0, ack 1, length 0
IP 10.0.0.2.40312 > 10.0.0.1.1883: Flags [.], ack 1, length 0
# 被拒:端口没人监听,对端直接回 RST
IP 10.0.0.2.40314 > 10.0.0.1.1884: Flags [S], seq 0, length 0
IP 10.0.0.1.1884 > 10.0.0.2.40314: Flags [R.], seq 0, ack 1, length 0
# 超时:只见 [S] 每隔 1、2、4、8 秒重发,始终没有回应
- [S] 是 SYN,[S.] 是 SYN+ACK,[.] 是纯 ACK:前三行就是三次握手
- [R.] 是 RST:端口没人监听,connect 立刻返回 ECONNREFUSED
- 只有 [S] 在重发:对端不可达,或 SYN 被防火墙丢弃
- Linux 缺省重发 6 次 SYN,约 2 分钟才报 ETIMEDOUT:连接超时要应用自己设
连接的一生 ② · 示意代码 长度前缀分帧
传输:一次 send 不等于一次 recv
/* 发送:4 字节长度头(网络字节序)+ 消息体 */
uint32_t len = htonl(body_len);
send_all(fd, &len, sizeof len); /* ① 循环直到写满 */
send_all(fd, body, body_len);
/* 接收:先读满 4 字节,再按长度读满消息体 */
recv_all(fd, &len, sizeof len); /* ② 循环直到读满 */
len = ntohl(len);
if (len > MAX_MSG_LEN) /* ③ 长度先校验 */
return -1;
recv_all(fd, buf, len);
- TCP 只保证字节有序到达,不保留 send 的边界:「粘包」「半包」都是正常现象
- ① ② send 可能只写出一部分,recv 可能只读到一部分:返回值要循环累加
- ③ 长度字段来自网络,不校验就可能按错位的长度去分配内存
- 用 MQTT、WebSocket、HTTP 时协议已定好边界,不必自己切
连接的一生 ③ · 断开
断开的四种样子,应用各看到什么
| 场景 | 链路上发生 | 应用看到 |
|---|---|---|
| 对端正常关闭 | 收到 FIN | recv 返回 0 |
| 对端已重启,设备再发数据 | 对端回 RST | ECONNRESET |
| 对端关闭后继续写 | 对端回 RST | 再写触发 SIGPIPE |
| 拔网线、NAT 回收 | 什么都收不到 | 不报错,假在线 |
- SIGPIPE 默认让进程退出:send 带 MSG_NOSIGNAL 或忽略该信号,断网才不会把程序带走
- 假在线:写要重传约 15 分钟才报错,keepalive 缺省 2 小时才探测 → 靠应用心跳
03 · TLS 安全通道
TLS:私钥证明身份,会话密钥加密数据
- 出示证书
服务端公钥 + 签名 - 验证身份
CA 根证书 · 域名 - 协商密钥
ECDHE 各算各的 - 加密传输
AES-GCM 会话密钥
- 服务端私钥只留在服务器;设备只内置公开的 CA 根证书,用来验对方证书
- 私钥只签名证明身份,不加密数据;会话密钥由 ECDHE 双方各自算出,不在网上传
- 数据用会话密钥做 AES-GCM 对称加密:抓包只见密文,私钥日后泄露也解不开
- 双向认证时设备也有私钥:每台一把、放安全存储,严禁同一把烧进全部固件
- Linux 常用 OpenSSL,RTOS 常用 mbedTLS:都要开证书与域名校验,报未生效先校时
04 · HTTP 与 WebSocket
WebSocket 借 HTTP 握手,变成双向长连接
HTTP · 示意报文 固件断点续传
HTTP 在设备上:用 Range 续传固件
GET /firmware/v2.1.0.bin HTTP/1.1
Host: ota.example.com
Range: bytes=1048576-
If-Range: "v2.1.0"
HTTP/1.1 206 Partial Content
Content-Range: bytes 1048576-4194303/4194304
Content-Length: 3145728
ETag: "v2.1.0"
- Range:只要 1 MB 之后的部分,弱网断了不必从头下载
- 206:服务端同意续传;回 200 说明返回的是完整文件,要从头写
- If-Range:文件已换版本时服务端回 200 整个文件,避免拼出半新半旧的固件
- 复用连接:HTTP/1.1 缺省长连接,每新建一次都要重付 TCP 与 TLS 握手
WebSocket · 握手报文(RFC 6455 示例值)
WebSocket 握手:一次 HTTP 请求换来 101
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
- 先是一次普通 HTTP 请求,所以能复用 80、443 端口
- Upgrade 与 Connection 申请升级;回 101 才算成功,其他状态码都是失败
- Accept 由 Key 拼上固定 GUID 后取 SHA-1 再 Base64,证明对端真懂 WebSocket
- 101 之后改传帧:文本、二进制、Ping/Pong、Close,客户端帧须加掩码
05 · MQTT 与物联网平台
平台 SDK 藏起 MQTT,露出物模型与影子
平台 SDK · 阿里云与华为云的 MQTT 使用限制
用了平台 SDK,MQTT 还剩什么要懂
| MQTT 概念 | SDK 已经替你做的 | 你仍要关心的 |
|---|---|---|
| Topic | 拼好平台 Topic 与 JSON | 抓包时认得出 Topic |
| 连接认证 | 用设备密钥算签名登录 | 一个设备只能一条连接 |
| Keep Alive | 定时心跳,断线重连 | 间隔只能取 30 到 1200 s |
| QoS | QoS 1 未确认时重发 | 通常只有 0 和 1,会重复 |
| 遗嘱 | 华为不支持 | 离线以平台状态为准 |
- 底层仍是发布订阅:上报是设备 PUBLISH,下发是平台向设备订阅的 Topic 推送
- 出处:两家的 MQTT 使用限制文档;华为明确不支持 QoS 2、遗嘱与 retain
物模型 · 阿里云 TSL 三要素
物模型:把设备描述成属性、事件、服务
属性 · 状态
- 设备当前状态,如开关、温度
- 设备上报,云端可设置
- 影子与期望值都基于属性
事件 · 通知
- 设备主动发生的事
- 故障、告警等,带输出参数
- 只上报,云端不设置
服务 · 调用
- 云端调用设备的功能
- 带输入参数与返回结果
- 同步调用 8 秒内须应答
- 华为云的产品模型同理:服务下定义属性与命令,命令即云端调用设备
设备影子 · 华为云 IoTDA 的规则
设备影子:离线也能下发,上线再对齐
- reported 是设备上报的状态,desired 是应用的期望;两者有差异时平台下发差异部分
- 带 version 做乐观锁:写入时版本号须等于当前影子版本,并发修改不会互相覆盖
示意代码 · 依阿里云 Link SDK 物模型 demo 改写
Link SDK:上报属性,背后是一次 PUBLISH
static void on_dm_recv(void *h, const aiot_dm_recv_t *recv, void *ud)
{
if (recv->type == AIOT_DMRECV_PROPERTY_SET) /* ③ */
apply_props(recv->data.property_set.params,
recv->data.property_set.params_len);
}
void *dm = aiot_dm_init();
aiot_dm_setopt(dm, AIOT_DMOPT_MQTT_HANDLE, mqtt_handle); /* ① */
aiot_dm_setopt(dm, AIOT_DMOPT_RECV_HANDLER, (void *)on_dm_recv);
aiot_dm_msg_t msg;
memset(&msg, 0, sizeof msg);
msg.type = AIOT_DMMSG_PROPERTY_POST; /* ② */
msg.data.property_post.params = "{\"LightSwitch\": 1}";
aiot_dm_send(dm, &msg);
- ① 物模型模块挂在 MQTT 句柄上,连接、心跳、重连都归 MQTT 核心
- ② SDK 把 params 包成 Alink JSON,PUBLISH 到属性上报 Topic
- ③ 云端设置属性走回调:先改本地状态,再上报一次,让云端看到结果
- 两个线程:aiot_mqtt_process 管心跳与重发,aiot_mqtt_recv 管收包与重连
- 华为对应:IOTA_PropertiesReport 与 IOTA_SetPropSetCallback
06 · 抓包排查实战
自下而上,找到第一个失败的那一层
排查 · 先判断要不要抓
什么问题该抓包,什么时候值得抓
该抓 · 现象在网络上
- 连不上:卡在 DNS、SYN 或握手
- 偶发断线、假在线、重连风暴
- 慢:重传、零窗口、时延抖动
- 对接新平台、新协议时核对报文
值得抓 · 回报大于成本
- 偶发难复现:环形缓冲长期抓
- 设备与云端各执一词,要证据
- SDK、模组固件是黑盒
- 日志与现象对不上
不必先抓 · 日志就够
- 业务字段错、JSON 解析失败
- 崩溃、内存泄漏、死锁
- 返回码已写明原因,如 401
- 平台控制台已有上下线记录
- 结论:先看日志与返回码;日志说不清卡在哪一关、或各方说法矛盾时,再抓包
设备与路由器抓包 · tcpdump 常用参数
在设备或路由器上抓:基本用法、实时回传与环形缓冲
# ① 基本:指定网卡,不反查名字,存成 pcap
tcpdump -i eth0 -nn -w /tmp/up.pcap 'host 10.0.0.1 and port 8883'
# ② 实时回传:经 ssh 管道送进 PC 上的 Wireshark
ssh root@10.0.0.2 "tcpdump -i eth0 -U -nn -w - 'not port 22'" \
| wireshark -k -i -
# ③ 偶发问题:5 个 10 MB 文件循环覆盖
tcpdump -i eth0 -nn -C 10 -W 5 -w /data/ring.pcap
# ④ 读回再筛:只看 RST
tcpdump -nn -r /tmp/up.pcap 'tcp[tcpflags] & tcp-rst != 0'
# ⑤ OpenWrt 路由器:先关硬件流量卸载,再在 LAN 网桥上抓
tcpdump -i br-lan -nn -w /tmp/dev.pcap 'host 10.0.0.2'
- ① ④ 引号里是捕获过滤器(BPF):host、port、tcp、udp 用 and、or、not 组合
- ② -U 逐包写出,not port 22 排除 ssh 自身流量,否则越抓越多
- ③ -C 按百万字节切文件,-W 限定个数,复现几天也只占 50 MB
- ⑤ Buildroot、Yocto、OpenWrt 都有 tcpdump 包;MCU 装不了就到路由器上抓
- MCU 也可连 Windows 移动热点,Wireshark 直接抓热点那块虚拟网卡
Wireshark · 常用入口
Wireshark 最常用的五个入口
- 专家信息:分析 → 专家信息,汇总重传、乱序、零窗口与 RST,先看这里
- 追踪流:右键 → 追踪流 → TCP 流,把一条连接的往来还原成一问一答
- 解码为:MQTT、HTTP 跑在非标准端口时不被识别,分析 → 解码为,手动指定协议
- 会话与 I/O 图表:统计菜单下,找出流量最大的连接,看吞吐随时间的变化
- TLS 解密:用 SSLKEYLOGFILE 或 OpenSSL 的 keylog 回调导出密钥,填进首选项 → Protocols → TLS
- 结论:先看专家信息找异常,再追踪流看细节;密钥日志只在测试设备上开
排查 · 症状对照表
按症状读包:每关一个过滤器
| 症状 | 先看 | 显示过滤器 | 判断 |
|---|---|---|---|
| 域名解析失败 | DNS | dns | 无应答或 NXDOMAIN |
| 连接超时 | TCP | tcp.flags.syn==1 | 只有 SYN 在重发 |
| 连接被拒 | TCP | tcp.flags.reset==1 | SYN 后紧跟 RST |
| 握手失败 | TLS | tls.alert_message | Alert 写明原因 |
| 认证被拒 | MQTT | mqtt.msgtype==2 | CONNACK 码非 0 |
- 顺序同第一章的四关,找到第一个失败的就停;走 8883 时先解密 TLS 才看得到 CONNACK
- 慢与卡另看:tcp.analysis.retransmission 找重传,I/O 图表看吞吐
练习 · tcpdump 输出(示意,字段有删节)
练一练:这条连接是谁断的
# 设备 10.0.0.2 显示在线,向 10.0.0.1:8883 发 MQTT 心跳
IP 10.0.0.2.40312 > 10.0.0.1.8883: Flags [P.], seq 1:32, length 31
IP 10.0.0.2.40312 > 10.0.0.1.8883: Flags [P.], seq 1:32, length 31
IP 10.0.0.2.40312 > 10.0.0.1.8883: Flags [P.], seq 1:32, length 31
IP 10.0.0.1.8883 > 10.0.0.2.40312: Flags [R], seq 1, length 0
- 问:同一段数据为什么连发三次
- 问:最后的 [R] 是谁发的,说明什么
- 问:设备接下来该怎么做
- 答:seq 1:32 原样重发,说明一直没收到对端的 ACK
- 答:服务端回了 RST,它已不认这条连接,比如重启期间无人应答,起来后已没有这条连接
- 答:关闭后按退避重连,并核对心跳间隔是否短于链路上的空闲超时
1 / 26← → 翻页 · F 全屏 · B 黑屏 · 页码加回车跳页