基於MARS的移動APP網絡通訊開發實踐

Mars簡介

MARS做爲優秀的跨平臺網絡層通訊方案開源1年多了,github上收穫過萬的star,期間較爲穩定更新並不頻繁。基於內核socket MARS針對弱網絡環境下的移動應用作了不少比較實用的優化,詳細的優化點和原理在其開源項目的wiki裏有不少文檔說的比較清楚了Mars wiki。本人恰好參與了多款具備IM功能的應用開發,底層網絡通訊集成了MARS,該底層通信模塊已經穩定服務於Android/Ios/windows平臺上多款產品。網上有關MARS使用的實踐經驗還比較少見,這裏總結一下供你們參考。android

Mars使用實踐

MARS支持長鏈接的同時也支持短連接,短連接主要映射成有限制的http鏈接。短鏈接不是MARS的長處,不在本文涉獵,後面提到的全部鏈接如無特指均爲長鏈接。git

長鏈接數據流及API一覽

讀完文檔就能把MARS用起來仍是得靠運氣的,索性把代碼走讀了一下,恰好能夠梳理梳理長鏈接的數據流。
長鏈接數據流github

上面數據流展現了client端要發送數據的整個過程和涉及到主要API,以Android API爲例,MARS提供了涉及數據輸出的如下重要APIwindows

//初始化
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);

實際過程當中MARS提供的接口就比較複雜了,這邊也放一張總結圖感覺一下。
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

  • 比較須要注意的是當APP的鑑權信息發送改變(token失效/登出從新登陸)時,就須要這種主動斷開當前鏈接從新鑑權。

重連

MARS一直致力於維持鏈接常在,鏈接斷開會自動重連。惋惜沒有提供給APP主動斷開鏈接和重連的API,APP會有場景須要主動斷開當前鏈接,好比上面提到的認證信息更新時或者用戶業務登出時。MARS的 redoTasks會有斷開鏈接的效果,咱們開發APP時就比較討巧的用了這個API來作主動重連的操做。異步

心跳改造

心跳是保持長鏈接的必需手段,MARS也提供了智能心跳的方案。很遺憾咱們的產品是server端主動發心跳包的方案,恰好跟MARS相反的方向。稍稍改造禁用掉MARS的客戶端心跳,走 onPushstartTask接口一樣能夠實現心跳。

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。實際線上產品使用推薦

  • 每一個進程一個日誌文件,每一個進程須要單獨配置日誌
  • 使用異步日誌打印
  • 定義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");
    }
};

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

相關文章
相關標籤/搜索