返回列表

部落格

深入協議:Mac 到 iPhone 的即時終端串流傳輸

我們如何建構了一個基於 WebSocket 的協議,將 tmux 終端輸出即時串流傳輸到 iPhone,支援差分更新、ANSI 渲染和嚴格的延遲目標。

發布於 標籤: engineering / protocol / performance / terminal
部落格

繼續查看該欄目下的產品與工程更新。

瀏覽此分類

終端串流傳輸是 Tactic Remote 的核心。所有功能——審批、通知、工作階段監控——都依賴於 iPhone 準確、即時地呈現 Mac 終端中正在發生的內容。要做到這一點,需要在協議層面解決幾個不那麼顯而易見的問題。

本文將介紹我們的串流傳輸協議如何運作,從 tmux 擷取到 iPhone 渲染的完整流程,以及我們在此過程中做出的工程取捨。

資料管線

終端資料流經五個階段:

  1. tmux 擷取 — Mac companion 應用程式從 tmux 面板讀取終端內容。
  2. 差分計算 — 將目前擷取與上次傳輸的狀態進行比較。
  3. 序列化 — 將變更區域打包成緊湊的二進位框架。
  4. WebSocket 傳輸 — 框架透過持久 WebSocket 連線發送到 iPhone。
  5. ANSI 渲染 — iPhone 用戶端解析跳脫序列並渲染帶樣式的文字。

每個階段都有自己的延遲預算。我們的目標是在區域網路上端到端傳輸在 100 毫秒以內,在每個階段內留有充足的餘量。

tmux 面板擷取

Tactic Remote 透過 tmux 管理 Claude Code 工作階段。這是一個早期的架構決策,對串流傳輸帶來了很大優勢:tmux 維護一個虛擬終端緩衝區,我們可以透過程式化方式讀取,而無需攔截實際的 PTY 串流。

我們使用帶 -p 和 -e 旗標的 tmux capture-pane 命令來擷取保留 ANSI 跳脫序列的面板內容。在活躍串流傳輸期間,擷取以 50 毫秒的輪詢間隔執行——足夠快以感覺像即時傳輸,又足夠低頻以避免明顯的 CPU 開銷。

一個細節要注意:capture-pane 回傳可見面板內容加上回捲緩衝區。預設情況下我們只擷取可見視區,並在使用者在 iPhone 上向上捲動時按需串流傳輸回捲內容。這使得常見情況下每個框架的酬載保持較小。

在現代 Mac 上,擷取過程每個框架增加約 3-8 毫秒,測量範圍涵蓋 M1 到 M4 硬體。

差分更新

每個框架都發送完整的終端緩衝區是浪費的。一個典型的 80x24 終端面板包含大約 1,920 個字元。加上 ANSI 序列,一個完整框架可以達到 8-12 KB。以每秒 20 個框架計算,頻寬需求為 160-240 KB/s——在本地 Wi-Fi 上可以接受,但透過 Cloudflare Tunnel 的行動網路連線就有問題了。

取而代之,我們計算連續框架之間的行級差分。大多數終端更新只影響幾行——出現一行新日誌、進度條前進、游標移動。我們的差分演算法透過雜湊比較識別變更的行,只傳輸修改的內容。

在實務中,差分更新將平均框架大小減少了 85-92%。Claude Code 活躍輸出期間,典型的差分框架為 400-800 位元組。閒置期間(等待使用者輸入或 API 回應時),根本不發送框架——我們只在內容變化時傳輸。

指標完整框架差分框架
平均框架大小8-12 KB400-800 位元組
20 fps 時的頻寬160-240 KB/s8-16 KB/s
閒置時的頻寬160-240 KB/s0 KB/s

傳輸格式

差分框架被序列化為緊湊的二進位格式,而非 JSON。每個框架包含:

  • 4 位元組標頭:協議版本(1 位元組)、框架類型(1 位元組)和酬載長度(2 位元組)。
  • 用於排序和遺失偵測的序列號。
  • 行更新記錄陣列,每條記錄包含行索引、新內容(UTF-8 編碼,嵌入 ANSI 序列)和用於完整性驗證的 CRC32 校驗和。

我們在 Protocol Buffers、MessagePack 和自訂二進位格式之間選擇了自訂二進位格式,因為基準測試表明,對於小型、高頻訊息,序列化開銷主要來自程式庫的初始化成本。我們手寫的序列化器每個框架增加不到 1 毫秒。

WebSocket 傳輸

串流連線使用單一持久 WebSocket。我們評估了 Server-Sent Events (SSE) 和原始 TCP,但 WebSocket 在雙向通訊(捲動請求和流量控制所需)、廣泛的網路相容性和內建框架結構方面提供了最佳組合。

連線管理處理三種情境:

區域網路 — iPhone 直接連線到 Mac companion 的本地伺服器。往返時間通常為 2-8 毫秒。端到端串流傳輸延遲(擷取到渲染)平均為 40-60 毫秒。

Cloudflare Tunnel — 流量經過 Cloudflare 的邊緣網路路由。往返時間增加到 30-80 毫秒,取決於地理位置。端到端串流傳輸延遲平均為 120-200 毫秒。對於監控來說仍然足夠即時,但使用者在快速捲動輸出時會注意到差異。

網路切換 — 當 iPhone 在 Wi-Fi 和行動網路之間切換(或在不同網路之間切換)時,WebSocket 連線會斷開。我們的重新連線邏輯在 1-2 秒內重新建立連線,並請求全框架重新同步以恢復狀態。重新連線期間,iPhone 上的視覺指示器顯示連線狀態,讓使用者了解輸出暫停的原因。

ANSI 跳脫序列處理

Claude Code 的終端輸出包含豐富的 ANSI 跳脫序列——用於語法高亮的顏色、用於強調的粗體、用於進度指示器的游標定位。在 iPhone 上準確保留這些序列對可讀性至關重要。

我們的 iOS 渲染器處理最常見的 SGR(Select Graphic Rendition)序列:16 種標準顏色、256 色調色盤、24 位元真彩色、粗體、斜體、底線和反色。我們刻意不在渲染檢視中支援游標移動序列(CSI H、CSI A/B/C/D)——這些在 tmux 擷取期間被消耗並反映在行定位中,而不是作為原始跳脫碼傳輸。

行動端面臨一個特殊挑戰:深色模式。為深色背景設計的終端配色方案在 iOS 淺色模式下渲染效果不佳(反之亦然)。我們偵測 iPhone 當前的外觀模式,並套用顏色對映來保持對比度。這不是佈景主題切換——而是亮度調整,在保持原始顏色意圖的同時確保可讀性。

效能預算

我們在三個百分位數上追蹤串流傳輸效能:

指標P50P95P99
擷取延遲4ms7ms12ms
差分計算1ms3ms5ms
序列化<1ms1ms2ms
網路(區域網路)3ms8ms15ms
網路(隧道)45ms90ms150ms
iPhone 渲染6ms12ms18ms
總計(區域網路)15ms31ms52ms
總計(隧道)57ms113ms187ms

這些數據來自我們 Beta 使用者群的生產遙測。P50 區域網路延遲為 15 毫秒,這意味著在大多數互動中,iPhone 顯示幕的更新速度快到人眼無法感知延遲。

我們學到的經驗

建構即時終端輸出串流傳輸協議讓我們學到了幾條經驗:

對於這個情境,輪詢優於事件驅動。 我們最初嘗試掛接 tmux 的內部更新事件,但發現 50 毫秒輪詢更簡單、更可靠,且效能開銷可以忽略不計。

行級差分已經足夠好。 我們原型實作了字元級差分,但額外的複雜性只帶來了 5-8% 的頻寬進一步減少,不值得承擔維護負擔。

高頻傳輸下二進位框架格式很重要。 對於每秒發送 20 次的訊息,序列化格式中每一個位元組的開銷都會被放大。JSON 的冗餘在測量中代價明顯。

重新連線的使用者體驗與連線效能同等重要。 如果使用者理解正在發生什麼,他們可以容忍 2 秒的重新連線。但如果沒有視覺回饋,即使 500 毫秒的重新連線也會被感知為故障。

有關實作細節,請參閱我們的架構文件。如果你正在建構類似的系統,歡迎透過支援頁面與我們討論。

立即體驗 Tactic Remote

用手機遠端操作程式開發 Agent

連線到 Mac、Windows 或 Linux 電腦上的 Claude Code、Codex 等 Agent,在 iPhone 或 iPad 上查看進度並傳送下一個請求。