以下是结合自己大型项目中的应用做一个回想总结。
一、场景引出
先看以下几个网络通信场景:
场景一: 客户端需定时主动上报轻量级消息
客户端需要定时N秒上报位置轨迹给服务端,要求尽量实时和轻量级,该如何实现?HTTP的请求和响应都是比较重的,轮询不太合适。
场景二:客户端需要及时获取状态变更信息,但变更时间不确定客户端A发起一个修改订单请求需要客户端B同意才能生效,那么如何才能及时得到B的同意或拒绝呢?HTTP请求轮询的方式及时性不高,且轮询间隔太小又导致后端QPS过高。
场景三:客户端A收到推送特定指令后需执行特定响应事件客户端A在前台运行时间过久(疲劳驾驶之类的),被服务器端检测到需要下线,此时需要收到服务端的推送消息后执行下线动作。
客户端A在订单流程中,不定时可能收到(也可能没有)特定的消息,需要处理..。
这些场景,该用什么方案实现?----- 这就是本篇要介绍的主题MQTT协议。
它是一种极其轻量级的发布/订阅消息传输协议,基于TCP长连接,专为低带宽、高延迟、不可靠网络环境下的通信而设计。关于细节可以查看# MQTT消息通道-基础篇。
二、系统如何集成MQTT
2.1 开源库
MQTT 原本是 IBM 的私有协议,2011 年捐赠给 OASIS 开放标准组织,成为 OASIS 标准。我们需要后台工程师配合部署到服务器,各客户端基本都有开源版本集成对接。
开源库:
- Swift版本:CocoaMQTT (github.com/emqx/CocoaM...)
- 其他端的源码可以在官网查看到对应的版本:mqtt.org/software/
2.2 iOS项目集成改造
上述开源库是一个MQTT协议的实现基础,在我们项目中需要做一层功能封装适配,这样各个业务模块能更方便地实现调用。
1. 架构层级
scss
XXMQTT (项目针对开源库的上层封装 Pod)
│ 负责:业务标签分发(bizTag)、消息去重、多级重试、配置管理、匿名连接
│
├── XXMQTTSeesionManager ──调用──> MQTTSessionManager.connectTo:
├── XXMQTTDispatcher ──接收──> MQTTSessionManagerDelegate.newMessage
└── XXMQTTConfigManager ──提供──> host/port/token (传给 connectTo:)
MQTTClient (开源库)
└── 纯粹 MQTT 协议实现,不感知上层业务逻辑(bizTag、去重、配置管理等)
2.关键设计要点
-
绕过 MQTTSessionManager :XXMQTT 直接操作
MQTTSession而非MQTTSessionManager,用自己的重试体系替代 Client 库自带的重连机制,实现更精细的控制(四级降级、30s 订阅超时) -
双 Topic 模式 :底层仅订阅
/rtc/global/im/{clientId}和/rtc/global/push/{clientId}两个 topic,业务层通过bizTag在 Dispatcher 中做二级分发,减少服务端订阅数 -
消息缓存兜底:IM 缓存 ≤10 条、PUSH ≤5 条,FIFO 淘汰。首次订阅时自动回放缓存消息
-
线程安全:pthread_mutex(状态锁)+ pthread_rwlock(读写锁)+ 两个串行队列(连接/发送分离)
-
环境隔离 :stg/pre/prd 三套环境,不同域名(
llrtc-center[-stg|-pre].xxx.cn),ConfigManager 自动选择 -
网络快速通道 :AFNetworking 监听网络恢复 → 立即
quickConnect(),不等待心跳定时器 -
connection refused 自动清配置:error.code==4(MQTTRefusedBadUserNameOrPassword)时自动清掉过期 token 缓存
-
发送节流 :未连接状态下发消息,最多每 60s 触发一次
quickConnect(),防止高频无效重连
三、iOS库的代码架构及实现原理
代码来自开源库:MQTT-Client-Framework(Christoph Krey, 2013-2017)
3.1、总体架构 ------ 四层结构
scss
┌──────────────────────────────────────────────────────────────────────┐
│ MQTTSessionManager │
│ 会话管理:自动重连 · 订阅管理 · 前后台生命周期 · 连接参数持久化 │
└────────────────────────────┬─────────────────────────────────────────┘
│ 持有并代理
▼
┌──────────────────────────────────────────────────────────────────────┐
│ MQTTSession │
│ MQTT 协议核心:CONNECT/DISCONNECT · SUBSCRIBE/PUBLISH ·QoS· KeepAlive│
│ 作为 MQTTDecoderDelegate + MQTTTransportDelegate 的接收方 │
└──────────┬──────────────────────────────┬────────────────────────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────────────────┐
│ MQTTDecoder │ │ MQTTTransport (协议) │
│ 字节流 → MQTTMessage │ │ MQTTCFSocketTransport │
│ (解析 MQTT 协议帧) │ │ CFStream TCP Socket · TLS/SSL │
└──────────────────────┘ └──────────────┬───────────────────┘
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│MQTTCFSocket │ │MQTTCFSocket │ │MQTTSSLSecurity │
│ Decoder │ │ Encoder │ │PolicyTransport │
│ (读流包装) │ │ (写流包装) │ │(SSL 证书固定) │
└──────────────┘ └──────────────┘ └──────────────────┘
3.2、核心类职责明细
1. MQTTSession --- 协议核心(最核心的类)
MQTT 协议的完整实现,管理连接生命周期、消息收发、心跳保活。
**内部状态机: **
vbnet
MQTTSessionStatus:
Created → Connecting → Connected → Disconnecting → Closed
↘ Error
关键属性:
| 属性 | 类型 | 说明 |
|---|---|---|
status |
MQTTSessionStatus | 当前连接状态 |
transport |
id<MQTTTransport> | 传输层实现(依赖注入) |
decoder |
MQTTDecoder | MQTT 协议解码器 |
persistence |
id<MQTTPersistence> | 消息持久化(QoS 1/2 需要) |
keepAliveInterval |
UInt16 | 心跳间隔(默认 60s) |
dupTimeout |
double | QoS 1/2 消息超时重发间隔(默认 20s) |
clientId |
NSString | 客户端标识(nil 时自动生成) |
delegate |
id<MQTTSessionDelegate> | 事件回调 |
connectHandler |
block | 连接结果回调 |
cleanSessionFlag |
BOOL | 清理服务端旧会话 |
protocolLevel |
MQTTProtocolVersion | 协议版本(3.1 / 3.1.1 / 5.0) |
内部管理结构:
-
subscribeHandlers--- 订阅回调字典,key = messageId -
unsubscribeHandlers--- 取消订阅回调字典 -
publishHandlers--- 发布回调字典 -
txMsgId--- 自增消息 ID(从 1 开始,1~65535 循环) -
keepAliveTimer/checkDupTimer--- GCDTimer 定时器
2. MQTTSessionManager --- 会话管理器
在 MQTTSession 之上封装自动重连、订阅管理、前后台切换。
内部状态机:
vbnet
MQTTSessionManagerState:
Starting → Connecting → Connected → Closing → Closed
↘ Error
核心职责:
- 连接参数记忆:保存 host/port/tls/keepalive/clean/auth/user/pass/clientId 等,参数变化时重建 Session
- 自动重连 :通过
ReconnectTimer实现指数退避重连(1s → 2s → 4s → ... → 64s) - 订阅管理 :维护
internalSubscriptions/effectiveSubscriptions字典,连接成功后自动复订阅 - 前后台管理 :通过
ForegroundReconnection实现退后台断开、回前台重连 - 消息透传 :接收 MQTTSession 的消息事件,转发给
MQTTSessionManagerDelegate
3 MQTTTransport(协议) / MQTTCFSocketTransport(实现)
MQTTTransport 协议定义了传输层的抽象接口:
perl
open → send(data) → close
状态: Created → Opening → Open → Closing → Closed
MQTTCFSocketTransport 基于 Apple CFStream 实现:
CFStreamCreatePairWithSocketToHost()创建 TCP socket- 内部持有
MQTTCFSocketDecoder(读流)+MQTTCFSocketEncoder(写流) - 读/写流绑定到指定
dispatch_queue_t - 支持 TLS/SSL(
kCFStreamSocketSecurityLevelNegotiatedSSL) - 支持 VoIP 后台模式(
NSStreamNetworkServiceTypeVoIP) - 支持客户端证书(PKCS12 格式)
4 MQTTSSLSecurityPolicyTransport --- SSL 证书固定
继承 MQTTCFSocketTransport,增强 SSL 安全性:
- 替换系统默认的证书链验证
- 支持自定义
MQTTSSLSecurityPolicy(证书固定 / 自签名证书) - 通过
MQTTSSLSecurityPolicyDecoder/Encoder包装原始流
5 MQTTDecoder --- MQTT 协议解码器
基于 NSInputStream 的 MQTT 帧解析器,将原始字节流解析为 MQTTMessage。 解码状态机:
scss
Initializing → DecodingHeader → DecodingLength → DecodingData → (循环回 DecodingHeader)
↘ ConnectionError / ProtocolError
解码流程:
-
DecodingHeader:读 1 字节 → 解析固定头部(消息类型 + 标志位) -
DecodingLength:逐字节读取剩余长度(变长编码,每字节低 7 位有效) -
DecodingData:读取指定长度的 payload 数据 → 完整消息 → 回调decoder:didReceiveMessage:
6 MQTTMessage --- 消息模型
MQTT 控制报文类型枚举(MQTTCommandType):
| 值 | 类型 | 方向 | 说明 |
|---|---|---|---|
| 1 | CONNECT | C→S | 连接请求 |
| 2 | CONNACK | S→C | 连接确认 |
| 3 | PUBLISH | 双向 | 发布消息 |
| 4 | PUBACK | 双向 | QoS 1 确认 |
| 5 | PUBREC | 双向 | QoS 2 接收 |
| 6 | PUBREL | 双向 | QoS 2 释放 |
| 7 | PUBCOMP | 双向 | QoS 2 完成 |
| 8 | SUBSCRIBE | C→S | 订阅请求 |
| 9 | SUBACK | S→C | 订阅确认 |
| 10 | UNSUBSCRIBE | C→S | 取消订阅 |
| 11 | UNSUBACK | S→C | 取消订阅确认 |
| 12 | PINGREQ | C→S | 心跳请求 |
| 13 | PINGRESP | S→C | 心跳响应 |
| 14 | DISCONNECT | C→S | 断开连接 |
| 15 | AUTH | 双向 | MQTT 5.0 认证 |
工厂方法 :为每种报文类型提供静态构造方法,自动序列化为 wireFormat(NSData 二进制格式)。
7 MQTTPersistence(协议) / 实现类
QoS 1/2 消息需要持久化存储以支持超时重发。
MQTTFlow 实体属性:clientId、方向(incoming/outgoing)、topic、data、qos、messageId、deadline、commandType
两种实现:
MQTTCoreDataPersistence:基于 CoreData 的持久化实现(默认)MQTTInMemoryPersistence:仅内存存储
关键约束:
maxWindowSize(默认 16)--- 飞行窗口大小,控制未确认消息数量maxMessages(默认 1024)--- 最大暂存消息数maxSize(默认 64MB)--- 最大存储空间
8 ReconnectTimer --- 重连定时器
基于 GCDTimer 的指数退避重连:
-
初始间隔 1.0s,每次触发后翻倍(2s → 4s → 8s → ... → maxRetryInterval,默认 64s)
-
schedule--- 启动定时器 -
stop--- 停止 -
resetRetryInterval--- 重置为初始间隔(连接成功后调用)
9 ForegroundReconnection --- iOS 前后台管理
监听三个系统通知:
| 通知 | 动作 |
|---|---|
UIApplicationWillResignActive |
断开 MQTT 连接 |
UIApplicationDidEnterBackground |
申请后台任务延长时间 |
UIApplicationDidBecomeActive |
重新连接 |
10 辅助类
| 类 | 职责 |
|---|---|
MQTTStrict |
全局开关,控制是否对 MQTT 协议参数做严格校验 |
MQTTLog |
日志宏(DDLogVerbose/DDLogWarn/DDLogError) |
MQTTReport |
未在 umbrella header 公开(内部使用) |
MQTTSessionLegacy |
对 mqttio-OBJC 旧版 API 的向后兼容扩展 Category |
MQTTProperties |
MQTT 5.0 属性模型 |
3.2、调用链路详解
1 连接建立全链路
ini
调用方
│
├─> MQTTSessionManager.connectTo:port:tls:...connectHandler:
│ │
│ ├── ① 判断参数是否变化,若变则创建新 MQTTSession
│ │ └─> MQTTSession.initWithClientId:userName:password:...
│ │ │ 设置 persistence (CoreData)
│ │ │ 设置 delegate = MQTTSessionManager
│ │
│ ├── ② 若已有 session 且参数未变 → reconnect:
│ │ 否则 → connectToInternal:
│ │
│ └── ③ connectToInternal:
│ ├── 创建 MQTTCFSocketTransport(或 SSLSecurityPolicyTransport)
│ ├── 设置 transport.host / port / tls / certificates / voip / queue
│ ├── session.transport = transport
│ └── [session connectWithConnectHandler:]
│
└─> MQTTSession.connect
│
├── ① 严格模式参数校验(clientId / userName / protocolLevel / will 等)
├── ② cleanSessionFlag=YES → 清空 persistence + 所有 handler 字典
├── ③ tell(发送队列中待发消息)
├── ④ status = MQTTSessionStatusConnecting
├── ⑤ 创建 MQTTDecoder,设 delegate=self,open
├── ⑥ transport.delegate = self
└── ⑦ [transport open]
│
└─> MQTTCFSocketTransport.open
├── CFStreamCreatePairWithSocketToHost(NULL, host, port, &readStream, &writeStream)
├── 如 tls=YES → 设置 SSL 属性(证书固定等)
├── 创建 MQTTCFSocketEncoder → 绑定 writeStream → open
└── 创建 MQTTCFSocketDecoder → 绑定 readStream → open
│
└── encoderDidOpen: 回调
└── state = MQTTTransportOpen
└── mqttTransportDidOpen: (MQTTSession 收到)
│
└── 发送 CONNECT 报文
encode([MQTTMessage connectMessageWithClientId:...])
└── transport.send(wireFormat)
└── encoder.send(data)
└── writeStream 写入
服务端返回 CONNACK →
│
└── MQTTCFSocketTransport 收到数据
└── decoder:didReceiveMessage: → transport delegate
└── MQTTSession.mqttTransport:didReceiveMessage:
└── MQTTDecoder.decodeMessage(data)
│
└── decoder:didReceiveMessage: (MQTTSession 收到已解析消息)
│
├── status=Connecting + type=CONNACK:
│ ├── returnCode == Success:
│ │ ├── status = Connected
│ │ ├── 启动 checkDupTimer(每 1s 检查待重发消息)
│ │ ├── 启动 keepAliveTimer(按 effectiveKeepAlive 间隔)
│ │ ├── 回调 delegate.connected:sessionPresent:
│ │ ├── 回调 connectionHandler(MQTTSessionEventConnected)
│ │ └── 回调 connectHandler(nil) ← 调用方收到连接成功
│ │
│ └── returnCode != Success:
│ └── 回调 connectHandler(error)
│
└── status=Connected + type=PUBLISH:
└── delegate.newMessage:data:onTopic:qos:retained:mid:
2 消息接收全链路
yaml
MQTT Broker → TCP Socket
│
└── CFReadStream 有数据到达
└── MQTTCFSocketDecoder.stream:handleEvent: (NSStreamEventHasBytesAvailable)
└── decoder.delegate (MQTTCFSocketTransport)
└── transport.delegate (MQTTSession)
└── MQTTSession.mqttTransport:didReceiveMessage:
└── [self.decoder decodeMessage:data]
│
└── MQTTDecoder.decodeMessage:
├── 创建 NSInputStream 读取原始字节
├── DecodingHeader: 读 1 字节 → 获取 type+flags
├── DecodingLength: 读变长长度
├── DecodingData: 读 payload
└── decoder:didReceiveMessage: → MQTTSession
│
└── MQTTSession.decoder:didReceiveMessage:
├── [MQTTMessage messageFromData:] → 解析为消息对象
├── 通知 delegate.received:type:qos:...
├── 询问 delegate.ignoreReceived:... (可选过滤)
│
└── switch (message.type):
├── PUBLISH:
│ ├── QoS 0: 直接回调 newMessage 给 delegate
│ ├── QoS 1: 存 persistence → 发送 PUBACK → 回调
│ └── QoS 2: 存 persistence → 发送 PUBREC →
│ 等 PUBREL → 发送 PUBCOMP → 回调
├── PUBACK/PUBREC/PUBCOMP: 更新 persistence flow
├── SUBACK: 回调 subscribeHandler
└── PINGRESP: 无操作
3 消息发布全链路
ini
调用方
│
├─> MQTTSessionManager.sendData:topic:qos:retain:
│ └── [session publishData:onTopic:retain:qos:]
│
└─> MQTTSession.publishData:onTopic:retain:qos:
│
├── ① 严格模式参数校验(topic 非空、不含通配符、长度限制)
│
├── ② QoS 0(At Most Once):
│ 直接构造 PUBLISH → encode → transport.send → 完成
│
└── ② QoS 1/2(At Least Once / Exactly Once):
├── msgId = nextMsgId()(自增,1~65535 循环)
├── 检查飞行窗口大小 windowSize ≤ maxWindowSize(16)
├── 窗口未满:
│ ├── 构造 PUBLISH 报文 → encode
│ ├── 存入 persistence(topic/data/qos/msgId/deadline=now+20s)
│ └── 等待服务端 PUBACK/QoS2 流程
├── 窗口已满:
│ └── 存入 persistence(commandType=MQTT_None)→ 等待 checkDupTimer 下次发送
└── tell()(触发 checkTxFlows)
4 QoS 2 完整握手流程
ini
Client Server
│ │
├─ PUBLISH (msgId=N, qos=2) ────────>│
│ │
│<─────────────── PUBREC (msgId=N) ──┤ (服务端确认收到)
│ │
├─ PUBREL (msgId=N) ────────────────>│ (客户端确认可发布)
│ │
│<────────────── PUBCOMP (msgId=N) ──┤ (服务端确认完成)
│ │
└─ 回调 publishHandler │
5 心跳保活机制
scss
MQTTSession.keepAliveTimer (GCDTimer)
│ 间隔 = effectiveKeepAlive(服务端返回的 serverKeepAlive 或本地 keepAliveInterval)
│
└─ 定时触发 keepAlive()
├── delegate.beforeKeepAlive: → 允许 delegate 修改 PINGREQ 消息
├── encode([MQTTMessage pingreqMessage]) → transport.send
└── delegate.afterKeepAlive:
6 消息重发机制
scss
MQTTSession.checkDupTimer (GCDTimer, 每 1 秒触发)
│
└─ checkDup() → checkTxFlows()
│
├── 获取 persistence 中所有出站 flow
├── 遍历,找到 deadline 已过期的 flow
│
└── 按 commandType 处理:
├── 0 (MQTT_None, 排队中):
│ └── 窗口未满 → 构造 PUBLISH → encode → commandType=MQTTPublish → deadline 延后 20s
├── MQTTPublish (已发送未确认):
│ └── 超时 → 重发 PUBLISH(dupFlag=YES)→ deadline 延后 20s
└── MQTTPubrel (QoS 2 第 3 步):
└── 超时 → 重发 PUBREL → deadline 延后 20s
7 断线重连机制
csharp
MQTTSessionManager 作为 MQTTSessionDelegate 接收事件:
│
├── on MQTTSessionEventConnectionClosedByBroker (非主动关闭):
│ └── triggerDelayedReconnect
│ └── ReconnectTimer.schedule → 1s 后 reconnect:
│
├── on MQTTSessionEventProtocolError / ConnectionRefused / ConnectionError:
│ └── triggerDelayedReconnect
│ └── ReconnectTimer.schedule
│
├── on MQTTSessionEventConnected:
│ └── ReconnectTimer.resetRetryInterval (重置为 1s)
│ └── 自动复订阅 internalSubscriptions
│
└── ReconnectTimer.reconnect():
├── stop timer
├── currentRetryInterval = min(currentRetryInterval * 2, maxRetryInterval) // 指数退避
└── 执行 reconnectBlock → MQTTSessionManager.reconnect:
└── state = Starting → connectToInternal → 重新走连接流程