「在百元級門禁機上做網路通訊,最天真的想法就是以為網路永遠很順;真正的工業級架構,是把每一天都當作『隨時會斷網、隨時會被拔插頭』的世界末日來設計。」
前面幾篇我們把高通驍龍 QM215(4 顆 A53、2GB RAM)的系統瘦身與相機零拷貝榨到了極致。但當門禁機掛上工廠大門時,現場的網路比晶片更慘:設備躲在企業內網深處(192.168.1.X 無公網 IP)、網路線隨時被拔、DNS 動輒卡死 30 在邊緣 IoT 系統中,WebSocket 與 RESTful API 到底該如何精準分工,做到「0.1 秒即時開門、零記憶體負擔、斷網 3 天也能穩健重連」!

一、 為什麼必須用 WebSocket?
在邊緣設備中,HTTP 短輪詢(Polling)在工廠現場會立刻遇到兩大死穴:
1. 延遲與伺服器當機的死循環
- 5 秒輪詢:訪客按鈴平均乾等 2.5 秒,體驗極差;
- 200ms 輪詢:全廠 100 台設備每秒產生 500 次請求,高達 4 個 RTT 的 TCP/TLS 握手瞬間打爆地端小伺服器。
2. 內網 NAT 穿透困境
門禁機無公網 IP,雲端伺服器完全無法主動發起 HTTP POST 發送開門指令。設備必須在開機時主動向伺服器建立 WebSocket 長連線,在防火牆上鑿出永久雙向通道(Pinhole):
- 下行即時控制:0.1 秒遠端開門、即時註銷離職員工、訪客動態密碼驗證;
- 上行即時回報:刷臉打卡抓拍照即時上傳、防拆機警報;
- 通道保活:15 秒輕量心跳維持防火牆通道暢通。
二、 為什麼大檔案不能進 WebSocket?RESTful API 的不可替代性
如果把所有功能全部塞進 WebSocket,現場會立刻引發嚴重的開門卡死事故。RESTful API 有其專屬戰場:
1. 第三方系統(SAP / HR 考勤)標準無縫對接
由伺服器提供標準 RESTful API(如 POST /api/v1/users 新增員工、GET /api/v1/events 批次匯出),客戶用最熟悉的 HTTP 即可串接,無需在他們的系統維護脆弱的長連線。
2. 50MB 韌體升級與大檔案下載
50MB APK 更新檔、3D 鏡頭韌體與高解析度照片批次下載,必須走標準 HTTP 進行分段流式傳輸與斷點續傳。
3. 防禦 TCP 隊頭阻塞(高速公路塞車效應)
WebSocket 底層為單一 TCP 連線。若將 50MB 韌體塞進 WebSocket,會瞬間佔滿整條連線;此時若突發「緊急開門」指令,開門封包被迫排在 50MB 檔案後面等幾十秒,導致門打不開!
將「大檔案傳輸(HTTP REST)」與「即時信令(WebSocket)」物理隔離,是保證 0.1 秒秒開門的關鍵底線。
三、 資料傳輸大瘦身:為何選「Protobuf over WebSocket」?
1. Base64 讓記憶體直接爆掉
早期將現場照片轉為 Base64 字串塞進 JSON 傳輸:
- 體積膨脹 33%,浪費頻寬;
- 一張 150KB JPEG 轉成 Base64 會在 Java 記憶體產生 200KB 的 String 垃圾。尖峰期頻繁觸發垃圾回收(GC),相機預覽直接從 30fps 掉到個位數導致人臉丟幀。
2. 為什麼不直接上全套 gRPC?(Web 瀏覽器直連硬傷)
- 瀏覽器天然限制:警衛室 Chrome 大螢幕需看即時通行。根據 W3C 標準,瀏覽器 JS 無法取得 HTTP/2 底層 Framing 與 gRPC 依賴的
Trailers(尾隨標頭),Chrome 永遠無法原生直連 gRPC; - Envoy 維運災難:若硬上 gRPC,必須在地端機房額外架設 Envoy Proxy 伺服器做轉譯,多一個組件就多一個當機點;
- 老舊防火牆地雷:企業內網老舊代理對 HTTP/2 二進位多工串流支援極差,常誤判為攻擊直接掐斷。
3. 最佳解答:Protobuf over WebSocket 全棧大一統
維持單一 8082 WebSocket 埠號,Payload 全面換成 Google Protocol Buffers:
syntax = "proto3";
package com.kuke.gate;
message PassEventPacket {
string device_id = 1;
int64 timestamp = 2;
string user_id = 3;
bool is_authorized = 4;
// 原生二進位 byte 陣列直入,0 Base64、0 額外字串記憶體垃圾!
bytes rgb_image = 5;
bytes ir_image = 6;
bytes depth_image = 7;
}
- 門禁機端:JPEG 位元組直接塞進 Protobuf,Java 記憶體垃圾歸零,封包比 JSON 小 60%;
- 警衛室 Web 端:Chrome 瀏覽器原生
new WebSocket()直連,掛載protobuf.js一行代碼解碼,零 Envoy 代理依賴!
四、 邊緣裝置的穩定性實戰:主流設計怎麼做?
1. HandlerThread 單執行緒事件排隊
網路連線(DNS 解析、TCP 握手、心跳)交由專屬 HandlerThread 在背景單行道依序排隊:
- UI 零延遲:UI 呼叫僅丟 Message 排隊,0.01ms 立刻返回;
- 內部狀態封閉:Socket 實體完全封裝在背景執行緒中,免除複雜跨執行緒加鎖。
public class WebSocketWorkerManager {
private static final int MSG_CONNECT = 1;
private static final int MSG_SEND_EVENT = 2;
private static final int MSG_HEARTBEAT = 3;
private final HandlerThread workerThread;
private final Handler workerHandler;
private final AtomicBoolean isConnected = new AtomicBoolean(false);
private WebSocketClient webSocketClient;
public WebSocketWorkerManager() {
workerThread = new HandlerThread("WebSocket-Worker");
workerThread.start();
workerHandler = new Handler(workerThread.getLooper()) {
@Override
public void handleMessage(@NonNull Message msg) {
switch (msg.what) {
case MSG_CONNECT:
handleConnect((String) msg.obj);
break;
case MSG_SEND_EVENT:
handleSendEvent((byte[]) msg.obj);
break;
case MSG_HEARTBEAT:
handleHeartbeat();
break;
}
}
};
}
/** UI 呼叫:僅丟 Message 排隊,0.01ms 返回 */
public void connect(String serverUrl) {
workerHandler.obtainMessage(MSG_CONNECT, serverUrl).sendToTarget();
}
/** 快速查詢:0 奈秒無鎖讀取快照 */
public boolean isConnected() { return isConnected.get(); }
/** 上傳通行抓拍:直接丟 byte 陣列排隊 */
public void sendPassEvent(byte[] protobufBytes) {
workerHandler.obtainMessage(MSG_SEND_EVENT, protobufBytes).sendToTarget();
}
private void handleConnect(String url) {
webSocketClient = new WebSocketClient(URI.create(url)) {
@Override
public void onOpen(ServerHandshake handshakedata) {
isConnected.set(true);
workerHandler.sendEmptyMessageDelayed(MSG_HEARTBEAT, 15000);
}
@Override
public void onClose(int code, String reason, boolean remote) {
isConnected.set(false);
}
};
webSocketClient.connect();
}
private void handleSendEvent(byte[] data) {
if (isConnected.get() && webSocketClient != null) webSocketClient.send(data);
}
private void handleHeartbeat() {
if (isConnected.get() && webSocketClient != null) {
webSocketClient.send(ProtobufHelper.buildHeartbeatPacket());
workerHandler.sendEmptyMessageDelayed(MSG_HEARTBEAT, 15000);
}
}
}
2. 斷網後的分批補傳機制(LIMIT 20)
若斷網 3 天累積數千筆打卡,網路恢復時設備每次僅上傳 20 筆,等伺服器回傳確認(ACK)寫入成功後才繼續撈下一批,平穩消化歷史資料,防記憶體爆掉。
3. 重連隨機延遲(Jitter)防連線風暴
伺服器重啟時,採用「指數退避 + 隨機加減 500ms」,徹底錯開數百台設備的重連請求,避免瞬間打垮伺服器。
- WebSocket 負責「快與即時」:穿透內網,專注於 0.1 秒即時開門、刷臉回報與 Protobuf 低延遲傳輸;
- RESTful API 負責「穩與通用」:處理第三方 HR 對接、50MB 韌體升級,隔離大檔案防禦隊頭阻塞;
- 邊緣端以 HandlerThread 與分批機制當防線:背景單執行緒排隊守護 UI,分批補傳與 Jitter 抵禦斷網衝擊。
參考文獻與出處(References)
- gRPC 官方團隊架構文件:gRPC-Web Official Documentation & Specification (GitHub:
grpc/grpc-web, State of gRPC in the Browser) - WHATWG Fetch Living Standard:Fetch API Specification – Section on HTTP/2 Framing and Trailer Header Limitations in Browsers
- IETF RFC 6455:The WebSocket Protocol – Protocol Upgrade & Multiplexing Specifications


發佈留言