- date
- entry
- 008
- topic
- integration
- rev
- —
MQTT 連不上的三種壞法,症狀都一樣
手機端看到的永遠是同一件事:連不上,然後無限重連。實際壞掉的可能是 client library 裡的一個 if 判斷、broker 對空 body 的預設解讀、或 Docker 預設網路不做 service name 解析——三個都不會在對方的 log 裡留下線索。
先把鏈畫出來。一個 app 要連上自架的 MQTT broker,中間經過的節點是:
Flutter client → CDN (TLS 443) → nginx (/mqtt location)
→ EMQX websocket listener → HTTP authn callback → backend /mqtt/auth
六節。任何一節壞掉,手機上看到的都是 NoConnectionException 或
Missing CONNACK,然後進重連迴圈。症狀完全沒有鑑別力。
下面三個坑我都踩過,成因分佈在鏈的三個不同位置。
坑一:client library 把你的設定蓋回去
Flutter 的 mqtt_client v10.x 有一個順序敏感的行為:同時設
secure = true 和 useWebSocket = true 時,函式庫內部的
if (secure) 分支會把 useWebSocket 蓋回 false。你以為在走 WSS,實際上走的是裸 TCP + TLS,而 nginx 那個 /mqtt location 只接 WebSocket upgrade。
正確的寫法是不要碰 secure:
client.useWebSocket = true;
client.useAlternateWebSocketImplementation = true; // 不要設 secure = true
client.websocketProtocols = ['mqtt'];
同一個函式庫還有第二個坑:WS2 的 handshake 是用 uri.path 拼出 GET 請求行的。如果你只給 host 沒給 path,它會送出
GET ? HTTP/1.1——一個無效的請求行,CDN 直接把連線關掉。所以第一個參數要寫完整的 URI:
MqttServerClient.withPort('wss://$host/mqtt', clientId, port);
要確認自己走在哪條路上,把 library 的 log 打開:
client.logging(on: true);
// 關鍵行:
// "WS URL is wss://..." → path 對不對
// "alternate websocket implementation selected" → 有走 WS
// 出現 MqttSecureConnection → 走成裸 TCP+TLS,錯了
坑二:broker 把「200 但沒 body」當成拒絕
EMQX 5 的 HTTP 認證回呼,預期後端回傳的是一個帶結果欄位的 JSON:
{"result": "allow"} // 或 "deny" / "ignore"
如果後端只回一個 200 OK 空 body——這是很多人寫 handler 的直覺——EMQX
會把它解讀成拒絕,客戶端收到 CONNACK code 5(notAuthorized)。
這個坑討厭的地方在於後端 log 看起來完全正常。access log 那行是
POST /api/v1/mqtt/auth 200,沒有錯誤、沒有警告。你從後端那一側看,這次認證是成功的。
// Go / Gin
c.JSON(http.StatusOK, gin.H{"result": "allow"})
這跟狀態碼對但 body 說謊是同一類問題的鏡像:那次是 body 被中間層換掉,這次是 body 從一開始就沒寫——而兩邊的 log 都會告訴你「200,沒事」。
坑三:Docker 預設網路不解析 service name
這是我那次真正的 root cause。EMQX 的認證設定裡寫的是
http://backend:8007/api/v1/mqtt/auth,看起來很合理——docker-compose 就是這樣寫的。
但那幾個 container 是用 docker run 個別啟動的,全部落在 Docker 的
default bridge 網路。而 default bridge 不支援用 service name 做 DNS 解析——那是 user-defined network 才有的功能。所以 backend 這個名字永遠解析不到,認證請求根本沒送出去。
應急修法是走 host gateway:
docker run --add-host=host.docker.internal:host-gateway ...
# 認證 URL 改成 http://host.docker.internal:8007/api/v1/mqtt/auth
長期修法是改用 compose 起,它會建 user-defined network,service name DNS 自動可用:
emqx:
extra_hosts:
- "host.docker.internal:host-gateway"
這一節的症狀是 listener 統計裡 current_conn: 0——沒有任何連線建立成功。
但 nginx 的 access log 有請求進來、backend 的 log 一片安靜。三份 log 拼不出一個故事,因為斷點在兩個 container 之間,不在任何一份 log 的視野裡。
從哪裡開始查
症狀沒有鑑別力的時候,正確的策略是先確定請求走到了第幾節,而不是猜哪一節做錯。由內往外問,每一節都有一個直接的問法:
容器內網路通不通(回 000 代表根本沒連上,這就是坑三):
docker exec my-emqx curl -s -o /dev/null -w "%{http_code}\n" \
-X POST http://host.docker.internal:8007/api/v1/mqtt/auth \
-H "Content-Type: application/json" -d '{"username":"x","password":"y"}'
# 200 或 401 都算通;000 = 連不到
broker 有沒有收到連線:
docker exec my-emqx /opt/emqx/bin/emqx ctl listeners
# current_conn: 0 → 沒人連進來,問題在 broker 之前
# shutdown_count 增加 → 有連進來但被踢,問題在認證
整條外部路徑通不通(CDN → nginx → EMQX 的 WebSocket upgrade):
curl -ki --http1.1 \
-H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Protocol: mqtt" \
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
https://api.example.com/mqtt
# 預期 HTTP/1.1 101 Switching Protocols + sec-websocket-protocol: mqtt
最後這條特別有用,因為它把「手機端有問題」和「伺服器端有問題」切開。101 回得來,就代表坑一在手機那側;回不來,就往內找。
三個坑的共通點
它們分佈在鏈的三個不同位置,成因也毫不相干,但有一個一樣的性質:出問題的那一節,自己回報一切正常。
- library 認為它照你的設定連了(它只是先把設定改掉)
- backend 認為它成功回應了認證(它只是沒寫 body)
- EMQX 認為它送出了認證請求(它只是連不到那個主機名)
沒有任何一份 log 是錯的。它們各自誠實,只是視野都停在自己那一節的邊界上,而壞掉的東西正好在邊界之間。
如果只記得一件事
跨層系統偵錯的第一個問題不是「哪一節做錯了」,是「請求走到了第幾節」。前者要猜,後者可以量。每一節找一個能直接問到答案的指令,一節一節往內問——比讀六份 log 快得多,因為 log 只會告訴你各節做了什麼,不會告訴你請求有沒有抵達。
修訂紀錄
- 首次發布