3. 会话状态详解:服务端会话状态、客户端会话状态、状态持久化机制
聊到 MQTT 会话恢复,核心绕不开一个词——状态。
说白了,会话恢复就是“把断掉之前的现场找回来”。那这个“现场”到底存了些什么?存在谁手里?怎么保证它不丢?
这一节,我们就把会话状态拆开揉碎,讲清楚。
3.1 服务端会话状态:Broker 替你记着什么
当客户端以 Clean Start = 0 连接时,Broker 会为它维护一份会话状态。我个人习惯把这份状态理解为“Broker 对客户端的承诺清单”。
具体包括以下几项:
- 会话是否存在标志:Broker 内部标记这个 Session 是否存活。
- 客户端的订阅信息:你订阅了哪些 Topic,以及对应的 QoS。
- 未分发的 QoS 1 和 QoS 2 消息:客户端离线期间,Broker 替它暂存的消息。
- 正在传输中的 QoS 2 消息状态:那些已经发出但还没收到 PUBREC 的消息。
- 遗嘱消息:如果客户端异常断开,Broker 要替它发的最后一条消息。
- 过期时间:Session Expiry Interval,超过这个时间 Broker 就会清理状态。
核心要点:服务端状态是“离线缓存 + 订阅记忆”的组合体。没有它,会话恢复就是空谈。
我在项目中遇到过一个问题:某 IoT 平台有 10 万台设备,每台设备订阅了 5 个 Topic。如果 Broker 不维护订阅状态,设备重连后需要重新订阅,那瞬间的订阅风暴能把 Broker 打趴下。嗯,这就是服务端状态存在的意义。
3.2 客户端会话状态:你的设备也得记点东西
别以为状态全是 Broker 的事。客户端自己也得维护一份状态,否则恢复流程走不通。
客户端会话状态主要包括:
- Client ID:唯一标识,Broker 靠它找到对应的服务端状态。
- 未确认的 QoS 1 和 QoS 2 消息:发出去但没收到 ACK 的消息,重连后要重发。
- 收到的 QoS 2 消息 ID:用于去重,防止重复处理。
- Session Expiry Interval:客户端自己也得记住这个值,重连时告诉 Broker。
你想想看,如果客户端不记这些,重连后 Broker 说“我这有 100 条消息没发给你”,客户端却不知道哪些已经收过了——那不乱套了吗?
我的建议:客户端状态最好持久化到本地存储(比如 Flash 或 SQLite),而不是只放在内存里。设备断电重启后,状态还能恢复。
3.3 状态持久化机制:怎么保证不丢
状态在内存里跑得飞快,但一断电就没了。所以,持久化是必须的。
常见的持久化方式有三种:
| 方式 | 说明 | 典型场景 |
|---|---|---|
| 文件存储 | 将状态序列化写入磁盘文件 | Mosquitto 默认的 .db 文件 |
| 数据库 | 使用关系型或 KV 数据库存储 | EMQX 的 Mnesia/Redis 后端 |
| 内存 + 定期快照 | 内存为主,定期落盘 | 高性能场景,兼顾速度与可靠性 |
我曾经踩过一个坑:某项目用 Mosquitto,默认持久化是每 5 秒写一次文件。结果 Broker 在两次写入之间宕机,丢了 3 秒内的所有会话状态。设备重连后,订阅全部丢失,平台瘫痪了 20 分钟。
避坑指南:我曾经以为默认配置够用,结果生产环境教做人。建议根据业务容忍度,调整持久化间隔或使用数据库后端。
3.4 状态生命周期:从创建到销毁
一个会话状态的生命周期,大致分为四个阶段:
- 创建:客户端以 Clean Start = 0 连接,Broker 发现没有旧会话,就新建一个。
- 活跃:客户端在线,状态正常读写。
- 休眠:客户端断开,但 Session Expiry Interval 还没到,状态保留。
- 销毁:过期时间到了,或者客户端以 Clean Start = 1 连接,Broker 清理状态。
这里有个细节:休眠期间,Broker 会替客户端缓存消息。但缓存是有上限的,超过上限就会丢弃最早的消息。嗯,这个上限通常由 max_queued_messages 配置控制。
3.5 一张图看懂会话状态全貌
下面这张 SVG 图,把服务端状态、客户端状态、持久化机制和生命周期串在了一起。建议你仔细看看,能帮你建立整体认知。
3.6 小结:状态是会话恢复的基石
没有状态,就没有恢复。服务端负责记忆订阅和离线消息,客户端负责记忆未确认的消息 ID。两者配合,再加上可靠的持久化机制,才能实现真正的“断线重连,无缝衔接”。
我个人觉得,理解会话状态是掌握 MQTT 会话恢复最关键的一步。后面的章节,我们会基于这些状态,一步步拆解恢复流程的具体细节。