用網(wǎng)絡(luò)架構(gòu)實戰(zhàn):信號監(jiān)聽與連接管理的核心策略)
1. 項目緣起一個被“信號”與“連接”困擾的移動應(yīng)用最近在做一個內(nèi)部代號為“DAVE”的移動應(yīng)用項目團隊里幾乎每個人都為兩個詞頭疼過信號和連接。這聽起來像是網(wǎng)絡(luò)工程師或者通信專業(yè)才該操心的事但事實上任何一個涉及實時數(shù)據(jù)交互、離線緩存、多端同步的現(xiàn)代App都繞不開這兩個基礎(chǔ)又核心的命題。我們最初的想法很簡單做一個能流暢展示實時數(shù)據(jù)、在不同網(wǎng)絡(luò)環(huán)境下都能穩(wěn)定工作的工具型App。但真做起來才發(fā)現(xiàn)從用戶點擊圖標(biāo)到界面成功渲染出數(shù)據(jù)這中間“信號”的強弱、“連接”的通斷直接決定了用戶體驗是“絲般順滑”還是“卡成PPT”。DAVE App的定位決定了它無法回避這兩個問題。它需要從服務(wù)器拉取動態(tài)內(nèi)容可能需要與藍牙外設(shè)通訊還要處理用戶主動觸發(fā)的各種網(wǎng)絡(luò)請求。在項目初期我們天真地認為用個成熟的網(wǎng)絡(luò)庫處理一下加載狀態(tài)和錯誤回調(diào)就萬事大吉。結(jié)果測試階段在地鐵、電梯、停車場等弱網(wǎng)環(huán)境或者網(wǎng)絡(luò)切換的瞬間各種詭異問題層出不窮頁面白屏、數(shù)據(jù)錯亂、操作無響應(yīng)甚至直接閃退。用戶不會關(guān)心你是不是用了最新的RESTful API或者GraphQL他們只在乎App“能不能用”、“快不快”。因此我們把“信號與連接”的穩(wěn)定性提升到了與核心業(yè)務(wù)邏輯同等重要的架構(gòu)層面來系統(tǒng)性地解決。2. 移動端“信號”的本質(zhì)不只是網(wǎng)絡(luò)信號強度當(dāng)我們談?wù)揂pp的“信號”時絕不僅僅指手機頂部的那個Wi-Fi或蜂窩信號格。那只是最表層的物理層信號。在應(yīng)用層我們需要關(guān)注的“信號”是一個更廣義的概念一切表征外部環(huán)境或內(nèi)部狀態(tài)是否“就緒”或“可用”的指示。2.1 網(wǎng)絡(luò)可達性信號從“有網(wǎng)無網(wǎng)”到“網(wǎng)絡(luò)質(zhì)量”最直接的信號就是網(wǎng)絡(luò)狀態(tài)。但簡單地監(jiān)聽“網(wǎng)絡(luò)已連接/斷開”是遠遠不夠的。我們遇到過在信號滿格但DNS解析失敗的公共Wi-Fi下App依然無法工作的案例。2.1.1 精細化網(wǎng)絡(luò)狀態(tài)監(jiān)聽我們摒棄了簡單的CONNECTED/DISCONNECTED二元判斷引入了一套分層狀態(tài)機完全離線無任何網(wǎng)絡(luò)連接飛行模式、徹底關(guān)閉網(wǎng)絡(luò)。受限連接連接了Wi-Fi或蜂窩網(wǎng)絡(luò)但無法訪問互聯(lián)網(wǎng)如需要認證的公共Wi-Fi、企業(yè)內(nèi)網(wǎng)隔離。慢速連接網(wǎng)絡(luò)可達但質(zhì)量極差如2G網(wǎng)絡(luò)、信號很弱的蜂窩網(wǎng)絡(luò)。良好連接網(wǎng)絡(luò)質(zhì)量滿足基本交互需求3G/4G/穩(wěn)定Wi-Fi。極佳連接高速低延遲網(wǎng)絡(luò)5G、高速寬帶Wi-Fi。在Android上我們結(jié)合ConnectivityManager和主動探測如對固定域名發(fā)起一個輕量級HTTP HEAD請求來判斷在iOS上除了Reachability我們也通過URLSession發(fā)起一個探測請求來評估實際連通性。這個狀態(tài)會作為一個全局的“信號強度”值供其他模塊決策。2.1.2 網(wǎng)絡(luò)類型感知與策略調(diào)整知道是蜂窩網(wǎng)絡(luò)還是Wi-Fi也至關(guān)重要。在DAVE App中如果檢測到用戶使用的是蜂窩網(wǎng)絡(luò)我們會自動降低非關(guān)鍵圖片的加載質(zhì)量從WebP高清降至中清。暫停大型數(shù)據(jù)包的預(yù)加載或后臺同步。對于視頻等富媒體內(nèi)容給出“當(dāng)前為蜂窩網(wǎng)絡(luò)繼續(xù)播放將消耗流量”的提示。 這些策略的調(diào)整都是基于“當(dāng)前網(wǎng)絡(luò)類型”這個信號觸發(fā)的。2.2 設(shè)備硬件與系統(tǒng)信號“信號”也來自設(shè)備本身。例如電量信號當(dāng)系統(tǒng)廣播電量低于20%時我們應(yīng)減少非必要的后臺網(wǎng)絡(luò)請求和計算以延長續(xù)航。存儲空間信號本地緩存快滿時需要更積極地清理過期數(shù)據(jù)并提示用戶。藍牙/GPS開關(guān)狀態(tài)如果App功能依賴這些硬件它們的開啟狀態(tài)就是關(guān)鍵的“信號”。我們需要優(yōu)雅地引導(dǎo)用戶開啟而不是在調(diào)用時直接崩潰。處理這些系統(tǒng)信號的關(guān)鍵在于監(jiān)聽與響應(yīng)。我們需要在合適的生命周期如App啟動、進入前臺注冊相應(yīng)的BroadcastReceiverAndroid或NotificationCenter觀察者iOS并在收到信號后更新App內(nèi)部的狀態(tài)機或UI。3. “連接”的構(gòu)建與維護從短連接到長連接如果說“信號”是環(huán)境感知那么“連接”就是主動出擊建立通道。在DAVE中連接主要分為兩類短連接請求-響應(yīng)和長連接持久通道。3.1 HTTP短連接不只是發(fā)個請求那么簡單對于大多數(shù)API調(diào)用我們使用基于HTTP/HTTPS的短連接。但“短”不代表簡單。一個健壯的短連接管理需要處理以下問題3.1.1 連接超時與重試策略這是最基礎(chǔ)的防線。我們?yōu)椴煌愋偷恼埱笈渲昧瞬町惢某瑫r時間關(guān)鍵業(yè)務(wù)請求如登錄、支付連接超時設(shè)為10秒讀寫超時設(shè)為30秒并配備指數(shù)退避算法的重試機制最多2次。重試前會再次檢查網(wǎng)絡(luò)信號狀態(tài)。普通數(shù)據(jù)請求如列表加載連接/讀寫超時設(shè)為15秒通常只重試1次。非關(guān)鍵請求如日志上報、行為統(tǒng)計超時時間更短如5秒且不重試失敗即丟棄。這里有個經(jīng)驗不要盲目重試。如果是因為網(wǎng)絡(luò)不可用信號差導(dǎo)致的失敗立即重試只會增加用戶設(shè)備的功耗和焦慮感。正確的做法是將失敗的請求暫存到一個待執(zhí)行隊列Pending Queue等網(wǎng)絡(luò)信號恢復(fù)為“良好”或以上時再自動重試。3.1.2 連接復(fù)用與連接池為了提升性能必須利用HTTP/1.1的Keep-Alive或HTTP/2的多路復(fù)用特性。這意味著我們需要正確配置網(wǎng)絡(luò)庫如OkHttp、URLSession的連接池參數(shù)。例如針對我們的主API域名我們設(shè)置了最大空閑連接數(shù)5 保持活動時間5分鐘這樣可以避免頻繁的TCP三次握手和TLS握手顯著降低延遲。特別是在用戶快速滑動列表觸發(fā)多個類似請求時效果提升明顯。3.1.3 請求優(yōu)先級與調(diào)度當(dāng)多個請求同時發(fā)起時誰先誰后我們實現(xiàn)了一個簡單的請求調(diào)度器根據(jù)請求的“優(yōu)先級”標(biāo)簽如HIGH,NORMAL,LOW來安排執(zhí)行順序。高優(yōu)先級的請求如用戶點擊按鈕觸發(fā)的操作會優(yōu)先進入連接池執(zhí)行低優(yōu)先級的請求如預(yù)加載下一頁數(shù)據(jù)可能會被延遲或合并。3.2 WebSocket長連接狀態(tài)同步與實時推送對于DAVE中需要實時數(shù)據(jù)更新的模塊如協(xié)同編輯、實時通知我們引入了WebSocket長連接。長連接的管理比短連接復(fù)雜一個數(shù)量級核心在于?;?、重連與狀態(tài)同步。3.2.1 心跳機制與自動重連WebSocket連接可能因為網(wǎng)絡(luò)波動、NAT超時、服務(wù)器重啟等原因斷開。我們必須實現(xiàn)心跳包Ping/Pong來保持連接活躍并探測連接健康度。我們的心跳間隔是30秒如果連續(xù)2次心跳無響應(yīng)則判定連接失效觸發(fā)重連。重連邏輯不是簡單的while(true)循環(huán)。我們采用了“增量退避重連”策略第一次斷開立即重連。重連失敗等待2秒后重試。再次失敗等待4秒、8秒、16秒...依次遞增直到達到最大值如64秒。一旦重連成功間隔時間重置。 同時在重連期間所有需要通過WebSocket發(fā)送的消息會被緩存到隊列中待連接恢復(fù)后按序發(fā)送。3.2.2 連接狀態(tài)與UI聯(lián)動長連接的狀態(tài)連接中、已連接、斷開、重連中必須清晰地反饋給用戶。我們在App的全局狀態(tài)管理中維護了一個websocketState變量關(guān)鍵的UI組件如顯示實時數(shù)據(jù)的頁面會監(jiān)聽這個狀態(tài)。當(dāng)狀態(tài)變?yōu)椤皵嚅_”或“重連中”時頁面頂部會顯示一個非模態(tài)的提示條告知用戶“連接已斷開正在嘗試重連...”而不是讓數(shù)據(jù)突然停止更新讓用戶困惑。4. 弱網(wǎng)與離線場景的應(yīng)對策略“信號”不可能永遠滿格“連接”也不可能永遠暢通。設(shè)計上就必須考慮弱網(wǎng)和離線情況這直接體現(xiàn)了App的魯棒性。4.1 數(shù)據(jù)緩存策略多級緩存體系我們建立了一個三級緩存體系來保證數(shù)據(jù)的可用性內(nèi)存緩存L1使用LRU策略緩存最常訪問的、已解析好的數(shù)據(jù)模型對象。響應(yīng)速度最快生命周期隨App或頁面。磁盤緩存L2將API返回的原始JSON數(shù)據(jù)或序列化后的對象以Key-Value形式存儲于本地數(shù)據(jù)庫如SQLite或文件系統(tǒng)。這里緩存的是“數(shù)據(jù)快照”有效期為業(yè)務(wù)決定如列表數(shù)據(jù)緩存1小時用戶信息緩存1天。預(yù)置緩存L3對于App首次啟動時必須展示的內(nèi)容如引導(dǎo)圖、城市列表直接打包在App資源文件中作為兜底數(shù)據(jù)。當(dāng)發(fā)起一個網(wǎng)絡(luò)請求時流程如下首先檢查內(nèi)存緩存命中則直接返回。未命中則檢查磁盤緩存如果存在且未過期則返回緩存數(shù)據(jù)同時異步發(fā)起網(wǎng)絡(luò)請求以獲取最新數(shù)據(jù)并更新緩存Cache-Then-Network。如果磁盤緩存也沒有或已過期則等待網(wǎng)絡(luò)請求結(jié)果。如果網(wǎng)絡(luò)請求失敗則返回過期的磁盤緩存數(shù)據(jù)如果有并明確標(biāo)記“數(shù)據(jù)可能不是最新的”。4.2 操作隊列與沖突解決在弱網(wǎng)環(huán)境下用戶的操作可能無法立即得到服務(wù)器響應(yīng)。例如用戶編輯了一條筆記后點擊保存此時網(wǎng)絡(luò)斷開。我們的策略是本地優(yōu)先立即在UI上顯示保存成功更改本地數(shù)據(jù)模型給用戶即時反饋。操作入隊將“更新筆記”這個操作封裝成一個任務(wù)放入一個持久化的待同步隊列Pending Sync Queue。這個隊列會被存儲在本地數(shù)據(jù)庫中即使App重啟也不會丟失。延遲同步當(dāng)網(wǎng)絡(luò)信號恢復(fù)時一個后臺服務(wù)會按序取出隊列中的任務(wù)嘗試向服務(wù)器提交。提交成功后將該任務(wù)從隊列中移除。這里會遇到經(jīng)典的數(shù)據(jù)沖突問題如果用戶在離線期間編輯了筆記A同時另一臺設(shè)備在線也編輯了筆記A并同步成功當(dāng)本設(shè)備聯(lián)網(wǎng)后誰的修改該被保留我們采用的策略是“客戶端最后寫入勝出”LWW但附帶一個重要的改進每次編輯都會生成一個基于時間戳的版本號。同步時如果服務(wù)器數(shù)據(jù)的版本號比本地更新則提示用戶“數(shù)據(jù)已被他人修改請確認是否覆蓋”。雖然不完美但對于我們的業(yè)務(wù)場景個人工具為主協(xié)同較少是簡單有效的。4.3 界面反饋與用戶體驗在弱網(wǎng)或斷網(wǎng)時UI的反饋至關(guān)重要目的是消除用戶的焦慮和不確定性。加載狀態(tài)任何網(wǎng)絡(luò)請求都要有明確的加載指示如骨架屏、加載動畫超時后要能取消。錯誤提示區(qū)分錯誤類型。是“網(wǎng)絡(luò)不可用”還是“服務(wù)器開小差了”5xx錯誤或是“請求的內(nèi)容不存在”404提示語要友好且 actionable。例如“網(wǎng)絡(luò)不給力請檢查后重試”比“請求失敗”要好得多。離線模式對于核心功能設(shè)計完整的離線使用流程。讓用戶明確知道哪些功能在離線時可用哪些不可用。例如DAVE的文檔查看和編輯功能在離線時完全可用而“分享協(xié)作”功能則會置灰并提示“需要網(wǎng)絡(luò)連接”。5. 實戰(zhàn)中的“信號”與“連接”調(diào)試技巧理論終須落地在開發(fā)和測試階段我們積累了一些實用的調(diào)試和優(yōu)化方法。5.1 模擬各種網(wǎng)絡(luò)環(huán)境不能依賴“我辦公室Wi-Fi很好”來開發(fā)。必須主動模擬惡劣環(huán)境。開發(fā)者工具Android Studio的Profiler和Xcode的Network Conditioner都提供了模擬不同網(wǎng)絡(luò)2G、3G、高延遲、丟包率的功能。這是開發(fā)階段的必備測試。硬件模擬使用網(wǎng)絡(luò)鏈路模擬器如ATC可以制造出更真實、更復(fù)雜的網(wǎng)絡(luò)波動場景比如周期性斷網(wǎng)、帶寬限制等。真實場景測試一定要去電梯、地下室、地鐵、快速移動的車廂里進行實測。很多問題如蜂窩網(wǎng)絡(luò)與Wi-Fi切換時的IP地址變化只有在真實環(huán)境中才會暴露。5.2 關(guān)鍵指標(biāo)監(jiān)控與埋點我們需要數(shù)據(jù)來證明優(yōu)化效果并發(fā)現(xiàn)問題。網(wǎng)絡(luò)請求大盤監(jiān)控平均響應(yīng)時間、成功率2xx/3xx比例、錯誤率4xx、5xx、超時、網(wǎng)絡(luò)斷開。按API端點、網(wǎng)絡(luò)類型Wi-Fi/蜂窩、系統(tǒng)版本等維度細分。自定義埋點在代碼關(guān)鍵路徑埋點記錄如“從點擊到頁面首屏渲染完成的時間”、“WebSocket連接建立平均耗時”、“弱網(wǎng)環(huán)境下操作失敗率”等。用戶反饋通道在App內(nèi)設(shè)置便捷的“報告問題”入口自動附帶當(dāng)前的網(wǎng)絡(luò)信號強度、連接類型、App版本等信息幫助快速定位網(wǎng)絡(luò)相關(guān)問題。5.3 常見坑點與解決方案DNS解析超時尤其是在國內(nèi)復(fù)雜的網(wǎng)絡(luò)環(huán)境下。解決方案是考慮接入HTTPDNS服務(wù)或者在本地的網(wǎng)絡(luò)庫中適當(dāng)調(diào)大DNS解析的超時時間并做好失敗后使用系統(tǒng)DNS的降級方案。SSL握手失敗在舊版本Android系統(tǒng)或某些定制ROM上可能遇到。確保服務(wù)器支持較廣泛的TLS協(xié)議版本和加密套件。對于非關(guān)鍵信息展示可以考慮在首次失敗后嘗試降級到HTTP需權(quán)衡安全風(fēng)險。后臺網(wǎng)絡(luò)請求被限制Android和iOS都有越來越嚴(yán)格的后臺網(wǎng)絡(luò)限制。對于需要后臺同步的任務(wù)要正確使用WorkManagerAndroid或Background TasksiOS等后臺任務(wù)調(diào)度機制并聲明合理的后臺網(wǎng)絡(luò)使用權(quán)限。Wi-Fi代理與證書鎖定企業(yè)環(huán)境或特殊網(wǎng)絡(luò)下用戶設(shè)備可能設(shè)置了代理或安裝了自定義根證書。如果你的App使用了證書鎖定Certificate Pinning在這些環(huán)境下會直接失敗。通常的折中方案是在Debug版本或特定配置下關(guān)閉證書鎖定便于調(diào)試和適配。開發(fā)DAVE App的過程就是不斷與“信號”和“連接”這兩個老朋友斗智斗勇的過程。它們不像炫酷的UI動畫或復(fù)雜的業(yè)務(wù)邏輯那樣吸引眼球但卻是整個應(yīng)用體驗的基石。處理好了用戶無感處理不好差評如潮。我的體會是對待網(wǎng)絡(luò)問題必須抱有“敬畏之心”不能假設(shè)環(huán)境永遠理想。從架構(gòu)設(shè)計之初就要把網(wǎng)絡(luò)當(dāng)作一個“不可靠的、狀態(tài)多變的”外部依賴來對待通過狀態(tài)監(jiān)聽、分層緩存、隊列管理、優(yōu)雅降級等一系列組合拳才能打造出真正健壯、用戶信賴的移動應(yīng)用。