Skip to content

Instantly share code, notes, and snippets.

@dolphinsue319
Created January 12, 2025 07:04
Show Gist options
  • Save dolphinsue319/68d92ffa0a074137e8dcd0a40413d3b4 to your computer and use it in GitHub Desktop.
Save dolphinsue319/68d92ffa0a074137e8dcd0a40413d3b4 to your computer and use it in GitHub Desktop.
TienYu_處理斷線與漏掉訊息的聊天室

Q: 用戶 A 在聊天室中斷線並於 10 秒後重新連線,如何確保用戶 A 收到斷線期間(這 10 秒內)的所有訊息?

  • 前提:每則訊息都帶有一個可排序的唯一標識(例如 messageID 或「遞增的序號」),或使用「時間戳 (timestamp) + 伺服器的邏輯排序」來判斷訊息先後。
  • local 端儲存:在 local 端中,為每個房間記錄一個 lastReceivedMessageID(或 lastReceivedTimestamp),並在每次成功收到新的訊息後都更新。
  • 當用戶重新連線時,前端將 lastReceivedMessageID 傳給後端,後端依此回傳從這個 ID(或 Timestamp)之後的新訊息列表。
  • 當偵測到斷線(WebSocket 連線斷開事件),客戶端開始進行重連邏輯。
  • 在連線成功的初始握手或授權階段,客戶端告知伺服器「我在哪些聊天室,有哪些 lastReceivedMessageID」。。
  • 伺服器回傳遺漏訊息:
    • 從用戶提供的 lastReceivedMessageID 之後開始,批次或分頁 (pagination) 地將訊息傳回。
    • 為避免伺服器壓力,後端可設定一次最多回傳多少筆訊息,若訊息量過多則分多次回傳,讓客戶端透過多次請求或多次 WebSocket Event 接收。
  • 客戶端合併訊息:
    • 如果收到的訊息 ID 已存在,則略過不要重複顯示。
    • 若後端因某些原因導致訊息先後順序不穩定,客戶端應該根據 messageID 或 timestamp 重新排一次順序,以保證顯示正確的順序。
    • 對於新的訊息,加入本地的聊天室資料結構中。
    • 同時更新 lastReceivedMessageID,確保後面不會重複拉取這些訊息。

Q: 請描述伺服器與客戶端的通信協議(如訊息序列化、確認機制)及其實現方式。

  1. 訊息序列化格式 (Message Serialization)
    • 常見的做法是使用 JSON 格式進行序列化,當然也可以使用 Protobuf 之類的二進位格式來提升傳輸效率,但若考量開發成本和跨平台需求,JSON 已足夠且實作便利。
  2. 確認機制 (Acknowledgement):
    • 當客戶端收到伺服器送來的 chatMessage 時,馬上回傳一個已讀訊息給伺服器,伺服器可據此知道用戶已成功收到了哪幾則訊息。
    • 也可採用前面提到的「最後已接收訊息」機制:客戶端會在心跳 (heartbeat) 或某些定時事件時,將「目前已讀取的最大 messageID」告知伺服器。

Q: 如果聊天室的訊息量非常大,例如每秒鐘產生 100 條訊息,客戶端需要如何高效地進行訊息同步?

  1. 減少每次傳輸量:分批 / 分頁 / 限制範圍
    1. 伺服器分批推送 (batch/pagination)
      • 當後端偵測到用戶尚未接收的訊息數量過多時,可以分批(例如每批 20 或 50 筆)依序推送給客戶端,而非一次丟給客戶端所有訊息。
      • 客戶端在接收到一批訊息後,先進行 UI 顯示與本地資料更新,再向伺服器請求或接收下一批,減少瞬間的處理壓力。
    2. 拉取歷史訊息時的分頁 (pagination on history)
      • 如果使用者想要查看更舊的訊息,採用「下拉加載更多」或「分頁」的設計,而不是一次拉取全部歷史紀錄。
      • 只有最新的訊息(即目前聊天中)使用 WebSocket 進行即時推送;舊訊息的載入,可在使用者明確操作時再跟 Server 要或從 local database 裡讀取出來,避免常態性的龐大資料傳輸。
    3. 限制聊天室可見範圍
      • 若訊息量極大,也可以考慮在 UI 上只保留最新 N 筆或一定時間範圍內的訊息,超過範圍的訊息暫時不載入。
      • 使用者需要瀏覽更舊訊息時,透過「載入更多」向伺服器請求過去的訊息。
  2. 資料結構與處理效能
    1. 以 Message ID 作為索引
      • 例如使用 messageID (遞增整數) 或 timestamp 作為索引,利於快速查詢新舊訊息範圍,也方便去重 (de-dup) 與補齊。
    2. 批次插入與渲染
      • 在 iOS 客戶端的實作上,若使用 UITableView 或 UICollectionView 來顯示訊息,一次插入大量 Cells 可能會出現卡頓。
      • 可以先將批次資料儲存在本地資料結構(如 Array),再一次性地更新 UI(批次更新 TableView/CollectionView),減少 UI 畫面刷新次數。
    3. 本地資料庫 (如 Core Data 或 Realm)
      • 若需要大量訊息的長期儲存,將訊息寫入本地資料庫可能比直接全部存在記憶體更有效率,也較不易造成記憶體暴增。
      • 但是要注意寫入資料庫的頻率與批量操作方式,過於頻繁的寫入也可能造成效能問題。
      • 建議做法是先在記憶體中暫存批次訊息,達到一定量或在適當時機(例如使用者閒置、App 進入背景等)再批次寫入。
  3. iOS App 性能優化重點
    1. UI Thread 與 Background Thread 分工
    • 解析訊息、寫入本地資料庫等操作,應該在背景線程中執行,減少主線程 (Main Thread) 的負擔。
    • 主線程只負責最終的 UI 更新。
    1. 減少主線程渲染次數
    • 批次更新 UI,而不是每收到一則訊息就直接 reloadData()。
    • 可先在背景將多筆訊息整理好,再一次插入 UITableView/CollectionView。
    1. 差異化更新 (diffable data source / performBatchUpdates)
    • 使用 iOS 提供的 Diffable Data Source 或 performBatchUpdates 方式,只更新新增或改變的訊息,避免整個清單的重載。
    1. 記憶體管理
    • 當訊息量極為龐大時,需謹慎管理本地記憶體;對於暫時看不到的舊訊息可只留索引或簡要資訊,不要全部緩存在 memory 內,避免造成記憶體過大而閃退 (OOM)。

Q: 假設伺服器支持批量發送歷史訊息,客戶端需要實現一個機制來根據用戶的滾動操作逐步加載更舊的歷史訊息(類似無限滾動的效果)。該機制應如何設計?

  1. 維護「目前已載入的最舊訊息」的標誌
  2. 每次只拉取有限數量的訊息(例如 20 筆或 50 筆)回來,避免一次請求過多資料。
  3. 請求結束後更新 oldestLoadedMessageID 為最新拿到的那批訊息中的最舊 ID。
  4. 當伺服器回傳的訊息已經沒有更多(或已經達到整體歷史訊息的最前端)時,就不再觸發向上載入的動作。
  5. UI 與使用者操作
    • UITableView / UICollectionView 實作
      1. 偵測使用者即將滾動到頂部
        • 監聽 TableView / CollectionView 的滾動事件 (scrollViewDidScroll:)。
        • 當使用者往上滾動且距離頂部小於某個閾值(例如距頂部剩下 50 px),就觸發「載入更多」邏輯。
      2. 顯示 Loading Indicator
        • 可以在列表頂部加一個「loading cell」或用 UIRefreshControl(常見於下拉更新),顯示正在載入的狀態。
        • 等到資料載入完成後,再更新 UI 讓使用者知道載入結束。
      3. 批次插入新資料
        • 在取得新的歷史訊息後,先把它們插入到本地資料結構中(例如陣列的前端),再呼叫 insertRows(at: with:) 或 performBatchUpdates 等方法,將這些新資料插入畫面頂部。
        • 需要注意更新 contentOffset,避免插入新資料時導致列表突然跳動。

Q: 如果需要在 UITableView 中高效地顯示多個 GIF 圖片,如何優化圖片加載與資源管理?

  1. 使用專門處理 GIF 的第三方框架
    1. FLAnimatedImage
      • 一個比較早期且相當穩定的 GIF 處理框架,專門用於在 iOS 上高效顯示 GIF。
      • FLAnimatedImage 幫助將 GIF 解析後,使用較少的記憶體,同時維持流暢播放。
      • 提供快取管理,可避免重複解析相同 GIF。
    2. SDWebImage (SDWebImageGIFCoder)
      • SDWebImage 可以額外安裝 SDWebImageGIFCoder 或其餘擴充,支援 GIF 動態播放。
      • 具有快取機制,並能與網路下載整合,在 TableView/CollectionView 中常被使用。
  2. 只在需要時載入與播放
    • 透過 tableView(:willDisplay:forRowAt:) 與 tableView(:didEndDisplaying:forRowAt:) 等方法,偵測 Cell 的顯示/消失。
    • 當 Cell 滑出畫面,應停止 GIF 的播放或釋放資源;當滑回畫面,再重新啟動播放。
    • 這能避免在背景繼續渲染多個離屏 GIF,浪費 CPU/GPU 資源。
  3. GIF 資料解碼與快取
    1. 預解碼 (Pre-decoding)
      • GIF 每張幀圖都需要進行解壓縮,過程中會消耗 CPU。
      • 部分框架 (例如 FLAnimatedImage、YYImage) 會在背景執行解碼,並將解碼後的畫面快取到記憶體,以避免在主線程中即時解碼導致卡頓。
    2. 多層快取 (Memory Cache + Disk Cache)
      • 使用如 SDWebImage 等框架,能同時支援記憶體與磁碟快取:
      • 記憶體快取:快速顯示最近使用的 GIF 幀圖。
      • 磁碟快取:長期儲存下載後的 GIF 檔,下次進入時不用重複下載。
      • 在 app 收到 memory warning 時自動釋放,減少 OOM(Out Of Memory)風險。
    3. 限制 GIF 大小與幀率 (Frame Rate)
      • 若 GIF 檔案過大或幀數很多,對 CPU/GPU 造成很大負擔。
      • 可以考慮降低 GIF 的解析度或幀率,或在後端預先壓縮處理(若場景允許)。
    4. 在 iOS 端,若 TableView 需要動態計算 Cell 高度,盡量使用 Auto Layout 或預先計算好的高度,以避免在顯示 GIF 時反覆計算而造成卡頓。

Q: 假設聊天室需要端到端加密,如何設計加密與解密邏輯?可以用哪種演算法?

  1. 端到端加密的核心概念

    1. 只有通訊雙方能解密
      • 所有訊息都是以密文的形式經過伺服器,伺服器無法(也不應)取得解密金鑰,確保訊息私密性。
    2. 金鑰交換 (Key Exchange) 機制
      • 在首次連線或建立聊天室時,雙方需要安全地交換彼此的公鑰 (public key),以便後續衍生出對稱密鑰加密訊息。
    3. 前向保密 (Forward Secrecy)
      • 若要增強安全性,可考慮使用臨時 (Ephemeral) 金鑰,每次對話或每次訊息都重新交換新的會話金鑰,避免長期金鑰外洩後能解開所有歷史訊息。
    4. 可靠的身分驗證
      • 為避免中間人攻擊 (MITM),通常需透過「相互驗證公鑰指紋」或「掃描 QR code」等方式,確保交換的公鑰屬於真正的對方。
  2. 常見的 E2EE 演算法選擇

    1. 非對稱式:ECDH / RSA
      • RSA:
        • 傳統的公私鑰加密方式,但在行動裝置上較 ECDH 耗資源,金鑰長度較長。
      • ECDH:
        • 較傳統 RSA 擁有更高的安全性與更短的金鑰長度,計算效率佳,並普遍用於行動裝置的 E2EE。
        • 搭配臨時金鑰 (Ephemeral Keys) 產生 ECDHE(Ephemeral ECDH),可實現前向保密。
    2. 對稱式:AES-GCM / ChaCha20-Poly1305
      • AES-GCM:
        • 常見的對稱式加密演算法,結合 AES 加密與 GCM 模式的認證,提供加密與驗證功能。
      • ChaCha20-Poly1305:
        • 由 Google 設計的輕量級加密演算法,適合行動裝置使用,並提供高效的加密與驗證。
  3. 總結:若要兼顧前向保密、效率與普遍性,可考慮 (ECDH or ECDHE) + AES-GCM 方案。

  4. 端到端加密的整體流程示範

    • 以下以 ECDHE + AES-GCM 為例說明:
      1. 建立長期身分公私鑰 (Identity Key Pair)
        • 每個使用者在註冊或初次啟動 App 時,產生一組「長期身分私鑰 / 公鑰」。
        • 公鑰可上傳到伺服器做「可公開的身分資訊」。
        • 私鑰存在 iOS 的 Keychain 或透過 CryptoKit Secure Enclave 安全儲存。
      2. 金鑰交換 (Key Exchange)
        • 當兩位使用者 A 與 B 開始聊天時,雙方需要:
      3. 獲取彼此的身分公鑰 (可從伺服器取得,但需驗證真實性)。
      4. 產生「臨時金鑰 (Ephemeral Key Pair)」,然後進行 ECDH 計算,得到會話用的「對稱金鑰」。
        • (選擇性)若要使用更先進的 Double Ratchet / Signal Protocol,可以在每次收發訊息都更新臨時金鑰,確保更高安全性。
      5. 使用對稱金鑰加密聊天訊息
        • 雙方約定好一把對稱金鑰 (Session Key) 後,以 AES-GCM(或 ChaCha20-Poly1305)進行訊息加解密。
        • 加密結果為:ciphertext + nonce + authTag,然後再透過 WebSocket 或其他管道傳送至對方。
      6. 解密與驗證
        • 收到對方的密文後,使用前面協商好的對稱金鑰進行解密。
        • 若解密或驗證失敗,則視為訊息遭到竄改或錯誤。
      7. 更新金鑰(前向保密)
        • 若使用 Double Ratchet 或等同機制,則會針對每則訊息或每個階段定期更新金鑰。
        • 即使某次私鑰被攔截,攻擊者也無法解密過去或未來所有訊息。
  5. iOS 實作的可能方式

    1. CryptoKit (iOS 13+)
      • 提供高階 API 處理雜湊、對稱加解密 (AES.GCM)、公私鑰演算法 (Curve25519)。
      • 可以使用 SecureEnclave 來儲存私鑰,避免私鑰被竊取。
    2. Security / Keychain Services
      • 若需支援舊版 iOS,可透過 SecKeyCreateRandomKey、SecKeyCreateEncryptedData 等函式進行加解密。
      • 在 Keychain 中儲存私鑰。
    3. 第三方框架:Signal Protocol
      • 若想直接使用現成的端到端加密協議,可考慮 Open Whisper Systems 推出的 Signal Protocol (libsignal-protocol-c 等實作),可提供完整的「雙向 Ratchet」與前向保密機制。
    4. 驗證公鑰
      • 類似訊息應用 (Signal, WhatsApp) 會顯示一組安全碼或 QR code,讓雙方在實體/安全管道下比對,以避免中間人攻擊。
  6. 總結

    • 端到端加密 = 「金鑰只掌握在通訊雙方」,伺服器不能解密任何訊息。
    • 演算法推薦:
      • 金鑰交換:ECDH (或 ECDHE)
      • 對稱加密:AES-GCM 或 ChaCha20-Poly1305
      • 可用 iOS CryptoKit 或 Security API 來實作。
    • 安全維護:
      • 確保私鑰只存在使用者裝置的安全儲存區 (Keychain / Secure Enclave)。
      • 透過「公開金鑰指紋驗證」防止中間人攻擊。
      • 若需求嚴謹,可採用 Signal Protocol 的 Double Ratchet,擁有更高強度的前向保密。
  7. 遵循以上步驟與原則,就能在 iOS 聊天室中實現端到端加密機制,確保訊息資料的機密性與完整性。

Q:假設聊天室訊息頻繁更新,如何優化 UITableView 或 UICollectionView 的更新性能?

  1. 減少整體重載 (Reload) 次數
    1. 批次更新 (Batch Updates)
      • 若每收到一筆新訊息就呼叫 reloadData(),會造成頻繁的 UI 重繪,導致卡頓。
      • 建議先將多筆新訊息暫存起來,等累積到一定量、或在某個時間間隔後再一次性更新 UI。
      • 使用 tableView.beginUpdates() / tableView.endUpdates()(或 performBatchUpdates for UICollectionView)插入、刪除、更新多筆內容,效能會比多次單筆操作好。
    2. 差異化更新 (Diffable Data Source / 差分演算法)
      • iOS 13 起提供 UICollectionViewDiffableDataSource、iOS 14 起也支援 UITableViewDiffableDataSource。
      • 透過差分演算法,UIKit 只會重繪「改變」的 Cells,而非整個列表。
  2. 背景處理資料,減輕主線程負擔
    1. 背景執行解析 (Background Thread)
      • 例如:收到了大量訊息時,先在背景線程對資料進行處理、排序、去重等,再把最終結果交回主線程進行 UI 更新。
      • 避免在主線程做大量資料操作而卡住 UI。
    2. 預先計算或快取 (Pre-calculation / Caching)
      • 如果需要顯示複雜的訊息(如帶有文字、圖片、影片等),可以預先計算好 Cell 所需的高度、排版結果,減少在 cellForRowAt: 或 layoutSubviews 中的即時計算量。
  3. 避免頻繁的自動高度計算 (Auto Layout)
    1. 預先計算高度 / 使用預估高度
      • 如果訊息形態多樣,使用 tableView.estimatedRowHeight 或 self-sizing 是方便的做法,但在大量更新時,iOS 可能頻繁計算每個 Cell 高度。
      • 若可以,建議根據訊息內容在背景先計算好高度,或使用固定高度模板,以降低布局計算量。
    2. 減少嵌套 View 層級
      • 減少過深的 Auto Layout 階層,也能優化列表的重繪效率。
      • 若 UI 結構簡單,可用程式計算 Frame 來微幅提升效能(但通常還是要權衡開發維護成本)。
  4. 特殊策略:合併訊息顯示
    1. 合併連續訊息
      • 聊天室常會有同一個人連續發多則訊息,或許可在 UI 上合併顯示成一個大的氣泡訊息欄位,減少 Cell 數量。
      • 例如 Telegram 就在同一發訊者的連續訊息間省略頭像、名稱,只在第一則顯示。這也可以優化表格的重新繪製。
    2. 部分訊息以文字為主,延遲載入多媒體
      • 若有圖片 / GIF / 影片,可先顯示文字或縮圖,等使用者滑動到該區域時,才載入更高解析度的內容。

Q:如果需要實現全局的 Authorization Token 管理(包括處理 Token 過期的情況),如何設計?

  • token 儲存位置:
    • Keychain or CryptoKit + Secure Enclave
    • 在 App 啟動時將 token 載入到記憶體中
  • 設計網路層攔截器 (Interceptor)
    • URLSession / Alamofire 都可以透過自定義的機制或建構一層封裝,用來在每次 API 呼叫前自動加上 Token。
    • 在攔截器中,可以檢查 token 是否過期,若過期則自動重新取得新的 token。
    • 如果是 401 (Token 過期或無效),則進入 Token 更新流程(Refresh Token Flow)。
      1. 攔截器檢查是否正在進行 Refresh Token 流程,如否,呼叫 Refresh Token API。
      2. 如果 Refresh 成功,更新新的 Access Token(必要時一併更新 Refresh Token),再次重試原本的 API。
      3. 如果 Refresh 也失敗(例如 Refresh Token 過期),則導引用戶重新登入。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment