01

總覽與定位

WebSocket 篇把「通道」做到極致;本篇的協定從第一行規格開始,就沒打算只當一條通道。

WebSocket 篇的最後留下一份帳單。WebSocket 這套設計預設你有完整的 TLS 與 HTTP 堆疊、一顆可以常時醒著跑心跳的 CPU,以及不計較每個位元組的頻寬;而且它從頭到尾只負責把位元組送到對面——訊息會不會重複、掉線那段時間的訊息去了哪裡、裝置突然斷電時誰替它說最後一句話,協定一概不管。

把這三個「不管」擺到一顆靠兩顆三號電池撐三年、掛在 NB-IoT 上、每天要被電梯與地下室吞掉十幾次連線的感測器面前,它們就不再是「應用層自己處理一下」的細節,而是會決定整個系統能不能成立的東西。MQTT(Message Queuing Telemetry Transport) 的選擇是把它們全部搬進協定:一則訊息的投遞保證、離線期間的訊息去向、以及裝置死亡時的公告,都由規格本身定義,而不是由你寫的第 3000 行 JavaScript 定義。

WebSocket:想當通道
點對點 · 對稱 · 無中介
兩端直連,協定只保證訊息邊界。誰收得到、收幾次、沒收到怎麼辦,全部是應用的事。
MQTT:想當訊息系統
Broker 中介 · pub/sub · 帶 QoS
所有人都只跟 Broker 說話。發佈者不知道誰在聽、聽的人不必在線上,投遞保證由協定分級定義。
兩者不是對立
MQTT over WebSocket
瀏覽器裡要跑 MQTT,走的就是 WebSocket 當 transport(子協定 mqtt)。一個是通道、一個是通道上的語義。
座標MQTT 在堆疊裡的位置:與 HTTP、WebSocket 平級的應用層協定

MQTT 是 OASIS 標準(目前主流是 3.1.1 與 5.0),跑在 TCP 之上,預設 port 1883。名字裡的 “Message Queuing” 是歷史遺留——它不是 JMS 那種佇列協定,也跟任何 MQ 產品沒有繼承關係;它 1999 年被設計出來時要解的問題是「用衛星鏈路把油管上的 SCADA 資料送回來,而衛星頻寬按位元組計費」。這個出身決定了它後來的每一個設計取捨。
加密版本沿用 TLS 篇的說法:MQTTS = MQTT over TLS,port 8883,常搭配 mTLS 做裝置身分(ALPN 值 mqtt)。而在瀏覽器裡,MQTT 走的是 WebSocket 當 transport——這是本篇與 WebSocket 篇真正的接點,第 6 節會說清楚兩者的分工。

1 · WebSocket 篇那張生命週期表的右欄,一半被搬進了協定

WebSocket 篇的總覽一節用一張生命週期表列出「協定提供什麼/你必須自己做什麼」,並在 caption 裡預告了本篇。現在把那張表的右欄拿出來,逐格對照 MQTT 的答案:

問題WebSocket 的答案MQTT 的答案本篇章節
訊息要送給誰你自己在 payload 裡塞 channel 欄位,伺服器自己維護訂閱表Broker 按 topic 路由,訂閱用 wildcard 表達第 2 節 · Broker 與 topic
訊息會不會重複/掉不管(TCP 只保證這條連線上不亂不掉)QoS 0 / 1 / 2 三級投遞保證,寫在協定裡第 3 節 · QoS
剛上線的人怎麼知道現況自己另外拉一次快照 APIRetained message:訂閱當下立刻收到最後一則第 4 節 · Retained
掉線那段時間的訊息自己做序號補發或 resume tokenPersistent session:Broker 替離線的你排隊第 4 節 · Persistent session
裝置突然斷電對面只會看到 1006,沒有人知道原因Last Will:Broker 代發你事先寫好的遺囑第 4 節 · Last Will
連線還活著嗎Ping/Pong control frame,週期與判定全自訂Keep Alive 寫進 CONNECT,逾時門檻由規格定死第 5 節 · Keep Alive

左欄不是在說 WebSocket 做錯了——它刻意只當通道,所以能被任何應用語義複用。右欄則是用「綁定一種語義」換「不必再寫一次」:一旦你的訊息模型不是 pub/sub,MQTT 的這些好處全部作廢。

2 · 一個貫穿全篇的視角:device constraints

本篇每一節都會回到同一組限制上,它們解釋了 MQTT 幾乎所有「看起來很小氣」的設計:

限制面向具體樣貌它逼出 MQTT 的哪一個設計
電池每次喚醒都要付電固定標頭最小 2 bytes;Keep Alive 可拉長到分鐘級;QoS 0 零往返,送完就能睡
頻寬按位元組計費沒有冗長標頭、沒有每次請求都要重送的 metadata;5.0 的 Topic Alias 再把長 topic 壓成兩個位元組(見第 5 節)
網路時斷時續persistent session 替離線裝置排隊;Last Will 讓 Broker 代發死亡公告(見第 4 節)
規模十萬~百萬條長連線裝置一律主動建長連線(外往內預設不通),Broker 單點承接;路由靠訂閱樹比對,不必逐一定址
運算MCU 等級,沒有 JSON 解析器payload 對協定完全不透明(愛放什麼二進位格式都行);客戶端狀態機小到塞得進韌體

右欄才是重點:MQTT 幾乎每一個「看起來很小氣」的設計,都能在左欄找到它的來源。

最直接的體現是固定標頭最小只有 2 bytes(1 byte 的封包型別+旗標,加上 1–4 bytes 的變長 Remaining Length)。放進本系列的脈絡:HTTP 篇的一個 HTTP 請求光標頭就是好幾百 bytes(HPACK 之後仍有動態表成本)、HTTP/2 的 frame 標頭固定 9 bytes、WebSocket 篇的 WebSocket frame 最小 2 bytes——MQTT 與 WebSocket 打平,但它用這 2 bytes 換到的是「一整套訊息語義」,而不只是「一個訊息邊界」。

陷阱「2 bytes 標頭」是行銷話術的一半

2 bytes 是固定標頭。一則 QoS 0 的 PUBLISH 實際上還要帶「2 bytes 的 topic 長度 + topic 字串本身」;QoS 1/2 再加 2 bytes 的 Packet Identifier。所以當你的 topic 長成 factory/taipei/line-3/robot-07/telemetry/temperature(50 bytes)而 payload 只有 4 bytes 的溫度值時,協定開銷佔了九成以上。這正是 MQTT 5.0 要引入 Topic Alias 的原因(見第 5 節)。談頻寬時請算完整封包,不要只念固定標頭那個數字。

一句話答法MQTT 是跑在 TCP(1883)或 TLS(8883)上的 pub/sub 協定,用一個 Broker 做中介,把投遞保證(QoS)、離線訊息(persistent session)、現況快照(retained)與裝置死亡公告(Last Will)直接寫進協定;WebSocket 想當通道,MQTT 想當訊息系統,而在瀏覽器裡 MQTT 就是跑在 WebSocket 上。
02

Broker 與 topic:一個中介,換三種解耦

MQTT 沒有「客戶端連客戶端」這回事。所有人都只認識 Broker——而這件事買到的東西比看起來多。

MQTT 的拓撲極其單純:每個客戶端只跟 Broker 建一條長連線,發佈者把訊息 PUBLISH 給 Broker,Broker 決定要複製給哪些訂閱者。發佈者永遠不知道有沒有人在聽、有幾個人在聽、聽的人是不是同一批。這種「刻意讓兩端互相不認識」正是 pub/sub 的核心價值,通常拆成三層:

解耦維度意思對 IoT 為什麼關鍵
空間解耦雙方不需要知道對方的位址裝置在 NAT/行動網路後面,位址每天換好幾次(TCP/UDP 篇已定調「外往內預設不通,裝置一律主動建長連線」)
時間解耦雙方不需要同時在線感測器一天只醒 4 次;後台服務要重啟部署。錯開在 IoT 是常態不是異常
同步解耦發佈是非阻塞的,不等對方處理完MCU 沒有餘裕維護「等 3 個下游回應」的狀態機

代價寫在同一行字裡:Broker 變成所有語義的集中點,也變成整個系統唯一的單點。這句話會在第 8 節與連線擴充篇各被提一次。

生命週期階段Device → Broker(請求)Broker → Device(回應)
連線CONNECTCONNACK(帶 Session Present)
訂閱SUBSCRIBE、UNSUBSCRIBESUBACK(帶實際授予的 QoS)、UNSUBACK
傳送PUBLISH(QoS 0 無回應;QoS 1/2 的 ack 見第 3 節)
保活PINGREQPINGRESP
收尾DISCONNECT(無回應;MQTT 5.0 起 Broker 也可主動送 DISCONNECT 並帶原因碼)

五個階段、四對往返:CONNECT/SUBSCRIBE/UNSUBSCRIBE/PINGREQ 每一個請求都有對應的 ack——PUBLISH 是唯一的例外,它有沒有 ack、有幾個,全由 QoS 等級決定(第 3 節)。MQTT 3.1.1 一共只有 14 種控制封包(5.0 多一個 AUTH),這張表就覆蓋了九成流量。

1 · Topic:一個用 / 分層的 UTF-8 字串,如此而已

MQTT 的 topic 沒有型別、沒有 schema、不需要事先建立。它就是一個 UTF-8 字串,用 / 切成階層:

Level 1factory
/
Level 2taipei
/
Level 3line-3
/
Level 4robot-07
/
Level 5telemetry
/
Level 6temperature

規格區分兩個很容易混用的名詞:Topic Name 是發佈時用的具體字串,不得含 wildcard;Topic Filter 是訂閱時用的樣板,才可以含 wildcard。兩個 wildcard 各管一件事:

Wildcard匹配範圍放置規則例子
+恰好一層,不跨 /可出現在任意層、可出現多次;該層必須整層都是 +factory/+/+/+/status 命中所有廠區所有機台的狀態
#剩下的所有層(含零層)只能是最後一個字元,且前面必須是 / 或整個 filter 就是 #factory/taipei/line-3/# 命中三線底下一切

兩個容易被追問的細節:(1) sport/# 也會匹配 sport 本身(# 可以吃零層)。(2) 以 $ 開頭的 topic(如 Broker 自身指標的 $SYS/...、5.0 共享訂閱的 $share/...)不會被第一層的 + 或 # 匹配到——所以訂 # 不會把 Broker 的內部指標一起吸進來。

互動 · 訂閱樹逐層比對 改 filter 或候選 topic,即時重算命中與比對成本

Broker 收到一則 PUBLISH 時做的唯一一件事,就是拿 topic name 去比對訂閱樹。下面兩個欄位都可以自由輸入:上面是訂閱用的 Topic Filter、下面是你自己想試的 topic name(它會排在固定語料的第一列)。每一格都是逐層比對的實際結果,不是事先寫好的答案。

Topic Filter(訂閱用,可含 + / #)
自訂候選 Topic Name(發佈用,不得含 wildcard)
該層字面相同 + 吃掉一層 # 吞掉剩下所有層 比到這一層就不符(後面不再比)

三個值得停下來看的地方:(1) 不符的那一格右邊全部變虛線——比對是逐層短路的,這也是「比對成本」那個數字算出來的方式。(2) 輸入 sport/# 配 sport 會命中,且標示「# 吞掉 0 層」。(3) 輸入 # 時 $SYS/broker/uptime 仍然落空——$ 開頭的 topic 不被第一層的 wildcard 匹配。

陷阱Topic 是路由鍵,不是佇列——本篇最重要的一句話

RabbitMQ 的 queue 是實體:要宣告、有容量、有堆積深度、可以在管理介面上看到 1,200 則待處理。Kafka 的 partition 是實體:磁碟上真的有那些 segment 檔。MQTT 的 topic 兩者都不是。它只是 Broker 在收到 PUBLISH 那一瞬間拿來比對訂閱樹的一個字串——比中了就複製轉發,沒比中就直接丟棄。
推論有三個,而且都是實務上被反覆踩的:(1) 沒有任何訂閱者在線時發佈的訊息會消失(除非它是 retained,或某個離線客戶端有 persistent session)。(2) topic 數量可以是動態且巨量的(每台裝置一組),因為它不佔儲存空間——但訂閱樹的比對成本會隨層級與 wildcard 複雜度上升。(3) 「這個 topic 積壓了多少訊息」這個問題在 MQTT 裡沒有意義;積壓只會發生在每個客戶端各自的離線佇列裡。

2 · Topic 設計準則:把「訂閱端想怎麼切」寫進層級順序

topic 一旦上線就幾乎改不動(韌體都燒進去了),所以它是 MQTT 系統裡最貴的一個設計決定。核心原則只有一條:層級順序要讓訂閱端能用 wildcard 切出它想要的視角。

# 好的 topic:靜態層級(可過濾)→ 動態識別 → 語義方向
factory/taipei/line-3/robot-07/telemetry/temperature   # 上報:裝置寫、後台讀
factory/taipei/line-3/robot-07/cmd/set-speed           # 下發:後台寫、裝置讀
factory/taipei/line-3/robot-07/status                  # presence:retained + LWT

# 訂閱端用 wildcard 切視角
factory/taipei/+/+/telemetry/temperature   # 全廠溫度(+ 不跨層,剛好卡在機台這層)
factory/taipei/line-3/#                    # 三線的一切(# 只能放最後)
factory/+/+/+/status                       # 全域 presence 看板

# 反例
sensor/data                          # 沒有層級,訂了就是全收,無法切
device/550e8400-.../temperature      # 識別擺太前面,無法「跨裝置看同一種指標」
factory/taipei/#/cmd                 # 非法:# 不是最後一個字元

另外三條慣例值得寫進團隊規範:(1) 不要用前導 /(/factory/... 等於多一個空層級,合法但沒人想要)。(2) topic 大小寫敏感,統一小寫加連字號。(3) 上報與下發要分不同的 topic 子樹(telemetry/ 對 cmd/)——否則裝置會收到自己剛發出去的訊息,這是新手第一個無限迴圈。(MQTT 5.0 的 No Local 訂閱選項可以擋,但分開設計更乾淨。)

一句話答法Broker 讓發佈端與訂閱端在空間、時間、同步三個維度解耦,代價是它成為所有語義的集中點;topic 是用 / 分層的字串,+ 匹配一層、# 匹配剩下所有層且只能放最後——而 topic 是路由鍵不是佇列,沒有訂閱者的訊息直接丟棄。
03

QoS 0 / 1 / 2:投遞保證怎麼被寫進協定

TCP 已經保證不掉不亂了,為什麼還需要三級 QoS?因為 TCP 保證的是「這條連線上」,而連線本身會死。

先回答那個必問的追問。TCP 的可靠性(TCP/UDP 篇)作用範圍是一條活著的連線:序號與重傳讓位元組不掉不亂,但當連線在 write() 成功之後、對端應用處理完之前斷掉,發送端根本無法分辨這則訊息是「已被處理」還是「消失在半路」。TCP 的 ACK 只代表「對端核心收下了」,不代表「對端應用處理了」。QoS 補的就是這一段——它是應用層對應用層的確認,而且明確定義了重傳與去重的責任歸屬。

等級語義封包往返失敗時會發生什麼成本
QoS 0At most once(至多一次)1 個封包,沒有 ack連線斷掉那一瞬間的訊息直接消失,沒有人會知道最低:無狀態、無重傳、無 Packet Id
QoS 1At least once(至少一次)2 個:PUBLISH → PUBACKack 遺失就重送,接收端可能拿到重複發送端要保留訊息直到收到 ack
QoS 2Exactly once(恰好一次)4 個:PUBLISH → PUBREC → PUBREL → PUBCOMP重送會被去重,接收端保證只處理一次雙方都要存狀態;兩個來回,延遲最高

實務分佈非常懸殊:絕大多數遙測資料用 QoS 0 或 1(下一筆溫度一分鐘後就來了,掉一筆不值得付重傳成本);QoS 2 只留給「重複執行會出事」的下發指令——開閘門、扣款、韌體升級觸發。

1 · QoS 1 為什麼會重複

sequenceDiagram
    participant P as Publisher
    participant B as Broker
    participant S as Subscriber
    P->>B: PUBLISH id=42 DUP=0
    B->>S: 轉發(第 1 次)
    B--xP: PUBACK id=42 在回程遺失
    P->>B: PUBLISH id=42 DUP=1(逾時重送)
    B->>S: 轉發(第 2 次)
    Note over B,S: QoS 1 接收端無去重義務

重複發生在 Broker → Subscriber 這一段,而不是遺失的那一段——PUBACK 掉的是回程,發佈端只知道「我沒收到 ack」,無法分辨訊息到底有沒有送達。

關鍵在最後一格:QoS 1 的接收端沒有去重義務。它看到同一個 Packet Identifier 再來一次,規格要求它就是再處理一次。DUP=1 這個旗標只是「這是一次重送」的提示,不能拿來當去重依據——因為在多跳(bridge、叢集)情境下,一則對你而言全新的訊息也可能帶著 DUP=1 抵達。

關鍵QoS 1 是 IoT 的甜蜜點,前提是你把冪等性做在應用層

HTTP 篇已經完整定義過冪等性(約束的是伺服器狀態,不是回應內容)。在 MQTT 上這件事變得很具體:QoS 1 的成本只有一次 ack,卻能擋掉絕大多數的訊息遺失;重複的代價則可以用一個應用層的訊息 id 消掉——payload 裡帶 msg_id 或 seq,接收端存最近 N 筆做去重。
這比 QoS 2 划算太多:QoS 2 每則要兩個來回、雙方都要落地狀態,在弱網上那兩個額外 RTT 是實打實的電量與延遲。「QoS 1 + 應用層去重」是實務上壓倒性的預設答案。

2 · QoS 2 為什麼要四步——兩對交換,各解一個問題

四步流程常被死記成「握手兩次」,但它其實是兩對語義完全不同的交換:

階段封包接收端做了什麼沒有這一對會怎樣
第一對PUBLISH → PUBREC把 Packet Id 記下來(標記為「已收到、待完成」),並回報「我收到了」就退化成 QoS 1:發送端不知道對方是否具備去重能力
第二對PUBREL → PUBCOMP看到 PUBREL 才真正交付給應用,然後忘掉這個 Packet Id 並回 PUBCOMP接收端永遠不知道何時可以忘記那個 id,去重表會無限成長;發送端也不敢重用 id

用一句話收攏:第一對解決「不重複處理」,第二對解決「什麼時候可以安全地忘記」。Packet Identifier 只有 16 bits(1–65535,且 0 不合法),是一種必須被回收的稀缺資源——第二對交換就是那個回收協定。這個「先記下、再確認可以遺忘」的兩階段結構,和分散式交易的 2PC 是同一個形狀。

流程中的每一個封包都可能遺失,所以規格也定義了重送規則:發送端逾時未收到 PUBREC 就用 DUP=1 重送 PUBLISH(接收端因為已經記下 id,不會再交付一次,只補回一個 PUBREC);未收到 PUBCOMP 就重送 PUBREL(注意 PUBREL 重送時 DUP 不設)。整條流程的狀態在 persistent session 裡是要跨連線存活的——這是第 4 節的伏筆。

3 · 最高頻的誤區:QoS 是分段的

陷阱「我用 QoS 2 發佈,所以訂閱者一定恰好收到一次」——錯

一則訊息從發佈者到訂閱者經過兩段獨立的 MQTT 交易:publisher → Broker 是一段,Broker → subscriber 是另一段。兩段各自協商、各自跑自己的 ack 流程,互不相干。第一段的 QoS 由發佈者在 PUBLISH 裡指定,第二段的 QoS 則由下面這個公式決定。

訂閱者實際收到的 QoS = min ( 發佈時的 QoS , 訂閱時被授予的 QoS )

「被授予」也是一個常被略過的字。訂閱者在 SUBSCRIBE 裡要求一個 QoS,Broker 在 SUBACK 裡回覆實際授予的等級——它可以降級(甚至回失敗)。所以會出現這種情境:裝置辛辛苦苦用 QoS 2 發了訊息,某個以 QoS 0 訂閱的看板收到的仍然是 fire-and-forget,掉了也不會有人知道。

更根本的一點:MQTT 從來沒有承諾 end-to-end exactly-once。它承諾的是「每一段 hop 上的 exactly-once」。中間只要有 Broker 重啟、訊息在 Broker 記憶體裡還沒落地、或流量經過 bridge 到另一個 Broker,端到端的重複就仍然可能發生。要真正的一次語義,答案永遠是在應用層做冪等,而不是把 QoS 調到 2。這一點在第 6 節對照 Kafka 的 exactly-once 時會再出現。

補充Inflight window:QoS 大於 0 時真正的吞吐瓶頸

QoS 1/2 的訊息在收到最終 ack 之前叫 inflight,發送端必須保留它。客戶端函式庫與 Broker 都會限制「同時最多幾則 inflight」(常見預設 10–20;MQTT 5.0 把它正式化成 Receive Maximum 屬性,雙方在 CONNECT/CONNACK 互相宣告)。
這個數字直接決定了弱網下的吞吐上限:如果 RTT 是 400 ms、inflight 上限是 10,那麼每秒最多 25 則,跟你的 CPU 和頻寬完全無關。裝置端遙測突然「卡住不上報」,第一個該看的就是 inflight 是不是被塞滿了——這是 TCP/UDP 篇談 backpressure 時那條因果鏈在 MQTT 上的具體長相。

一句話答法QoS 0 至多一次、1 至少一次(PUBACK,會重複)、2 恰好一次(PUBREC/PUBREL/PUBCOMP 四步:第一對讓接收端記下 Packet Id 去重,第二對確認何時可以安全遺忘並回收 id);而 QoS 是分段的——publisher→Broker 與 Broker→subscriber 各自協商,訂閱端實得 QoS 取兩者最小值,所以 MQTT 從不保證 end-to-end exactly-once。
04

Broker 替你記住的三件事

Retained message 管「我剛上線,現在是什麼狀況」;persistent session 管「我不在的時候」;Last Will 管「我回不來了」。

這三件事有一個共同結構:把「應該有人在線上」這個假設拆掉。它們也正是把 MQTT 從「pub/sub 語法」變成「訊息系統」的那部分。

1 · Retained message:每個 topic 的最後一則快照

發佈時把 RETAIN 旗標設為 1,Broker 除了照常轉發給當下的訂閱者之外,還會把這則訊息存起來,覆蓋該 topic 上的前一則。之後任何新訂閱者一訂上,立刻就會收到它,不必等下一次上報。

性質說明
每個 topic 幾則恰好一則(最新的覆蓋舊的)。它是快照不是歷史,沒有任何回溯能力
存活多久直到被覆蓋或被清除;不受 session 影響,客戶端斷線也還在
怎麼清除對同一個 topic 發佈一則 payload 長度為 0 且 RETAIN=1 的訊息。這則空訊息不會被存下來,只會把既有的抹掉
收到時怎麼分辨因新訂閱而投遞的 retained 訊息會帶 RETAIN=1;正常轉發給既有訂閱者的則是 0——客戶端可以據此分辨「這是歷史快照」還是「這是剛發生的事」

典型用途:裝置的目前狀態(在線與否、目前設定值、韌體版本)。反例:不要拿它存感測器讀數的時序資料——它只留一則,而且沒有時間軸。MQTT 5.0 額外提供 Retain Handling 訂閱選項(0 = 照送、1 = 只在這是全新訂閱時送、2 = 完全不送),解掉「重連後重複收到一堆 retained」這個很煩的問題。

2 · Persistent session:Broker 替離線的你排隊

客戶端在 CONNECT 裡宣告要不要保留 session。這是 3.1.1 與 5.0 差異最明顯的地方之一:

MQTT 3.1.1MQTT 5.0
旗標Clean Session = 0 表示要保留Clean Start = 0(是否沿用既有 session)+ Session Expiry Interval(斷線後留多久,秒)
存活期沒有明確上限,由 Broker 實作決定——殭屍 session 會慢慢吃光記憶體明確可控:0 = 斷線即銷毀、0xFFFFFFFF = 永不過期、其餘為秒數
兩件事是否分離綁在一起:要保留 session 就永遠保留分離:可以「這次開新的(Clean Start=1),但斷線後保留一小時」

3.1.1 那個沒有 TTL 的設計是實務上真實的營運痛點:測試時用過的每一個 client id 都會在 Broker 上留下一份訂閱與佇列。「升 5.0 最有感的一項」通常就是 Session Expiry。

Session 裡到底存了什麼,是面試最容易問細的地方:

session 裡的項目為什麼非存不可重連後的影響
客戶端的所有訂閱訂閱是狀態不是請求,重建一次要一趟往返Session Present=1 時原封不動;=0 就得全部重新 SUBSCRIBE
已送出但未收到 ack 的 QoS 1/2 訊息發送端不留著就沒東西可重送沿用同一批 Packet Identifier,帶 DUP=1 重送
已收到但未走完 QoS 2 流程的 Packet Id去重表若隨連線消失,exactly once 當場破功補完 PUBREL/PUBCOMP,不會重複交付
離線期間累積的 QoS 1/2 訊息這就是「時間解耦」的實作本體一重連就整批投遞
QoS 0 訊息不排隊——規格允許 Broker 選擇保留,實務上都不留。離線期間發的 QoS 0 直接消失,重連後補不回來

最後一列是反例,也是最常被踩的一格:開了 persistent session 卻收不到離線訊息,九成是因為那些訊息是用 QoS 0 發的。

最後一格是關鍵:QoS 0 的訊息在你離線時直接被丟掉(規格允許 Broker 選擇保留,但實務上都不留)。所以「我明明開了 persistent session,重連後卻沒收到離線訊息」——九成是因為那些訊息是用 QoS 0 發的。這也讓 QoS 從一個「傳輸可靠性」的旋鈕,變成一個「離線期間值不值得替你存」的旋鈕。

另外兩件必須綁在一起講:(1) session 用 Client Identifier 當鍵——所以 client id 必須穩定且唯一;用隨機 UUID 每次重連都是新 session(persistent 形同虛設),而兩台裝置撞同一個 id 則會互相把對方踢下線,形成一個非常難查的無限重連迴圈。(2) Broker 在 CONNACK 裡回一個 Session Present 旗標,告訴你它究竟有沒有找到舊 session。客戶端必須看這個旗標:如果是 0,代表訂閱全部沒了,你得重新 SUBSCRIBE——很多「重連後就再也收不到訊息」的 bug 就出在沒檢查它。

3 · Last Will and Testament:裝置斷電時,Broker 替它說最後一句話

WebSocket 篇談保活時花了一整節講 half-open:一端已經消失,另一端卻毫無察覺。MQTT 沒辦法讓 half-open 消失(那是 TCP 的事),但它做了一件更有用的事——讓「我死掉時要通知誰、說什麼」在還活著的時候就先講好。

客戶端在 CONNECT 封包裡就可以夾帶一份遺囑:Will Topic、Will Payload、Will QoS、Will Retain。Broker 收下後存著,只在特定條件下才發出去:

連線結束的方式Broker 會不會發 LWT為什麼
客戶端送了 DISCONNECT 才斷不會這是「我要走了」,不是意外——Broker 收到 DISCONNECT 時會丟棄那份遺囑
Keep Alive 逾時會最常見的觸發:裝置斷電、進隧道、電池耗盡(第 5 節)
TCP 連線被 RST/FIN 意外中斷會網路故障、行程被 kill、中間設備拆連線
協定錯誤/認證後違規(如訂了無權的 topic)會Broker 主動關閉且視為異常

MQTT 5.0 補了兩個很實用的東西:Will Delay Interval(延遲 N 秒才發遺囑;若裝置在這段時間內帶著同一個 client id 回來,遺囑就取消)——這正好消掉「行動網路抖一下就發出離線告警」的誤報;以及帶 Reason Code 的 DISCONNECT,讓客戶端可以說「我是正常關機」還是「我要斷線但請幫我發遺囑」。

關鍵Presence 模式:LWT + retained 是一組,單獨用哪一個都不完整

這是 MQTT 最漂亮的一個組合技,也幾乎是每個 IoT 系統都會實作的第一個功能:
1. 裝置連上後,立刻對 .../robot-07/status 發佈 online,帶 RETAIN=1。
2. 同一台裝置在 CONNECT 時就把 LWT 設成:同一個 topic、payload offline、Will Retain = 1。
3. 裝置正常運作時,retained store 上是 online;裝置斷電後 Keep Alive 一逾時,Broker 代發 offline 並覆蓋掉 retained。
效果是:任何時候上線的看板,只要訂閱 factory/+/+/+/status,一瞬間就能拿到全廠所有機台的當前在線狀態,完全不需要一個「裝置狀態」資料庫,也不需要輪詢。少了 retain,新看板要等到下一次狀態變化才知道;少了 LWT,斷電的裝置會永遠停在 online。

一句話答法Retained 是每個 topic 一則的最新快照,讓新訂閱者立刻拿到現況(用零長度 payload 清除);persistent session 以 client id 為鍵,替離線客戶端保留訂閱與 QoS 1/2 訊息(QoS 0 不排隊),5.0 用 Clean Start + Session Expiry 取代 3.1.1 沒有 TTL 的 Clean Session;Last Will 只在非正常斷線時由 Broker 代發,配上 retain 就是標準的 presence 方案。
05

連線層:Keep Alive、MQTTS 與裝置身分

前面三節都建立在「Broker 知道你還在不在」之上。這一節處理那條連線本身,以及連上來的到底是誰。

1 · Keep Alive:把保活的判定寫死在規格裡

WebSocket 篇談保活的章節建了一張三層保活分工表(TCP keepalive/協定層 Ping-Pong/應用層 heartbeat),並且結論是:週期與逾時判定全部由你自己決定。MQTT 走的是同一個機制、不同的哲學——它把這件事變成連線建立時的協商參數:

WebSocket Ping/Pong(見 WebSocket 篇)MQTT Keep Alive
週期怎麼決定應用自訂,兩端各自為政客戶端在 CONNECT 裡宣告秒數(16-bit,最大 65535 秒;0 = 停用);5.0 的 Broker 還可以在 CONNACK 裡用 Server Keep Alive 覆寫它
誰主動伺服器發 Ping,瀏覽器自動回 Pong(JS 無法主動發)客戶端主動發 PINGREQ,Broker 回 PINGRESP
什麼時候要發固定週期只在沒有其他封包可送時才發——任何一個封包(PUBLISH、SUBSCRIBE…)都算數,等於免費保活
逾時判定自訂:連續 N 次無 Pong規格定死(見下)
Broker 判定連線死亡的門檻 = Keep Alive × 1.5

也就是說:客戶端宣告 60 秒,Broker 就會在連續 90 秒沒有收到任何封包時斷開這條連線——並且觸發第 4 節的 Last Will。這個 1.5 倍的寬容量是留給時脈飄移與網路抖動的。反過來,客戶端送出 PINGREQ 後若遲遲收不到 PINGRESP,也應該主動斷線重連(規格建議「合理時間內」,函式庫通常實作成一個 Keep Alive 週期)。

Keep Alive 的選值是純粹的 device constraints 題,而且和 TCP/UDP 篇的 NAT 結論直接掛鉤(那裡已經定調:心跳既是活性偵測,也是餵養 NAT 映射——這裡不重講):

選值換到什麼付出什麼
短(30–60 s)離線偵測快(presence 準)、NAT 與 LB 的 idle timeout 一定來不及先到行動網路上每次 PINGREQ 都要喚醒無線模組,電池殺手;百萬連線時 Broker 的 CPU 帳單也很可觀
長(10–20 分鐘)極省電,適合電池供電的感測器裝置死掉後最久要 1.5 倍才被發現;而且必須確認路徑上每一段的 idle timeout 都比它長,否則連線會被中間設備靜默拆掉而你毫不知情

實務起手式:市電供電的閘道器 60 秒;電池裝置盡量拉長,但先量測路徑上最短的 idle timeout(電信商 NAT 常見 5–30 分鐘),取它的一半。這條原則和 WebSocket 篇面試折衷一節的「心跳週期必須小於路徑上最短的 idle timeout」是同一條,只是這裡的成本單位從「流量」換成了「電池壽命」。

2 · MQTTS 與裝置身分:mTLS 在這裡才真正變成主流做法

TLS 篇已經把 mTLS 的機制講完(雙向憑證、CertificateVerify、私有 CA、短效憑證輪替),這裡只補「為什麼 IoT 是它最自然的戰場」。答案是裝置沒有人類:沒有人可以輸入密碼、沒有瀏覽器可以跳登入頁、也沒有辦法在外洩後叫使用者改密碼。一台裝置一張憑證,於是變成最乾淨的身分模型。

身分方案怎麼做適合風險 / 限制
X.509 + mTLS私有 CA 簽發 per-device 憑證,Broker 驗證後把 CN/SAN 當 client 身分產線可燒錄、生命週期長的裝置要建 PKI 與撤銷機制(CRL/OCSP 在裝置端常不可行,實務多靠短效憑證輪替+Broker 端黑名單);MCU 要有足夠 RAM 做非對稱運算
Username / PasswordCONNECT 封包內建這兩個欄位,Broker 查後端驗證改造成本最低、閘道器場景必須跑在 TLS 之上(否則明文);一定要 per-device 不同——共用一組帳密等於整批裝置一起淪陷,卻是最常見的做法
Token(JWT 等)把短效 token 放進 password 欄位,Broker 驗簽手機 App、能連後端換 token 的裝置token 會過期,但 MQTT 連線可能活好幾天——需要「到期前重連」或 5.0 的 AUTH 封包做連線內重新認證

兩個一定要成對記住的結論。(1) 沿用 TLS 篇:mTLS 是 authentication 不是 authorization——證明了「你是 robot-07」,不代表 robot-07 有權訂閱 factory/#。Broker 端必須另外做 topic 級 ACL(把 client id 綁進 topic、限制它只能讀寫 factory/+/+/robot-07/#),否則任何一台被拆解的裝置都能監聽全廠。(2) 沿用 TLS 篇談版本選型的章節:「只開 TLS 1.3 還是保留 1.2」在 IoT 要先盤點——你不能像 Web 一樣宣布淘汰舊版,因為現場那批 2018 年出廠、跑著舊版 mbedTLS 的裝置,韌體可能根本升不上去。

# 訂閱端:wildcard + QoS 1,保留 session(-c = disable clean session)
$ mosquitto_sub -h broker.example.com -p 8883 --cafile ca.crt \
    -i dash-01 -c -q 1 -t 'factory/taipei/+/+/status' -v
factory/taipei/line-3/robot-07/status online     # 一訂上就收到 retained,不必等下次上報

# 發佈端:mTLS 身分 + 上線公告(retained) + 事先寫好的遺囑
$ mosquitto_pub -h broker.example.com -p 8883 --cafile ca.crt \
    --cert robot-07.crt --key robot-07.key \
    -i robot-07 --keepalive 60 \
    -t 'factory/taipei/line-3/robot-07/status' -m 'online' -q 1 -r \
    --will-topic 'factory/taipei/line-3/robot-07/status' \
    --will-payload 'offline' --will-qos 1 --will-retain

這兩條指令就是第 4 節那個 presence 模式的完整實作:-r 讓 online 成為 retained,--will-* 讓 Broker 在 90 秒(60 × 1.5)沒收到封包時代發 offline 覆蓋它。把 mosquitto_pub 那個行程直接 kill -9,就能在訂閱端看到遺囑跳出來——這是驗證 LWT 設定最快的方法(用 Ctrl-C 反而不行,因為函式庫會送出乾淨的 DISCONNECT)。

3 · MQTT 5.0 相對 3.1.1:值得知道的五項

3.1.1 仍然是裝置端相容性最好的版本,但雲端 Broker 幾乎都支援 5.0,新專案沒有理由不用。除了前面已經出現的 Session Expiry、Will Delay、Retain Handling、Receive Maximum、Server Keep Alive 之外,還有五項值得點名:

特性解決什麼
Reason Code + Reason String3.1.1 的 ack 幾乎沒有錯誤資訊,連線被拒只知道「失敗」。5.0 在所有 ack 上都帶原因碼——這是可觀測性上最大的一項改善
Shared Subscription訂閱 $share/{group}/{filter},同組內的訂閱者輪流收訊息而不是每人一份——這才讓 MQTT 有了類似 consumer group 的競爭消費模型(第 6 節對照)
Message Expiry Interval給訊息設 TTL。離線佇列裡放了三天的「開燈」指令,裝置回來時不應該還被執行
Topic Alias把長 topic 字串在連線內映射成一個 2-byte 整數,之後只送整數——直接回應第 1 節那筆「50 bytes topic 配 4 bytes payload」的帳單
Request / ResponseResponse Topic + Correlation Data 兩個屬性,把請求/回應關聯做進協定。這正是 WebSocket 篇談 framing 的章節那張表裡「WebSocket 沒有、要自己加 id 欄位」的那一格

另外 5.0 還有 User Properties(任意 key-value,等於 MQTT 版的 HTTP 標頭)與 Subscription Identifier(讓客戶端知道這則訊息是因為哪一條訂閱被送來的——多重 wildcard 訂閱時很有用)。

一句話答法Keep Alive 由客戶端在 CONNECT 宣告、只在沒別的封包可送時才發 PINGREQ,Broker 在 1.5 倍週期無封包時斷線並觸發 LWT;MQTTS 是 MQTT over TLS(8883,ALPN mqtt),IoT 場景下 per-device X.509 + mTLS 是最乾淨的身分方案,但 mTLS 只做認證,topic 級 ACL 必須另外做。
06

定位對照:WebSocket、Kafka、RabbitMQ 各自在解什麼

這一節只回答一個問題:什麼時候不該用 MQTT。而答案幾乎都是「因為你要的是另外三個東西之一」。

1 · 與 WebSocket:一個是通道,一個是通道上的語義

WebSocketMQTT
拓撲點對點:客戶端 ↔ 伺服器星狀:所有人 ↔ Broker,發佈者與訂閱者互不認識
訊息定址沒有——你自己在 payload 裡設計topic + wildcard 訂閱,由協定路由
投遞保證沒有(TCP 之外什麼都沒有)QoS 0/1/2
離線 / 死亡沒有——重連後的狀態恢復全部自己寫persistent session + retained + Last Will
典型客戶端瀏覽器、行動 AppMCU、閘道器、車機、感測器
原生瀏覽器支援有(new WebSocket)沒有——瀏覽器不能開裸 TCP,必須走 MQTT over WebSocket

最後一列是實務上最重要的一條綁定。MQTT over WebSocket 的作法是把 MQTT 的位元組流塞進 WebSocket 的 binary frame,子協定名固定為 mqtt(Sec-WebSocket-Protocol: mqtt),通常直接跑在 wss:// 443 上——順帶解掉「企業防火牆只開 443」這個現實問題。注意分層:WebSocket 的 frame 邊界與 MQTT 的封包邊界不對齊,一個 WS frame 可以裝多個 MQTT 封包、一個 MQTT 封包也可以跨多個 frame,所以接收端仍然要照 MQTT 的 Remaining Length 自己切。這就是「一個是通道、一個是通道上的語義」最具體的樣子。

2 · 與 Kafka、RabbitMQ:三個都叫「訊息系統」,但幾乎沒有一格是一樣的

這是本篇最需要記住的一張表。面試裡「MQTT 跟 Kafka 差在哪」如果只答「一個給 IoT 一個給大數據」,會被追問到底;真正的差異在投遞語義、消費模型、順序、保留與吞吐這五個維度上,而且每一格的差異都會反過來限制它能做什麼。

維度MQTTRabbitMQ(AMQP 0-9-1)Kafka
本質裝置接入協定訊息中介/任務佇列分散式 commit log
Delivery semanticsQoS 0/1/2,但分段(per-hop),無 end-to-end exactly-oncepublisher confirm + consumer ack;至少一次,重複要自己防預設至少一次;idempotent producer + transaction 可做Kafka 內部的 exactly-once
Consumer modelPush:Broker 主動推給所有匹配的訂閱者,客戶端無 offset 概念Push(basic.consume + prefetch)為主;同一 queue 上多 consumer 競爭Pull:consumer group 分配 partition,offset 由消費端自己管
扇出(fan-out)天然扇出:一則訊息複製給 N 個訂閱者。要競爭消費得用 5.0 的 shared subscription用 fanout/topic exchange 綁到多個 queue,複製發生在 exchange多個 consumer group 各自獨立讀同一份 log,互不影響
Ordering同一發佈者、同一 topic、同一 QoS 內有序;跨 topic 或重傳後無保證單 queue 單 consumer 有序;一旦多 consumer 或 requeue 就破partition 內嚴格有序(同 key → 同 partition),這是它最強的保證
Retention幾乎不保留:只有每 topic 一則 retained + 離線 session 佇列投遞並 ack 後即刪除——queue 是暫存不是儲存按時間/大小保留(天~永久),消費不刪除
重播 / 回溯不可能(沒有 log)不可能(除非另建 replay 機制)可以:seek 到任意 offset 或時間戳重讀
連線規模十萬~百萬條長連線;2-byte 標頭、Keep Alive 可拉到十幾分鐘千~萬(每連線多個 channel),協定較重數千個 client;每個 client 要抓 metadata、跑 fetch 迴圈、參與 rebalance
Throughput每則小、總量中等;瓶頸在連線數與扇出倍數中等;routing 能力豐富但單 queue 吞吐有限極高:順序寫、批次、零拷貝,單 partition 就能吃下數十 MB/s
典型場景感測器上報、遠端下發指令、車聯網、行動推播任務佇列、RPC、工作流、需要複雜路由與死信事件溯源、串流處理、入湖、指標管線

把整張表壓成一句:MQTT 解決「怎麼把幾十萬台不穩定的裝置接進來」,RabbitMQ 解決「怎麼把工作可靠地分派給 worker」,Kafka 解決「怎麼把事件存成一份可以重讀的歷史」。三者不是同一個問題的三種答案,因此不可直接替換。

陷阱兩個方向的誤用,都很常見

「用 Kafka 直接接裝置」:Kafka client 是重量級的——要拉 metadata、維護 fetch 迴圈、參與 consumer group rebalance,MCU 上根本跑不動;而且它沒有 Last Will、沒有 retained、沒有為省電設計的 keep-alive,也扛不住幾十萬條時斷時續的連線(每次重連都可能觸發 rebalance)。
「用 MQTT 當事件儲存」:MQTT 沒有 log。新上線的分析服務無法回頭讀昨天的資料,訂閱之後才發生的事才看得到;retained 只有一則,也不是歷史。想「重跑一次上週的資料」在純 MQTT 架構下是不可能的——這通常是團隊決定引入 Kafka 的那一天。

3 · 所以實務上的答案是接力,不是二選一

裝置 × 500k
MCU 等級、弱網、時斷時續
MQTT / QoS 1──▸
MQTT Broker
扛連線數、跑 QoS 與 LWT、替離線裝置排隊
bridge──▸
Kafka topic
一份可重播的 log,留存與分發
consumer group──▸
下游消費者
流處理 · 入湖 · 告警
◂────── 下發方向:應用或 Kafka 寫回 cmd/ 子樹,由 Broker 找到裝置、按 QoS 投遞、離線就排隊 ──────

分工只有一句話:Broker 負責「接入」(連線數、弱網重連、QoS、LWT),Kafka 負責「留存與分發」(一份可重播的 log)——而下發指令時方向反過來,因為只有 Broker 知道 robot-07 現在連在哪。

這是 IoT 後端幾乎標準化的長相,分工非常清楚:MQTT Broker 負責「接入」——扛連線數、處理弱網重連、跑 QoS 與 LWT、替離線裝置排隊、省裝置的電;Kafka 負責「留存與分發」——把事件寫成一份可重播的 log,讓分析、告警、入湖、機器學習各自用獨立的 consumer group 讀同一份資料,互不干擾,且新服務上線可以回頭補跑歷史。中間那個 bridge 通常是 Broker 內建的 connector(EMQX/HiveMQ/VerneMQ 都有)或雲端的規則引擎(AWS IoT Core Rule → Kinesis/MSK)。

反方向同樣重要:後台要下發指令時,是從 Kafka 或應用服務寫回 MQTT 的 cmd/ 子樹,由 Broker 負責找到那台裝置、按 QoS 投遞、離線就排隊。Kafka 完全無法自己完成這件事——它不知道 robot-07 現在連在哪。而三者也常同時存在:MQTT 接裝置、Kafka 存事件、RabbitMQ 派工單給人類操作員或第三方系統。

一句話答法MQTT 是裝置接入協定(push、天然扇出、幾乎零保留、百萬連線),RabbitMQ 是任務佇列(競爭消費、ack 後刪除、複雜路由),Kafka 是可重播的 commit log(pull、consumer group 自管 offset、partition 內嚴格有序、長期保留);三者的投遞語義與消費模型根本不同,不可互換——標準架構是 MQTT 接裝置、bridge 到 Kafka 做留存與流處理,指令再從 MQTT 反向下發。
07

控制封包的位元組帳單

第 1 節說「2 bytes 標頭只是行銷話術的一半」。這一節把另一半攤開:一則 PUBLISH 到底是哪些位元組組成的。

MQTT 的每一個控制封包都是同一個三段式結構:固定表頭(1 byte 的「4 bit 封包型別 + 4 bit 旗標」,加上 1–4 bytes 的剩餘長度)、可變表頭(依封包型別而異:PUBLISH 是「2 bytes 長度前綴 + topic 字串」再加上只有 QoS ≥ 1 才出現的 2 bytes Packet Identifier;CONNECT 是協定名、版本、連線旗標與 Keep Alive;SUBSCRIBE 是 Packet Identifier),以及payload(協定完全不看內容)。所謂「最小 2 bytes」指的只有固定表頭那一段,而且是在剩餘長度只佔 1 byte 時;真正要付錢的是中間那一段——尤其是每則訊息都要重送一次的 topic 字串。

剩餘長度這個欄位值得單獨講,因為它是 MQTT 省位元組的手法裡最漂亮的一個:它用變長編碼,每個 byte 只拿 7 bit 存數值、最高位當接續位元(1 表示「後面還有一個 byte」)。所以 127 bytes 以內只花 1 byte、16,383 以內 2 bytes、2,097,151 以內 3 bytes、上限 268,435,455 花 4 bytes——小封包不必為大封包的可能性預付欄位寬度。要注意它算的是「這個封包在固定表頭之後還有幾個 bytes」,不包含它自己與第 1 個 byte。下面這台拆解器就把這些規則真的跑一遍:你調 topic、payload、QoS、旗標與 Keep Alive,它把位元組一格一格組出來。

互動 · 控制封包逐 byte 拆解 改任一個欄位,位元組會重新組一次
封包型別
Topic Name(發佈用)
Payload 大小:4 bytes
發佈端 QoS
訂閱端被授予的 QoS(SUBACK)
Packet Identifier(1–65535)
Keep Alive:60 秒(斷線門檻 90 秒)
發佈間隔:60 秒
固定表頭 剩餘長度(變長) 長度前綴 字串 Packet Id 其他可變表頭 Payload

這台拆解器和子網篇那條「固定 32 格位元條」的差別,就在格數本身是輸出:把 payload 從 71 調到 72,剩餘長度會從 1 格變 2 格、位元展開多長出一整列,總長度一次跳 2 bytes;把 topic 換成中文,長度前綴算的是 UTF-8 的 bytes 而不是字元數。另外兩個算出來的數字值得看:(1) 「離下次進位還剩幾 bytes」告訴你這個封包還能長多大而不多付一個欄位。(2) 每日 PINGREQ 次數是 ceil(發佈間隔 ÷ Keep Alive) − 1 乘上每日發佈次數——只要發佈間隔小於 Keep Alive,這個數字就是 0,因為任何封包都算保活,心跳等於免費。

說明這台拆解器簡化了什麼

它組的是 MQTT 3.1.1 的布局(CONNECT 可變表頭固定 10 bytes:0x00 0x04 'M' 'Q' 'T' 'T' + 版本 + 連線旗標 + 2 bytes Keep Alive),不編碼 5.0 的 properties 區——5.0 會在可變表頭與 payload 之間多一段屬性,本質上是同一套「長度前綴 + 內容」的疊加。CONNECT 的遺囑固定用 <client-id>/status 與 7 bytes 的 offline;PUBLISH 的 payload 因為協定完全不看內容,所以只調大小、不編內容(超過 12 bytes 的部分會摺疊成一格)。每日流量假設「除了這筆交換之外沒有其他封包」,那是電池裝置的典型情況,也是 PINGREQ 會真的被送出去的前提。

一句話答法MQTT 封包=固定表頭(4 bit 型別+4 bit 旗標,加上 1–4 bytes 的變長剩餘長度)+可變表頭(PUBLISH 是 2 bytes 長度前綴+topic,Packet Id 只在 QoS ≥ 1 出現)+payload;「最小 2 bytes」只算固定表頭,真正的帳單在每則都要重送的 topic 字串上——50 bytes 的 topic 配 4 bytes 的溫度值,開銷佔九成以上,這正是 5.0 要用 Topic Alias 把它壓成 2 bytes 的理由。
08

面試折衷與實務選型

MQTT 的選型幾乎全部發生在兩個地方:QoS 要開幾級,以及狀態該放在 Broker 還是應用。

決策點選項Trade-off
QoS 等級0 vs 1 vs 2遙測用 0/1(掉一筆,下一筆一分鐘後就來);會改變狀態的下發指令用 1 + 應用層冪等;QoS 2 只給「重複執行會出事且無法做冪等」的極少數操作,因為它要多一個來回、雙方都要落地狀態
SessionClean vs PersistentPersistent 讓裝置斷線期間的指令不遺失,代價是 Broker 要替每台裝置維護佇列(記憶體與磁碟成本 × 裝置數)。一定要設 Session Expiry,否則測試用過的 client id 會永久佔用資源
Keep Alive短 vs 長短:presence 準、NAT 不會先斷;長:省電。上限是路徑上最短的 idle timeout,取其一半是安全起手式
Topic 結構粗(少層級) vs 細(多層級)細:訂閱端能用 wildcard 精準切視角、ACL 好寫;粗:Broker 的訂閱樹比對便宜。但 topic 上線後幾乎改不動(韌體燒死),寧可一開始多切兩層
Payload 格式JSON vs 二進位(CBOR/protobuf)JSON 好除錯、生態成熟;MCU 上 JSON 解析器可能比整支韌體還大,且每則多幾十 bytes——按位元組計費的 NB-IoT 上這是真金白銀
裝置身分mTLS vs 帳密 vs tokenmTLS 最乾淨但要建 PKI 與輪替機制;帳密最省事但絕對不能全機隊共用一組;token 要處理「連線活得比 token 久」
協定版本3.1.1 vs 5.0新專案直接上 5.0(Session Expiry、Reason Code、Shared Subscription、Topic Alias 都很有感);但現場舊裝置的韌體可能只支援 3.1.1,Broker 要能同時服務兩種版本
要不要接 Kafka純 MQTT vs MQTT + Kafka只要出現「回頭重跑歷史資料」「多個下游各自消費同一份事件」「事件要留三個月」任一項,就是該 bridge 的訊號。純 MQTT 架構下這三件事都做不到

共同結構:MQTT 把狀態集中到 Broker 換取裝置端的極簡——每多開一級 QoS、多留一份 session、多存一則 retained,省下的是裝置的電與程式碼,付出的是 Broker 的記憶體、磁碟與複雜度。這個天平的支點永遠在「你有幾台裝置」。

面試高頻追問怎麼答

Q:TCP 已經可靠了,為什麼還要 QoS? 答:TCP 只保證「一條活著的連線上」不掉不亂,而且 ACK 只代表對端核心收下。連線斷掉時發送端無法分辨訊息是被處理了還是消失了。QoS 是應用層對應用層的確認,並定義了重傳與去重的責任歸屬。
Q:QoS 2 是不是就等於 end-to-end exactly-once? 答:不是。QoS 是分段的——publisher→Broker 與 Broker→subscriber 各自協商,訂閱端實得等於兩者取 min。QoS 2 保證的是每一段 hop 的 exactly-once;真正的一次語義要在應用層做冪等。
Q:QoS 2 為什麼要四個封包? 答:兩對交換。第一對(PUBLISH/PUBREC)讓接收端記下 Packet Id 以便去重;第二對(PUBREL/PUBCOMP)確認何時可以安全忘記那個 id 並回收它——否則去重表會無限成長,發送端也不敢重用 16-bit 的 Packet Identifier。
Q:我開了 persistent session,為什麼重連後沒收到離線訊息? 答:三個可能——訊息是用 QoS 0 發的(不排隊)、client id 每次重連都變(等於每次都是新 session)、或是 CONNACK 的 Session Present 是 0 而客戶端沒有重新 SUBSCRIBE。
Q:怎麼做裝置在線狀態? 答:LWT + retained 一組。上線發 online(retain=1),CONNECT 時把遺囑設成同一個 topic 的 offline(will retain=1)。斷電後 Keep Alive 的 1.5 倍逾時,Broker 代發並覆蓋。新看板一訂上就拿到全機隊現況。
Q:MQTT 跟 WebSocket 怎麼選? 答:不是二選一。WebSocket 是點對點通道,MQTT 是 Broker 中介的 pub/sub 語義;瀏覽器裡跑 MQTT 就是拿 WebSocket 當 transport(子協定 mqtt,走 wss 443)。要的是「自訂雙向訊息」選 WebSocket,要的是「投遞保證 + 離線 + presence」選 MQTT。
Q:MQTT 跟 Kafka 差在哪? 答:五個維度——MQTT 是 push、無 offset、幾乎不保留、天然扇出、扛百萬連線;Kafka 是 pull、consumer group 自管 offset、按時間保留可重播、partition 內嚴格有序、吞吐極高但 client 很重。所以標準架構是 MQTT 接裝置、bridge 到 Kafka 做留存與流處理。

把本篇收攏成一句:MQTT 用一個 Broker 換掉了裝置端的複雜度。訊息該送給誰(topic 路由)、會不會重複(QoS)、離線時去哪(session 佇列)、剛上線怎麼知道現況(retained)、死掉了誰來說(Last Will)——這五件事在 WebSocket 上全部是你的程式碼,在 MQTT 上全部是 Broker 的職責。裝置端因此可以小到跑在一顆幾十 KB RAM 的 MCU 上。

而這正是問題所在。retained store、每個 client id 的 session 與離線佇列、每條連線的 inflight window、整棵訂閱樹——全部活在那一台 Broker 的記憶體裡。這句話你在 WebSocket 篇談連線狀態的章節已經看過一次,當時的主詞是 WebSocket gateway。兩條線在這裡合流了:無論你選的是 WebSocket 還是 MQTT,只要連線數超過一台機器扛得住的量,真正的題目就變成同一個——長連線要怎麼橫向擴充?訊息怎麼跨節點找到「持有那條連線的那一台」?Broker 組成叢集之後,session 與 retained 要不要複製、誰來判定 Keep Alive 逾時並代發 Last Will、發版時怎麼把幾十萬條連線優雅地趕去別台而不引爆重連風暴?還有,哪些工作應該下沉到 edge、哪些必須留在中心?系列收尾的連線擴充篇處理它。

一句話答法MQTT 適合「大量受限裝置、不穩定網路、需要投遞保證與離線語義」的場景,選型的核心是 QoS 等級與 session 策略——它們決定了 Broker 要替你記住多少東西;而 MQTT 不是 Kafka 或 RabbitMQ 的替代品,它解的是接入問題,留存與重播要靠 bridge 出去。