MQTT与WebSocket深度对比:消息协议与传输协议的边界与融合
1. 从单向消息推送的旧方案说起为什么要把 MQTT 和 WebSocket 放到同一篇笔记里我整理这篇计算机理论文档阅读笔记的起因是一次很典型的选型争论某块业务需要“服务器主动给客户端推送数据”团队里一部分人提 MQTT另一部分人提 WebSocket。两边都能说出理由但争论一直往下走的时候就发现双方根本不是在同一个抽象层上讨论问题。MQTT 用户关心的是“消息有没有可靠送达、离线之后怎么办、Broker 能不能帮忙缓存”WebSocket 用户关心的是“浏览器和服务器之间能不能建立一条低延迟的双向通道、帧数据怎么解析”。一个站在消息语义层一个站在传输通道层硬放在一起对比会越对比越混乱。这篇笔记不是传统意义上的读后感更像是我带着问题去回读协议规范、技术通稿以及一些内部讨论文档之后把容易混淆的知识点重新拆开一遍的记录。标题虽然是“MQTT vs WebSocket”但最后我得到的结论并不是“谁替代谁”而是“两者面向的问题域不同甚至经常出现在同一条技术链路上”。先给一个基础判断MQTT 属于消息协议核心建模对象是“消息的发布者、订阅者、主题、服务质量”WebSocket 属于传输类协议核心建模对象是“客户端与服务器之间的一条持久连接、全双工数据帧”。这句话看起来简单但它是整篇笔记的骨架。后面所有关于报文格式、握手方式、重连机制、应用场景的对比都在给这一句话做注脚。如果把这篇笔记当成一份导览我要记录的核心问题有这么四个第一为什么 MQTT 的文档会花大量篇幅描述“连接保活”“遗嘱消息”“会话状态”第二为什么 WebSocket 的文档几乎不讨论消息可靠性第三两者在真实链路上的字节开销与行为差异到底有多大第四工程上如何同时使用它们。2. MQTT 文档精读低带宽、高延迟、弱连接的发布/订阅协议2.1 阅读提纲为什么 MQTT 规范的行文顺序是“连接优先消息其次”我在读 MQTT 相关文档时第一个强烈感受是它开篇没多久就开始讲“客户端建立连接”“心跳保活”“遗嘱消息”而不是像很多应用协议那样直接进入“消息格式说明”。这个顺序本身就是在告诉你MQTT 面对的典型网络环境是老旧的、不稳定的、高延迟的物联网链路。设备可能一天掉线很多次可能长时间处于休眠状态可能所在网络的 NAT 映射会周期性失效。如果协议设计者不先把“连接怎么活着”这条路走通后面再设计多少消息格式都是白搭。文档里对“遗嘱消息Will Message”的定位我重读了三遍才真正理解客户端在建立连接时可以声明一个遗嘱主题和遗嘱内容。如果客户端在异常断线时没来得及清理连接Broker 就会代为发布这条遗嘱消息。典型用途是设备掉线告警比如一个温度传感器网络里设备 A 在连接建立时设置遗嘱“设备A离线”当设备 A 长时间没有心跳而 Broker 判定其连接断开时订阅该主题的其他端就收到这条自动消息。这里最关键的地方在于遗嘱不是客户端主动发的而是由 Broker 代表客户端发的它把“客户端异常消失”这件事转变成了“一条可被订阅者感知的消息”。“连接保活”的设计逻辑也值得细看。文档要求客户端在两个相邻的控制报文之间维持一个最大时间间隔默认场景下大约 60 秒。如果客户端在这个间隔内没有发出任何其他控制报文就必须发送一个专用的心跳报文。Broker 如果在超过一定时间后没有收到任何报文就可以判定连接失效进而触发遗嘱消息等清理动作。这个机制解决的核心矛盾是底层 TCP 连接可能在路由器、防火墙或运营商策略的干预下已经静默断开了但应用层还不知道。定期心跳相当于双方约定好“如果你还在请定期告诉我”而不是让一端一直傻等。用大道理套一下MQTT 文档的结构是在反复表达一个理念消息可靠性必须建立在“对连接生命周期的认真管理”之上。没有持久连接的健康度就谈不上消息能否被稳妥地送达。2.2 QoS 0/1/2这三种等级不是“快慢”的区别而是可靠性契约的差异MQTT 文档里另外一个容易让人误解的概念是服务质量等级 QoS。我第一次接触时直觉是 QoS 0 快、QoS 1 不快不慢、QoS 2 慢但最安全这个说法本质上没错但它把学习重点带偏离了。文档想表达的是三个等级分别解决了不同的重复与丢失问题而不是简单的速度档位。QoS 0 意味着“最多一次”发送方丢出消息之后不再关心结果。适用于丢失可容忍的周期读数比如温度传感器每一分钟上报一次偶尔丢一拍完全不影响后续趋势判断。这个等级在三个等级里几乎没有额外报文字段是最小成本的选择。QoS 1 意味着“至少一次”发送方需要等待接收方返回确认应答。如果发送方没有及时收到确认就会重发消息。这个方案带来的副作用是接收方可能收到重复消息。协议文档并没有帮你把重复数据去重它只是保证消息不丢除非你在应用层做幂等处理否则重复是一条常态。换句话说QoS 1 的真实语义是“我知道你可能收到两遍但我至少要确保你收到一遍”。QoS 2 经常被过度神话。它被表述为“恰好一次”但协议实现时并不是靠一个确认包走完的而是采用了两轮报文交换。第一轮是 PUBLISH接收方返回 PUBREC第二轮是发送方发送 PUBREL接收方返回 PUBCOMP。为什么需要两轮因为要在发送方、接收方各自的状态机里把消息的发送与确认变成可恢复的要处理报文丢了之后的重发还要保证即使重发也不会被重复递交给应用层。这个设计的代价就是报文数量更多、交互复杂度更高在嵌入式低功耗设备上必须谨慎使用。所以我在笔记里写的自己的简要结论是QoS 0 适合高频低价值数据QoS 1 适合默认数据通道QoS 2 适合“一份命令只能执行一次”的高价值指令。但高价值指令如果落在 MQTT 上即使选了 QoS 2应用层仍然要做好幂等兜底因为协议保证的是投递层面的语义不等于业务层面的“只执行一次”。这一点文档读得越细越能体会到。2.3 额外隐藏在规则里的细节保留消息、订阅过滤与 session 状态MQTT 规范里还有一批很实用但容易被博客文章一笔带过的规则。保留消息是其中之一发布者可以在发布消息时标记 retain 位Broker 会把这个消息保留在对应主题上。后续新订阅者接入后不必等下一次新数据Broker 会立即把这条保留消息推给新订阅者。这个机制对“设备状态同步”特别合适比如一个智能网关上线后订阅一个设备状态主题如果该主题有保留消息网关第一时间就能知道当前状态而不是干等下一次状态变化。订阅主题里的通配符规则也有讲究。MQTT 的主题名是分层的用/分层比如home/livingroom/temperature。订阅方可以用匹配单层、用#匹配多层。我在文档阅读笔记里专门标注过一个容易踩的细节不是所有消息系统都允许#出现在主题中间MQTT 规定#必须是最后一个字符而且#匹配的是“父层以及之后的所有层”语义上和正则里的*有差别。理解这一点在写设备端订阅逻辑时能省不少调试时间。session 状态也是文档契约里非常重要的一环。客户端建立连接时有一个 clean start 概念如果 clean start 为真Broker 不保留客户端的会话状态如果为假Broker 会保存客户端的订阅关系、未确认消息、离线消息等内容。简单类比这就是“关掉 App 之后之前订阅还在不在”的问题。在弱网络环境里设备经常离线重连如果每次重连都清空 session重连后订阅关系需要重新建立断线期间的消息就拿不到了。所以文档建议对“重要设备”使用持久会话。但持久会话也有代价Broker 需要为每个离线客户端维护状态设备数量一大内存和存储压力会明显上升需要提前规划。我个人的阅读体会是MQTT 文档表面上在讲报文、主题、握手实际是在讲一套“边缘网络下如何优雅处理不确定性”的套路。设备会掉线、链路会抖动、消息会重复、顺序会乱协议能做的不是假设网络完美而是把这些不确定性变成可量化、可选择的机制。3. WebSocket 文档精读一个把“升级、全双工、帧”逐段展开的协议规范3.1 握手阶段HTTP Upgrade 和随机 Sec-WebSocket-Key 的作用WebSocket 的文档阅读路径和 MQTT 完全相反。MQTT 里的客户端一上来就要发 CONNECT 报文而 WebSocket 的第一步是通过 HTTP 完成“升级”。客户端发送一个带 Upgrade 头部的 HTTP 请求比如Upgrade: websocket服务端如果支持则返回101 Switching Protocols之后这条 TCP 连接不再按 HTTP 语义工作而是进入 WebSocket 帧传输状态。我读文档时特别留意了其中Sec-WebSocket-Key字段的设计意图。客户端会生成一段随机值服务端把它和一段固定字符串拼接后做哈希再把结果通过Sec-WebSocket-Accept返回给客户端。很多人把它当成“加密认证”其实它更重要的作用是让两端都能确认“对方真的理解了 WebSocket 协议”避免因为代理或缓存服务器把普通 HTTP 与 WebSocket 搞混。它解决的是一种“协议升级后的语义确认”问题而不是身份认证问题。真正认证用户身份的逻辑仍需在应用层通过 token、cookie 等机制完成。这个握手过程还带来一个值得记住的工程约束WebSocket 连接必须从客户端发起而且通常走 80 或 443 端口。规范没有规定只能从浏览器使用但在实际部署里浏览器被放开是最核心的诉求。所以 WebSocket 文档里大篇幅围绕“浏览器、同源策略、代理穿透”做描述我一点都不意外。它是带着“被 HTTP 世界接纳”的包袱设计出来的和 MQTT 这种纯为消息流而生的协议出发点完全不同。3.2 数据帧与分片为什么会要求 16 bit 长度和掩码握完手之后WebSocket 规范就进入了数据帧说明。我先用一个最简拆法来描述单帧结构先是 FIN 位和 opcode用于标识帧是否结束、当前帧是二进制还是文本然后是 mask 位和 payload len如果长度较大会走扩展长度字段再接着是可选掩码密钥最后才是真正要传输的 payload。这个结构表面复杂但设计意图很清晰解析器不需要依赖外部上下文只靠帧头几个字节就能知道“这一帧数据有多长、后面要不要继续拼接”。为什么要允许分片关系文档中说发送方可以把一个消息拆成多个帧只有最后一个帧的 FIN 位置 1。这个设计对“不知道总长度的大消息”非常有用。浏览器生成本地文件上传或者长文本发送时可以一边读取数据一边发送不需要等整块数据全部进内存。对于接收方只要按照 opcode 与 FIN 位把多个帧重新拼接就可以还原完整消息。这个机制给应用层留了自由度也意味着如果你不做特殊处理接收方拿到的“一条消息”可能是由好多个独立帧拼出来的。还有一个容易让初学者产生疑问的点为什么规范要求浏览器客户端发送的所有帧都必须做掩码处理看起来多了一串随机 4 字节密钥和一次逐字节异或操作什么收益都没有。实际上这个设计是被早期 Web 环境的“缓存污染风险”逼出来的。当浏览器发起 WebSocket 连接时网络路径上可能有 HTTP 缓存设备它们可能把数据误认为 HTTP 响应缓存下来后续被其他请求读取到敏感内容。客户端掩码伪装了载荷让缓存设备无法通过模式识别确定数据含义从而降低这种风险。从这一点能看到 WebSocket 文档的设计逻辑它必须在一个充满中间设备、兼容性和安全焦虑的 Web 环境里工作所以“防御性设计”比“极致简洁”更重要。这种取舍放到 MQTT 文档里是找不到的。3.3 浏览器接入决定了它的设计边界没有离线队列没有消息级 QoS读完 WebSocket 文档最容易被反问的一个问题是它为什么不提供发布/订阅模型为什么不提供 QoS答案是WebSocket 的抽象层级根本不负责这些。WebSocket 的定位更接近一个“通用双向字节管道”你建立一条连接就可以在任意时刻双向传输字节。至于这些字节是一行命令、一条聊天消息、一段二进制压缩流它不关心。它也不负责“当你断线时把数据缓存下来等重连后继续”。规范里虽然有 ping/pong 控制帧但 ping/pong 更多是用于探测连接是否还活着不是为了消息可靠投递。一旦连接断开操作系统和网络环境会把它当作普通 TCP 连接销毁之前传输中的消息如果没有被应用层确认过就当作丢失处理。所以我在阅读笔记里给 WebSocket 盖了一个总结它把“持续连接上的双向帧传输”这件事做到了标准统一但把“消息语义、路由、重试、离线补发”全部留给了应用层。这不代表它不好而是说任何工程判断离开了这个边界就会变得奇怪。比如用 WebSocket 在后端服务之间做海量消息队列等于重新实现一个消息中间件代价不小。也正是因为这样我在实际做技术对比时从来不会把“WebSocket 能否替代 MQTT”当成一个可以一句话回答的问题。在浏览器与服务器之间替代 TCP 做全双工通道WebSocket 是自然选择在设备与云端之间做弱网消息可靠传输MQTT 是自然选择。它们两边的文档其实已经各自把边界画得很清楚了。4. 一帧一包对照MQTT 和 WebSocket 在传输层面的真实差异4.1 MQTT 的一次 PUBLISH 报文由哪几块组成文档读得再深最后还是要落到字节层面看开销。这里我用一个最简单的情景客户端发布一条 payload 只有 5 字节、主题名只有 6 字节的消息。假如使用 QoS 0MQTT 报文结构大概是这样固定头一个字节包含报文类型和标志位加上一个字节的剩余长度。变量头主题长度占 2 字节主题内容占 6 字节。载荷5 字节。去掉主题本身协议附加开销大概是固定头 2 字节加上主题长度字段 2 字节一共 4 字节。主题内容 6 字节本质上是业务地址不能算纯空耗但也不能完全无视。整个 publish 消息从 MQTT 客户端视角看大约就是 9 字节 payload 相关部分加 4 字节控制开销。如果改用 QoS 1报文里还要增加一个 2 字节的消息标识符用于匹配 PUBLISH 与 PUBACK同时发送后还要等待一条 PUBACK 应答链路往返次数增加。QoS 2 则要额外经历 PUBREC、PUBREL、PUBCOMP控制开销和交互次数都会进一步增加。所以仅从“单条消息的最小字节成本”看MQTT 已经压缩得相当小。在智能设备那种动辄几百字节一条消息的环境里这是非常有价值的优势。另外需要提一个文档里常常用示例强调的细节MQTT 的固定头里剩余长度字段使用变长编码可以占 1 到 4 个字节。当消息载荷达到一定阈值后这个可变字段会额外增加一个字节。计算带宽时不要永远按最小值估要在前端设计时给变长开销留出余量。4.2 WebSocket 发送一帧数据时实际在链路里走了什么WebSocket 单帧的字节开销可以这样估算如果是一帧文本消息载荷小于 126 字节帧头是 FIN/opcode 一个字节、mask 位和长度一个字节、掩码密钥 4 字节。如果服务端发送数据给客户端规范允许服务端不做掩码则可以省掉 4 字节如果是从浏览器客户端发往服务端掩码密钥 4 字节就省不掉。所以客户端发送载荷 5 字节的文本帧时最小帧头可能是 6 字节总线上大约是 11 字节服务端发送同样载荷时可能只需要 2 字节帧头加上 5 字节载荷总共 7 字节。如果再叠加 HTTP 握手时的开销两者的差异就被放大了。WebSocket 建立连接需要先完成一次 HTTP Upgrade 请求与 101 响应这段数据通常有几百字节还包括 Cookie、认证头等业务字段。MQTT 的 CONNECT 报文虽然也要携带 ClientID、用户名等信息但在规模上通常比 HTTP 头瘦身不少。因此如果链路极其关注“每次连接的建立成本”WebSocket 在握手环节并不便宜。不过这里要和前面 MQTT 的附加开销区分开MQTT 的一次发布是一次性动作发布者不需要先升级什么WebSocket 的数据发送是在连接维持期间反复利用握手成果。所以在长期存在的连接上WebSocket 的每帧开销并不高真正的成本在低频场景下被平摊后通常可以接受。这也是为什么我始终强调脱离“连接时长”去比较报文大小没有太大意义。4.3 握手、再连接、保活三张对比表的具象化为了把前面分散的细节收敛起来我在笔记的最后部分画了几张对比表。这里的“画表”不是指流程图而是用表格把关键差异列清楚方便回查。维度MQTTWebSocket抽象层级消息层主题、发布、订阅、服务质量传输层双向字节帧通道连接建立客户端发 CONNECT服务端回 CONNACK客户端发 HTTP Upgrade服务端回 101路由能力有主题树和通配符订阅天然多对多一对一连接路由需应用层实现离线能力持久会话可缓存订阅与离线消息默认不保留任何离线数据消息可靠性原生 QoS 0/1/2 可选无消息级可靠性需应用层补齐保活机制控制报文级别的 ping超时触发遗嘱ping/pong 控制帧只探测存活典型运行位置设备、网关、服务端浏览器、App、Web 服务端再做一张“失败处理”的比较表会更有意思。MQTT 里设备断线重连后可以恢复之前未完成的消息流程WebSocket 里连接断开就意味着之前的帧序列终止重连后要从零开始。这直接影响到架构设计一个依赖 MQTT 的设备可以“断线继续干活”一个依赖 WebSocket 的聊天前端则更依赖应用层的“断线重连状态机”。实际抓包场景里MQTT 的连接失败通常会表现为 CONNACK 返回错误码比如协议版本不匹配、客户端标识符被占用、认证失败等。WebSocket 握手失败则是 HTTP 状态码体现比如 403、404或者干脆是同一个端口上的普通 HTTP 响应。排障时看到这两类响应就要立刻意识到自己站在哪一层前者在说“MQTT 会话被 Broker 拒绝”后者在说“HTTP 网关没有完成协议升级”。5. 从文档回到工程选型什么时候用哪个协议或者干脆两层都用5.1 一个可复用的混合架构浏览器走 WebSocket后端与设备走 MQTT读完这两类文档后我在实际系统里最常用的是一种混合架构。大致链路是设备与后端服务之间维持 MQTT 连接后端服务把收到的设备消息写入一个内部消息总线当某个浏览器页面需要实时展示设备状态时浏览器先通过 WebSocket 连上一个实时推送服务实时推送服务内部订阅 MQTT 上的对应主题再把消息转发成 WebSocket 帧推给页面。这个架构的好处是把两个协议变成各自擅长的段。设备侧强网络不确定MQTT 的 QoS、持久会话、遗嘱机制正好兜底浏览器侧必须走 Web 标准链路WebSocket 从握手到帧解析都有成熟支持。后端可以按主题把 MQTT 消息映射成 WebSocket 推送也可以反过来把浏览器发来的控制指令转成 MQTT 消息下发到设备。唯一要小心的是在桥接结构里做协议转换时的“语义损耗”。比如 MQTT 有 QoS 2但浏览器端 WebSocket 没有对应机制。如果一条控制指令从浏览器发出经 WebSocket 传到后端再转换成 MQTT QoS 1 消息下发整个链路的可靠性等同于最低那一环还是最高那一环取决于后端如何处理。通常的做法是WebSocket 客户端在发送指令后等待应用层 ACK服务端在确认 MQTT 消息已经成功发布之后再回这一个 ACK。这个 ACK 与 MQTT 本身的 PUBACK 是两回事需要单独设计。这是文档阅读笔记很容易忽略的地方但恰恰是线上事故多发的根源。5.2 选型决策清单把阅读笔记变成可复查的判断题我给自己整理过一个选型决策清单核心只有几个问题。第一消息是否需要在中转节点上长期保存为离线者补发如果是MQTT 的持久会话和 retained 消息明显更合适。第二通信双方是否天然是“一人发、多人订阅”的关系如果是MQTT 的主题发布订阅模型可以直接复用。第三客户端是否被限制在浏览器环境几乎只要选择后端实时推送到页面就必须考虑 WebSocket或者基于 HTTP 的流式方案但那又是另一套兼容性预算。第四是否需要在同一根连接上既传输用户数据又维护消息属性如果是轻量化设备上报MQTT如果在浏览器里频繁交换二进制、文本混合数据WebSocket。这套清单不是真理但它非常有助于避免“新技术光芒”带偏判断。读协议文档时最容易产生一种错觉既然协议 A 提供了好用的能力那用了 A 就能解决所有问题。实际上MQTT 和 WebSocket 之间不存在“谁比谁更高级”它们只是两个不同时期、不同背景下为不同问题而生的协议。放到工程里最好的选型往往是“在明确的边界内使用而不是在死胡同里辩论”。5.3 我在阅读这几份文档之后的几个直接收益最后写一点偏个人感受的收益这部分其实比任何知识总结都更接近“阅读笔记”的本意。当我再把 MQTT 文档里关于会话状态的部分和 WebSocket 文档里关于帧结构的部分放到一起看时我意识到一个协议的设计者最先解决的问题不是“如何实现更多功能”而是“我的用户处于什么环境我最应该让用户不用操心什么”。MQTT 为了弱网设备把连接存活管理做进协议核心WebSocket 为了 Web 中间设备把掩码与升级设计拧进规范细节。各自的付出都很有必要。对我自己的实践来说这套笔记带来的直接变化是做技术方案时不会再把“用 MQTT 还是 WebSocket”当成一个非此即彼的单选而是先把业务场景拆成“设备端、服务端、展示端”三个段落再分别考察每一段的网络约束、可靠性要求、客户端形态最终很可能得出结论是“你两个都要”。这种并存的答案比“只有一个能胜出”更有工程价值。如果让我给正在读类似文档的人一条建议我会说不要只背“MQTT 是推消息的、WebSocket 是双向的”这种速记结论。去把两边的握手指、报文头、保活字段重新读一遍你会发现两个协议每一种机制背后都有明确的历史问题和网络困境理解了那些困境选型时才能真正不糊涂。