基于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

相关文章
相关标签/搜索