設計一對一聊天 App 的行動客戶端架構與它跟後端的通訊。難點不在後端中繼,而在 app 端:即時連線、本地訊息 DB、離線送出佇列、訊息狀態與可靠排序。
📋 Requirements
- 功能:依最新時間戳排序檢視對話、即時收發文字訊息、線上狀態/正在輸入/送達狀態/已讀回條、離線瀏覽聊天歷史(分頁載入)、推播提醒新訊息。
- 規模:全球約 3,000 萬 DAU;聚焦一對一聊天,群組/媒體/加密不在範圍。
- 非功能:擴展性(各種網路條件下可靠)、資料完整性(離線時維持一致、暫存待送訊息)、耐久性(客戶端資料是訊息的真實來源)。
🧠 Core Ideas
TIP
核心是「混合通訊(HTTP 送出 + WebSocket 收即時)+ 本地關聯式 DB 當真實來源 + 離線送出佇列與重試 + 訊息狀態機驅動 UI」。
客戶端彼此不直接通訊,透過後端中繼。兩類互動要分開處理(見同步 vs 非同步):客戶端主動操作(送訊息、同步)用 HTTP request-response;後端主動推送(新訊息、已讀、狀態)用即時更新機制。App 啟動時建立 WebSocket 連線並持續保持。
⚖️ Reference Design
通訊協定選型 — 為何是 HTTP + WebSocket
- 伺服器主動連線可選 Polling/Long polling/SSE/WebSocket。Polling 在無新資料時浪費;Long polling 逾時與多伺服器路由複雜;SSE 單向、仍需另一機制處理上行。
- 採混合架構:HTTP + REST 處理客戶端主動操作;WebSocket 接收即時推送。
- 選 WebSocket 而非 SSE:延遲更低、header overhead 小,對頻繁小訊息(輸入指示、已讀回條)關鍵;建於 TCP 之上提供可靠、依序傳輸。
- 嚴格限縮 WebSocket 只用於即時功能,以控管持久連線的擴展複雜度。
客戶端分層架構
- 採分層架構,遵循 UDF、reactive programming、關注點分離。
- 資料層核心是 Messages Repository(管理所有讀寫),搭配 Users Repository。
- 雙資料來源:Messages Local DataSource 走 HTTP 端點;Real-time Updates Remote DataSource 維持 WebSocket 接收即時更新。
- UI 層兩個主畫面 Chat List 與 Conversation;Message List 依時序顯示所有訊息,含失敗/待送出訊息並提供重送。
本地儲存 — SQLite
- key-value 儲存不適合成長中的訊息歷史與複雜關係,選關聯式資料庫(訊息本質結構化,需複雜查詢、ACID、可擴充 schema)。
- 行動端用 SQLite(iOS:CoreData 可以 SQLite 為底、或 SQLite.swift/FMDB;Android:Jetpack Room/SQLDelight)。
Messages(已同步)與MessageRequests(未同步、尚無 message id)分表,Users、Messages、MessageRequests 全放同一chats.db;為常查欄位加索引、批次交易、所有 DB 操作在背景執行緒避免阻塞 UI。
訊息排序 — 混合策略
- 邊界案例:網路延遲導致亂序、離線訊息重連後整合、裝置時鐘不一致。
- 候選排序欄位:
messageId(伺服器產生、各端一致,但離線時尚無)vscreatedAt(低延遲、支援離線,但受時鐘差影響)。 - 混合策略:已被後端確認的訊息依
messageId排序;等待/送出中的訊息依createdAt排序並永遠置於列表末端。 messageId由後端產生、必須可依時間排序(新訊息 ID 恆大於舊),後端是 ID 的真實來源。
送出訊息 — 狀態機與離線重試
- 訊息生命週期:Repository 立即建立並存本地、UI 馬上顯示(樂觀更新)→ 封裝
NewMessageRequest經 HTTP 送後端 → 後端產生messageId並回應 → 客戶端收回應後以messageId更新本地。 requestId當 idempotency key,讓後端對重複請求去重;fromMessageRequestId把回應對應回原請求。- 用
MessageRequest(狀態 DRAFT/PENDING/SENT/FAILED)追蹤送出中訊息;送達確認後移除 MessageRequest、建立新 Message(刻意無 SUCCEEDED 狀態,UI 顯示時合併兩表)。 - 失敗/逾時:標記 FAILED、
failCount++,以指數退避重試;達最大重試次數後標永久失敗並讓使用者手動重送或刪除。
推播通知
- 接收者無活躍 WebSocket 連線時,後端向 Push Provider(Android FCM/iOS APNs)發送;後端維護使用者↔裝置 token 映射。
- App 在前景時由 Push Notifications Client 處理並觸發同步,背景/終止時顯示於通知列。
- 挑戰:系統限縮/省電延遲、需使用者同意、雙平台 API 不一致。因此推播只當輕量警示,即時功能仍靠 WebSocket。
🔑 Takeaways
- 這是客戶端題:真實來源在裝置端,設計重心是本地 DB、離線佇列與訊息狀態機,而非後端服務。
- 通訊用混合架構:HTTP 送出 + WebSocket 收即時,並嚴格限縮 WebSocket 範圍以控管擴展成本。
- 可靠送出靠 idempotency key + 狀態機 + 指數退避重試;可靠排序靠「已確認用
messageId、待送用createdAt置底」的混合策略。 - 本地用 SQLite、Messages 與 MessageRequests 分表、DB 操作全在背景執行緒。
No notes yet — jot your takeaways or Q&A here.