参考 Reference
v3.1.1
其他资源
网站
测试工具
基本概念 Basic Conception
Session 会话
定义
- 定义:某个客户端(由ClientID作为标识)和某个服务器之间的逻辑层面的通信
- 生命周期(存在时间):会话 >= 网络连接
ClientID
- 客户端唯一标识,服务端用于关联一个Session
- 只能包含这些 大写字母,小写字母 和 数字(0-9a-zA-Z),23个字符以内
- 如果 ClientID 在多次 TCP连接中保持一致,客户端和服务器端会保留会话信息(Session)
- 同一时间内 Server 和同一个 ClientID 只能保持一个 TCP 连接,再次连接会踢掉前一个
CleanSession 标记
- 在Connect时,由客户端设置
- 0 —— 开启会话重用机制。网络断开重连后,恢复之前的Session信息。需要客户端和服务器有相关Session持久化机制。
- 1 —— 关闭会话重用机制。每次Connect都是一个新Session,会话仅持续和网络连接同样长的时间。
客户端 Session
- 已经发送给服务端,但是还没有完成确认的 QoS 1 和 QoS 2 级别的消息
- 已从服务端接收,但是还没有完成确认的 QoS 2 级别的消息
服务器端 Session
- 会话是否存在,即使会话状态的其它部分都是空 (SessionFlag)
- 客户端的订阅信息 (ClientSubcription)
- 已经发送给客户端,但是还没有完成确认的 QoS 1 和 QoS 2 级别的消息
- 即将传输给客户端的 QoS 1 和 QoS 2 级别的消息
- 已从客户端接收,但是还没有完成确认的 QoS 2 级别的消息
- (可选)准备发送给客户端的 QoS 0 级别的消息
长连接维护与管理
Keep Alive 心跳
- 目的是保持长连接的可靠性,以及双方对彼此是否在线的确认。
- 客户端在Connect的时候设置 Keep Alive 时长。如果服务端在 1.5 * KeepAlive 时间内没有收到客户端的报文,它必须断开客户端的网络连接
- Keep Alive 的值由具体应用指定,一般是几分钟。允许的最大值是 18 小时 12 分 15 秒
Will 遗嘱
- 遗嘱消息(Will Message)存储在服务端,当网络连接关闭时,服务端必须发布这个遗嘱消息,所以被形象地称之为遗嘱,可用于通知异常断线。
- 客户端发送 DISCONNECT 关闭链接,遗嘱失效并删除
- 遗嘱消息发布的条件,包括:
- 服务端检测到了一个 I/O 错误或者网络故障
- 客户端在保持连接(Keep Alive)的时间内未能通讯
- 客户端没有先发送 DISCONNECT 报文直接关闭了网络连接
- 由于协议错误服务端关闭了网络连接
- 相关设置项,需要在Connect时,由客户端指定
- Will Flag —— 遗嘱的总开关
- 0 -- 关闭遗嘱功能,Will QoS 和 Will Retain 必须为 0
- 1 -- 开启遗嘱功能,需要设置 Will Retain 和 Will QoS
- Will QoS —— 遗嘱消息 QoS
- 可取值 0、1、2,含义与消息QoS相同
- Will Retain —— 遗嘱是否保留
- 0 -- 遗嘱消息不保留,后面再订阅不会收到消息
- 1 -- 遗嘱消息保留,持久存储
- Will Topic —— 遗嘱话题
- Will Payload —— 遗嘱消息内容
消息基本概念
报文标识 Packet Identifier
- 存在报文的可变报头部分,非零两个字节整数 (0-65535]
- 一个流程中重复:这些报文包含 PacketID,而且在一次通信流程内保持一致:
- PUBLISH(QoS>0 时),PUBACK,PUBREC,PUBREL,PUBCOMP
- SUBSCRIBE, SUBACK
- UNSUBSCIBE,UNSUBACK
- 新的不重复:客户端每次发送一个新的这些类型的报文时都必须分配一个当前 未使用的PacketID
- 当客户端处理完这个报文对应的确认后,这个报文标识符就释放可重用。
- 独立维护:客户端和服务端彼此独立地分配报文标识符。因此,客户端服务端组合使用相同的报文标识符可以实现 并发 的消息交换。可能出现一下情况,并不算异常:
Payload 有效载荷,消息体
- 最大允许 256MB
- Publish 的 Payload 允许为空。在很多场合下,代表将持久消息(或者遗嘱消息)清空。
- UTF-8编码
Retain 持久消息(粘性消息)
- RETAIN 标记:每个Publish消息都需要指定的标记
- 0 —— 服务端不能存储这个消息,也不能移除或替换任何 现存的保留消息
- 1 —— 服务端必须存储这个应用消息和它的QoS等级,以便它可以被分发给未来的订阅者
- 每个Topic只会保留最多一个 Retain 持久消息
- 客户端订阅带有持久消息的Topic,会立即受到这条消息
- 服务器可以选择丢弃持久消息,比如内存或者存储吃紧的时候
- 如果客户端想要删除某个Topic 上面的持久消息,可以向这个Topic发送一个Payload为空的持久消息
- 遗嘱消息(Will)的Retain持久机制同理
QoS 服务等级(消息可靠性)
最多一次 At most Once(QoS == 0)
- 没有回复,不需要存储。有可能丢失(网络异常断开,业务层繁忙或者错误)
至少一次 At least Once(QoS == 1 )
- 发送者S 发送前需要做持久化存储,接受者R 不需要持久化存储
- 如果 发送者S 没有收到 接收者R 的回复 PUBACK,过一段时间 发送者S 会重新发送,DUP标记为1(在同一Session内)。
- 接受者R 发送 PUBACK 后,不需要知道对方是否收到,马上把消息交给上层业务。如果此时网络异常,会导致发送者重发。这样接受者收到多个消息(所以叫至少一次)。
有且仅有一次 Exactly Once(QoS == 2 )
- 发送者S 发送 PUBLISH 前,需要做持久化存储。接受者R 回复PUBREC 后,也需要做持久化存储
- 如果 发送者S 没有收到 接收者R 的回复 PUBREC,过一段时间 发送者S 会重新发送,DUP标记为1(在同一Session内)。
- 如果 接受者R 没有收到 发送者S 的回复 PUBREL,过一段时间 接受者R 会重新发送PUBREC。
- 发送者S 收到 PUBREC后,删除持久化消息,但是要保存 PacketID
- 接收者R 受到 PUBREL后,删除持久化PUBREC。然后将消息发给上层,同时回复 PUBCOMP。
- 发送者S 收到 PUBCOMP 后,删除 PacketID,通信完美结束。
- 这套流程可以 严格保证 一个包不管在什么情况下 接收者R 只收到一次 。
重传标记 DUP 与重传机制 (QoS > 0)
- 如果客户端或者服务器发送了一个 Publish 消息,一段时间内没收到 PublishAck 回复,则认为消息丢失,进行重传。
- 在一个Session内,进行重传的时候,头部的 DUP 重传标志 设置为1。
- 客户端有可能收到 DUP == 0 的重传包(Payload相同,PacketID不同)。因为可能因为网络问题,下次重传时间较久,Session已经释放,PacketID 已经变更。
- 客户端发给服务器的和服务器转发给别的客户端的 Publish 消息,DUP 重传标志不会传递
- 接收者收到一个 DUP 标志为 1 的控制报文时,并不能保证之前收到过相同的报文
消息重传顺序
- 重发任何之前的 PUBLISH 报文时,必须按原始 PUBLISH 报文的发送顺序重发 (适用于QoS 1 和 QoS 2 消息)
- 必须按照对应的 PUBLISH 报文的顺序发送 PUBACK 报文 (QoS 1 消息)
- 必须按照对应的 PUBLISH 报文的顺序发送 PUBREC 报文 (QoS 2 消息)
- 必须按照对应的 PUBREC 报文的顺序发送 PUBREL 报文 (QoS 2 消息)
- QoS == 1 时,虽然是PUBLISH有序的,但是可能会重复。例如,发布者按顺序 1,2,3,4 发送消息,订阅者收到的顺序可能是 1,2,3,2,3,4。
- QoS == 1 时,如果限制 传输窗口 (in-flight window) ==1,即同一时刻只有一个包在传输,就可以保证乱序。例如,订阅者收到的顺序可能是 1,2,3,3,4,而不是 1,2,3,2,3,4
- QoS == 2 时,肯定不会存在乱序的问题。
话题 与订阅机制 Topic & Subcribe
Topic 话题 和 TopicFilter 话题过滤器
- Pub-Sub消息模型的核心机制
- UTF-8 编码字符串,不能超过 65535 字节。层级数量没有限制
- 不能包含任何的下文中提到的特殊符号(/、+、#),必须至少包含一个字符
- 区分大小写,可以包含空格,不能包含空字符 (Unicode U+0000)
- 在收部或尾部增加 斜杠 “/”,会产生不同的Topic和TopicFilter。举例:
- “/A” 和 “A” 是不同的
- “A” 和 “A/” 是不同的
- 只包含斜杠 “/” 的 Topic 或 TopicFilter 是合法的
TopicFilter中的特殊符号
- 层级分隔符 /
- 用于分割主题的每个层级,为主题名提供一个分层结构
- 主题层级分隔符可以出现在 Topic 或 TopicFilter 的任何位置
- 特例:相邻的主题层次分隔符表示一个零长度的主题层级
- 单层通配符 +
- 只能用于单个主题层级匹配的通配符。例如,“a/b/+” 匹配 “a/b/c1” 和 “a/b/c2” ,但是不匹配 “a/b/c/d”
- 可以匹配 任意层级,包括第一个和最后一个层级。例如,“+” 是有效的,“sport/+/player1” 也是有效的。
- 可以在多个层级中使用它,也可以和多层通配符一起使用。 例如,“+/tennis/#” 是有效的。
- 只能匹配本级不能匹配上级。例如,“sport/+” 不匹配 “sport” 但是却匹配“sport/”,“/finance” 匹配 “+/+” 和 “/+” ,但是不匹配 “+”。
- 多层通配符 #
- 用于匹配主题中任意层级的通配符
- 匹配包含本身的层级和子层级。例如 “a/b/c/#" 可以匹配 “a/b/c”、“a/b/c/d” 和 “a/b/c/d/e”
- 必须是最后的结尾。例如“sport/tennis/#/ranking”是无效的
- “#”是有效的,会收到所有的应用消息。 (服务器端应将此类 TopicFilter禁掉 )
以$开头的,服务器保留
- 服务端不能将 $ 字符开头的 Topic 匹配通配符 (#或+) 开头的 TopicFilter
- 服务端应该阻止客户端使用这种 Topic 与其它客户端交换消息。服务端实现可以将 $ 开头的主题名用作其他目的。
- $SYS/ 被广泛用作包含服务器特定信息或控制接口的主题的前缀
- 客户端不特意订阅 $开头的 Topic,就不会收到对应的消息
- 订阅 “#” 的客户端不会收到任何发布到以 “$” 开头主题的消息
- 订阅 “+/A/B” 的客户端不会收到任何发布到 “$SYS/A/B” 的消息
- 订阅 “$SYS/#” 的客户端会收到发布到以 “$SYS/” 开头主题的消息
- 订阅 “$SYS/A/+” 的客户端会收到发布到 “$SYS/A/B” 主题的消息
- 如果客户端想同时接受以 “$SYS/” 开头主题的消息和不以 $ 开头主题的消息,它需要同时 订阅 “#” 和 “$SYS/#”
订阅 Subscribe 与 QoS降级
- 订阅机制基于TopicFilter匹配
- 一个Subsribe请求 可订阅多个 Topic(节省带宽,多订阅尽量用一次请求)。取消订阅也同理
- 每一个订阅需要指定一个QoS,指定了客户端接收消息所允许的最大QoS级别。但是服务器端最终授权返回的QoS可能会小于等于客户端请求的QoS
- 对于高于QoS的消息(比如说订阅的QoS限制到1,消息的QoS指定到2),那么客户端会收到一个QoS降低为指定的 限制QoS 的消息(消息的QoS降为1,不保证只收到一次)
- 订阅关系可以被覆盖,以TopicFilter为标识。如果后面订阅一个相同的TopicFilter,但是指定的QoS不同,则以后面的为准,QoS升高后,重发相应等级的 Retain 消息
安全传输与鉴权认证 Security & Certification
传输层
- 可以采用 TCP、SSL/TLS [RFC5246] 、WebSocket 作为传输层。UDP不可以,因为不保证可靠传输与有序传输。
- 服务器端返回的数据极有可能出现 粘包 的情况。客户端经常会在连接建立之后,连续调用多个订阅,这样服务器端就会回复多个订阅ACK包,同时还有各个Topic上的持久消息,一般粘成一个TCP包返回过来
- 端口(IANA分发)
- 1883:over TCP,无加密
- 8883:over SSL/TLS,单向认证(强烈建议)
- 8884:over SSL/TLS,双向认证
- 8080:over WebSockets,未加密
- 8081:over WebSockets,加密
- 可使用SOCKS代理,可利用安全隧道(如SSH)
潜在的风险与应对机制
- 潜在风险
- 设备可能会被盗用
- 客户端和服务端的静态数据可以被访问(比如客户端Root导致数据泄露、服务器被拖库)
- 协议规定的行为可能有副作用 (如计时器攻击 “timing attacks”)
- 拒绝服务攻击(DoS)
- 通信可能会被拦截、修改、重定向或者泄露(抓包、中间人)
- 虚假控制报文注入
- 应对的机制
- 用户和设备身份认证
- 服务端资源访问授权
- 控制报文和 Payload 的完整性校验
- 控制报文和 Payload 的隐私控制
客户端身份验证与授权 (Authentication & Authorization of Client)
- 用户名+密码验证:Connect 登录的时候,传入 UserName 和 Password
- 相关标记位:在Connect时,由客户端设置
- 用户名(UserName Flag)标记设置为1,才可以穿入
- 密码(Password Flag)标记设置为1
- 外部验证:LDAP、OAuth 或者 操作系统的认证机制
- 用户名密码加密:防止中间人攻击和重放攻击
- 应用层:客户端通过应用消息给服务端发送凭证用于身份验证。
- 授权:基于客户端提供的信息如用户名、客户端标识符(ClientId)、客户端的主机名或 IP 地址,或者身份认证的结果,服务端可以限制对某些服务端资源的访问
服务端身份验证 (Authentication of Server by Client)
- MQTT 协议不是双向信任的,它没有提供客户端验证服务端身份的机制
- TLS:客户端可以使用服务端发送的SSL证书验证服务端的身份
- 应用层:可以通过服务端给客户端发送凭证用于身份验证的应用层消息
- VPN:在客户端和服务端之间使用虚拟专用网(VPN)可以确保客户端连接的是预期的服务器。
控制报文和 Payload 的完整性(Integrity)
- TLS:提供了对网络传输的数据做完整性校验的哈希算法
- 应用层:可以在应用消息中单独包含哈希值。这样做可以为 PUBLISH 控制报文的网络传输和静态数据提供内容的完整性检查
- VPN:在客户端和服务端之间使用虚拟专用网(VPN)连接可以在 VPN 覆盖的网络段提供数据完整性检查
控制报文和 Payload 的保密性(Privacy)
- 轻量级加密:AES or DES,可适用于低端设备
- TLS:可以对网络传输的数据加密
- 应用层:可以单独加密 Payload 内容。这可以提供 Payload 传输途中和静态数据的私密性。但不能给应用消息的其它属性如 Topic 加密
- 静态数据加密:客户端和服务端实现可以加密存储静态数据,例如可以将应用消息作为会话的一部分存储
- VPN:在客户端和服务端之间使用虚拟专用网(VPN)连接可以在 VPN 覆盖的网络段保证数据的私密性
异常行为的检测
- 服务端实现可以监视客户端的行为,检测潜在的安全风险。例如:
- 重复的连接请求
- 重复的身份验证请求
- 连接的异常终止
- 主题扫描 (请求发送或订阅大量主题)
- 发送无法送达的消息 (没有订阅者的主题)
- 客户端连接但是不发送数据
- 应对策略
- 发现违反安全规则的行为,服务端实现可以断开客户端连接
- 可以基于 IP地址 或 ClientID 实现一个 动态黑名单列表
- 可以使用网络层面的控制,实现基于 IP 地址或其它信息的 速率限制 或黑名单
- 连接拒绝与错误码
最佳实践 Best Practice
客户端 Client
选型
- Android
- iOS
- Javascript
- PHP
- Java
服务器端 Server
Broker选型
- Mosca
Benchmark