基於MARS的移動APP網絡通訊開發實踐
做者|付明旺
編輯|覃雲
android
Mars 簡介
MARS 做爲優秀的跨平臺網絡層通訊方案開源 1 年多了,github 上收穫過萬的 star,期間較爲穩定更新並不頻繁。基於內核 socket MARS 針對弱網絡環境下的移動應用作了不少比較實用的優化,詳細的優化點和原理在其開源項目的 wiki 裏有不少文檔說的比較清楚了 Mars wiki(https://github.com/Tencent/mars/wiki)。 本人恰好參與了多款具備 IM 功能的應用開發,底層網絡通訊集成了 MARS,該底層通信模塊已經穩定服務於 Android/Ios/windows 平臺上多款產品。網上有關 MARS 使用的實踐經驗還比較少見,這裏總結一下供你們參考。git
Mars 使用實踐
MARS 支持長鏈接的同時也支持短連接,短連接主要映射成有限制的 http 鏈接。短鏈接不是 MARS 的長處,不在本文涉獵,後面提到的全部鏈接如無特指均爲長鏈接。github
長鏈接數據流及 API 一覽
讀完文檔就能把 MARS 用起來仍是得靠運氣的,索性把代碼走讀了一下,恰好能夠梳理梳理長鏈接的數據流。
上面數據流展現了 client 端要發送數據的整個過程和涉及到主要 API,以 Android API 爲例,MARS 提供了涉及數據輸出的如下重要 API:
windows
// 初始化 public static void init(Context _context, Handler _handler) // 設置長鏈接 server public static void setLonglinkSvrAddr(final String host, final int[] ports) //client 發送任務接口 public static native void startTask(final Task task); //server 主動推送回調 void onPush(final int cmdid, final byte[] data); //client 發送數據的回調 boolean req2Buf(final int taskID, Object userContext, ByteArrayOutputStream reqBuffer, int[] errCode, int channelSelect); //client 收到迴應數據的回調 int buf2Resp(final int taskID, Object userContext, final byte[] respBuffer, int[] errCode, int channelSelect); //client 發送任務結束的回調 int onTaskEnd(final int taskID, Object userContext, final int errType, final int errCode); ![](https://s4.51cto.com/images/blog/202012/20/44f7d22c42bf27ed2aee9206437cb2dc.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)``` 實際過程當中 MARS 提供的接口就比較複雜了,這邊也放一張總結圖感覺一 ### task 概念及消息流程 Mars 對外提供的消息收發接口是基於 task 的,要先理解 task 的概念。Mars 經過任務來描述一次數據的發送、應答和最終結束。 * APP 啓動發送數據 startTask; * MARS 回調 req2Buf 從 APP 得到該任務要傳輸的數據; * MARS 回調buf2Resp 向 APP 投遞該任務的應答數據; * MARS 回調 onTaskEnd通知 APP 該任務執行狀態,成功或者失敗。 數據傳輸過程有許多控制參數,任務的定義就是這些控制參數的集合。
public int taskID; // 任務惟一標識,會自動生成。
public int channelSelect; // 任務走長連仍是短連,或者兩個均可以,可選值見 EShort。ELong EBoth
public int cmdID; // 長連的 cgi 命令號,用於標識長連請求的 cgi。長連必填項,至關於短連的 cgi。
public String cgi; // 短連的 URI,短連必填項。
public ArrayList<String> shortLinkHostList; // 短連所用 host 或者 ip,若是是走短連的任務,必填項。
安全
//optional
public boolean sendOnly; // true 爲不須要等待回包,false 爲須要等待回包。默認值爲 false
public boolean needAuthed; // true 爲須要登錄態才能發送的任務,false 爲任何狀態下均可以發送的任務,默認值爲 true。
public boolean limitFlow; // true 在手機網絡狀況下會走流量限制,false 不會。默認值爲 true。大數據包請置爲 false。
public boolean limitFrequency; // true 會走頻率限制,false 不會。默認值爲 true。 頻繁發送相同包內容的 Task 請置爲 false。
服務器
public int channelStrategy; // channelSelect 爲 EBoth 狀況下,該值爲 ENORMAL 長連存在則走長連,該值爲 EFAST,即便長連存在,可是長鏈接隊列裏有別的任務的時候,會優先走短鏈接。默認值爲 ENORMAL
public boolean networkStatusSensitive; // true 沒網絡的狀況下任務會直接返回失敗,不會嘗試去走網絡,false 即便沒網絡,也會嘗試創建鏈接。默認爲 false。
public int priority; // 任務的優先級,可選值見 ETASK_PRIORITY_XX。
public int retryCount = -1; // 任務重試次數,設爲 -1,若是任務失敗,會走 Mars 的重試邏。輯,設置大於等於 0 的數,會以此爲準,默認值 -1。
public int serverProcessCost; // 該 Task 等待 SVR 處理的最長時間, 也即預計的 SVR 處理耗時。
public int totalTimeout; // 該 Task 總的超時時間,設置小於等於零的值,會走 Mars 的超時邏輯,不然以此值爲準,默認值爲 0。
public Object userContext; // 用戶變量,可填任何值,Mars 不會更改該變量。
public String reportArg; // 統計上報所用,可忽略。
網絡
### 多 ip server 端配置多個 IP,MARS 同時發起多個鏈接並取其中最快創建的鏈接使用,其餘釋放掉。該策略確實能提升 client 創建鏈接的成功率和速度,同時也給 server 端帶來了併發的壓力,須要根據自身的用戶規模和 server 資源狀況謹慎使用。咱們開啓了多 IP 的功能,有幾點值得注意。 MARS 提供的接口上定義了幾種不一樣的 ip,必定要當心應用。 IP 使用 Debug IP 調試 IP,線上勿用。 NewDns IP 自開發 DNS 解析 IP。 DNS IP MARS 解析出的 DNS IP。 Backup IP 保底 IP。 * 經過 setLonglinkSvrAddr 配置了 server 的域名地址,雖然該域名對應多個 IP,但不必定多 IP 的功能就啓用了。不少狀況下 MARS DNS 解析時,DNS 服務器返回的 IP 會根據運營商狀況只返回一個 IP 地址。 * 能夠經過 onNewDns 的回調,本身把多個 IP 傳給 MARS 使用,解決 1 的問題。 * BackupIp 推薦配置一個穩定的 IP,不要空着。由於前面的各種 IP 在屢次失敗的狀況下會短時間禁用掉,但 backupIp 會一直生效。 ### 認證 安全是永恆的話題,長鏈接創建後的第一件事情就是用戶鑑權認證。過程就是 client 發送一些 server 端認識的信息來證實本身是合法用戶,能夠繼續通訊。 MARS 提供了makesureAuthed/getLongLinkIdentifyCheckBuffer/onLongLinkIdentifyResp等接口給 APP,但該接口是經過回調的方式被動觸發發送鑑權信息的。APP 主動發起鑑權信息,也一樣能夠走通用 startTask接口。 比較須要注意的是當 APP 的鑑權信息發送改變 (token 失效 / 登出從新登陸) 時,就須要這種主動斷開當前鏈接從新鑑權。 ### 重連 MARS 一直致力於維持鏈接常在,鏈接斷開會自動重連。惋惜沒有提供給 APP 主動斷開鏈接和重連的 API,APP 會有場景須要主動斷開當前鏈接,好比上面提到的認證信息更新時或者用戶業務登出時。MARS 的 redoTasks會有斷開鏈接的效果,咱們開發 APP 時就比較討巧的用了這個 API 來作主動重連的操做。 ### 心跳改造 心跳是保持長鏈接的必需手段,MARS 也提供了智能心跳的方案。很遺憾咱們的產品是 server 端主動發心跳包的方案,恰好跟 MARS 相反的方向。稍稍改造禁用掉 MARS 的客戶端心跳,走 onPush和 startTask接口一樣能夠實現心跳。 ### APP 協議實現 MARS 要求實現 longlink_packer.cc.rewriteme 中定義的函數來達到自定義 APP 協議的目的。實際產品中 server 端和 client 的通訊協議確定須要開發定製的,這部分的實現幾乎是必需的。 能夠根據產品本身的特性定製私有的通信協議,這裏本人給出一個通信協議的例子:
struct MessageFormat { uint32_t magicNum; // magically defined num for error message checking uint32_t messageId; // unqiue message identification uint32_t len; // body length char data[]; // body start byte };
這幾乎是最精簡的一個通信協議了,尤爲比較重要的是 messageId。messageId 對應於 MARS 的 taskId,用於串聯起來 IM 消息的發送和應答消息對。好比 A 發送了 messageId=1(taskId=1)的「How are you?」到 B,B 收到後一樣以 messageId=1(taskId=1) 迴應「I'm fine"。這樣在對 A 端 MARS taskId=1 的任務管理全靠這個 messageId 來標記了。同時有幾點注意事項以下: * req2Buf/buf2Resp/onPush/onTaskEnd/__unpack_test 等數據傳輸相關的回調都是發生在長鏈接線程裏,切記不要在這些回調裏面作阻塞性或者耗時的操做,會影響數據傳輸的效率和鏈接的維持。 * __unpack_test 回調主要是解決業務包投遞時機的問題。tcp 是流式協議,業務包有可能分紅多個 tcp 包投遞,經過該回調來告訴 MARS 是否已經收到完整的業務包,是否能夠往業務層投遞了。 * onTaskEnd用來回調給業務層發送任務的最終狀態。一般業務層的發送包都會指望一個業務層的應答包,這樣順序就是 startTask-->req2Buf(業務組包)-->server-->buf2Resp(業務解包)-->onTaskEnd。若是 client 只是發送業務包不要求業務應答 (task 屬性設置爲 send_only=true),順序是這樣的 startTask-->req2Buf(業務組包)-->onTaskEnd-->server,onTaskEnd 直接返回成功不表明 server 端確定收到了該業務包。 我這邊有一個 MARS 的二次封裝,提供了上面簡單的通信協議同時封裝了 Mars task 的管理,有興趣的同窗能夠參考一下,文末有連接地址。 ### 日誌 MARS xlog 經過磁盤文件內存映射的方式得到高效可靠的日誌方案,詳細原理見高性能日誌模塊 xlog: https://mp.weixin.qq.com/s/cnhuEodJGIbdodh0IxNeXQ 實際線上產品使用推薦: * 每一個進程一個日誌文件,每一個進程須要單獨配置日誌; * 使用異步日誌打印; * 定義 XLOGGER_TAG 來嵌入日誌 tag,方便日誌過濾; * 每條日誌設置合理等級,控制日誌文件大小; * 日誌內不包含敏感信息能夠不加密。 ### 監控 MARS 有單獨的網絡監控模塊 SDT,目前還不能獨立使用。網絡通訊模塊 STN 裏面也有不少網絡狀況和任務統計的實現,能夠稍微改造一下把這些統計項暴漏給 APP 層。APP 就能夠蒐集統計這些信息彙總到 server 端,而後運營人員能夠比較輕鬆的瞭解當前全部客戶端的網絡表現啦。 順帶提一下 MARS 的上報長鏈接狀態的接口 reportConnectInfo 一個小小的提示。該回調函數上報的狀態存在必定的迷惑性。底層網絡長鏈接狀態發生變化時會觸發該狀態上報接口調用,但真正調用到該接口時上報的網絡狀態反應的是當時的鏈接狀態。舉個例子,鏈接斷開觸發上報,上報接口 reportConnectInfo 是在另一個線程裏被調用的,真正調用時狀態可能已經變爲已鏈接了,這樣 APP 就缺失一個感知鏈接斷開的機會。因此 APP 不能直接依賴該接口作嚴格的邏輯處理或狀態維護。 ## 使用總結 * IM 長鏈接維持「費盡心機」。多 ip 併發鏈接,超時重傳策略,智能心跳,網絡 RTT 時間監測,玩的花樣百出,甚至連電信運營商網絡這層的保活都作了,結果就是 MARS 提供了更靈敏、反應更迅速、更適合移動通訊的網絡通道。 * 日誌方案穩定高效,性能很好,使用期間基本沒遇到丟日誌的問題。 * 跨平臺,android/IOS/windows 一致性的通信能力體驗,同時節省開發資源。 * 接口繁冗,深度使用須要使用者仔細讀源代碼。 * 文檔不夠友好,社區不活躍。 * MARS 層次能夠更清晰些,突出網絡層通道的重點。剝離業務層的功能,好比認證功能。去除 task 概念代之以跟業務層約定簡潔的協議頭 (好比全部包開頭的 32bit 爲包 sequence),這樣接口可能會簡潔不少。 總的來講,MARS 是一款出色的移動通訊產品網絡層解決方案,若是你須要移動端實時通訊能夠嘗試在產品中集成 MARS。若是你以爲接口使用有些複雜,我這邊有一個 MARS 的二次封裝,你能夠作一個參考或者直接用一下,至少看起來簡單了不少。好比這個 C++ 的例子:
// 推送監聽類
class PushHandler :PushListener {
virtual void onPush(const std::string &message) {
併發
}
};
// 應答監聽類
class ResponseHandler :ResponseListener {
virtual void onResponse(const std::string &message) {
printf("response received:%s \n",message.c_str());
}
virtual void onError(const int err, const std::string &errMsg) {
printf("message send failed:%d \n",err);
}
virtual void onSuccess() {
printf("message send ok \n");
}
};
app
int main(int argc, char argv[]) {
MarsConfig config("39.106.56.27",9001);
init(config);
PushHandler pushHandler;
registerPushListener((PushListener
)&pushHandler);
_sleep(2000);
ResponseHandler responseHandler;
std::string message = "hello";
sendMessage(message.c_str(), message.size(), (ResponseListener*)&responseHandler);
_sleep(200000);
return 0;
}
異步
這個 MARS 的二次封裝我放在了 github 上,你們能夠做爲一個瞭解怎樣使用 MARS 的入口: MarsWrapper:https://github.com/microelec/mars_wrapper