基本概念 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证书验证服务端的身份 应用层:可以通过服务端给客户端发送凭证用于身份验证的应用层消息 V**:在客户端和服务端之间使用虚拟专用网(V**)可以确保客户端连接的是预期的服务器。 控制报文和 Payload 的完整性(Integrity) TLS:提供了对网络传输的数据做完整性校验的哈希算法 应用层:可以在应用消息中单独包含哈希值。这样做可以为 PUBLISH 控制报文的网络传输和静态数据提供内容的完整性检查 V**:在客户端和服务端之间使用虚拟专用网(V**)连接可以在 V** 覆盖的网络段提供数据完整性检查 控制报文和 Payload 的保密性(Privacy) 轻量级加密:AES or DES,可适用于低端设备 TLS:可以对网络传输的数据加密 应用层:可以单独加密 Payload 内容。这可以提供 Payload 传输途中和静态数据的私密性。但不能给应用消息的其它属性如 Topic 加密 静态数据加密:客户端和服务端实现可以加密存储静态数据,例如可以将应用消息作为会话的一部分存储 V**:在客户端和服务端之间使用虚拟专用网(V**)连接可以在 V** 覆盖的网络段保证数据的私密性 异常行为的检测 服务端实现可以监视客户端的行为,检测潜在的安全风险。例如: 重复的连接请求 重复的身份验证请求 连接的异常终止 主题扫描 (请求发送或订阅大量主题) 发送无法送达的消息 (没有订阅者的主题) 客户端连接但是不发送数据 应对策略 发现违反安全规则的行为,服务端实现可以断开客户端连接 可以基于 IP地址 或 ClientID 实现一个 动态黑名单列表 可以使用网络层面的控制,实现基于 IP 地址或其它信息的 速率限制 或黑名单 连接拒绝与错误码
最佳实践 Best Practice 客户端 Client 选型 Android iOS Javascript PHP Java 服务器端 Server Broker选型 Mosquitto ActiveMQ RabbitMQ HiveMQ VerneMQ EMQ Apollo (from ActiveMQ) RocketMQ 商用版 Mosca Benchmark Mosquitto Apollo Rabbit MQ Mosca VerneMQ 测评 RabbitMQ vs ActiveMQ RocketMQ vs Kafka(18项差异) RocketMQ vs Kafka vs RabbitMQ 消息中间件的对比 —— 消息发送性能 业界主流MQ对比 分布式部署 Cluster TopicTree 设计 参考 HiveMQ: mqtt-essentials-part-5-mqtt-topics-best-practices