Android 爛裝置想跑人臉辨識 第 12 篇 – 說到底還是一台IOT

「在百元級門禁機上做網路通訊,最天真的想法就是以為網路永遠很順;真正的工業級架構,是把每一天都當作『隨時會斷網、隨時會被拔插頭』的世界末日來設計。」

前面幾篇我們把高通驍龍 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)

  1. gRPC 官方團隊架構文件:gRPC-Web Official Documentation & Specification (GitHub: grpc/grpc-web, State of gRPC in the Browser)
  2. WHATWG Fetch Living Standard:Fetch API Specification – Section on HTTP/2 Framing and Trailer Header Limitations in Browsers
  3. IETF RFC 6455The WebSocket Protocol – Protocol Upgrade & Multiplexing Specifications

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *