微信号:frontshow

介绍:InfoQ大前端技术社群:囊括前端、移动、Node全栈一线技术,紧跟业界发展步伐。

基于MARS的移动APP网络通信开发实践

2018-09-19 17:27 付明旺
作者|付明旺
编辑|覃云
Mars 简介

MARS 作为优秀的跨平台网络层通信方案开源 1 年多了,github 上收获过万的 star,期间较为稳定更新并不频繁。基于内核 socket MARS 针对弱网络环境下的移动应用做了很多比较实用的优化,详细的优化点和原理在其开源项目的 wiki 里有很多文档说的比较清楚了 Mars wiki(https://github.com/Tencent/mars/wiki)。 本人刚好参与了多款具有 IM 功能的应用开发,底层网络通信集成了 MARS,该底层通讯模块已经稳定服务于 Android/Ios/windows 平台上多款产品。网上有关 MARS 使用的实践经验还比较少见,这里总结一下供大家参考。

Mars 使用实践

MARS 支持长连接的同时也支持短链接,短链接主要映射成有限制的 http 连接。短连接不是 MARS 的长处,不在本文涉猎,后面提到的所有连接如无特指均为长连接。

  长连接数据流及 API 一览

读完文档就能把 MARS 用起来还是得靠运气的,索性把代码走读了一下,刚好可以梳理梳理长连接的数据流。

上面数据流展示了 client 端要发送数据的整个过程和涉及到主要 API,以 Android API 为例,MARS 提供了涉及数据输出的以下重要 API:

// 初始化
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 提供的接口就比较复杂了,这边也放一张总结图感受一下。

  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 的客户端心跳,走 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:

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");
    }
};

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

  作者简介

付明旺,微医集团资深 C++ 架构师,负责研发跨平台底层通信组件及 windows APP,十多年从事移动通信网络协议栈开发和系统设计工作。

 
前端之巅 更多文章 阿里是如何让iOS 12越狱成功的? 多端统一开发框架 Taro 1.0 正式发布,全面支持小程序 15行代码让苹果设备崩溃,最新的iOS 12也无法幸免 选择JavaScript开源库时,你需要考虑这些问题 styled-components v4测试版发布:原生支持 ref,性能提升25%
猜您喜欢 perl模块推荐18-图形界面编程利器Tk 在英语流利说,我们这样管理数据采集需求 不仅仅要搞定技术,CTO更得懂点平衡之道 Openstack liberty源码分析之云主机的启动过程1 【技术帖】使用KyBot优化Apache Kylin存储