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

welldum 2019-06-28

Mars简介

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

Mars使用实践

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

长连接数据流及API一览

读完文档就能把MARS用起来还是得靠运气的,索性把代码走读了一下,刚好可以梳理梳理长连接的数据流。
基于MARS的移动APP网络通信开发实践

上面数据流展示了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提供的接口就比较复杂了,这边也放一张总结图感受一下。
基于MARS的移动APP网络通信开发实践

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 IPMARS解析出的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。实际线上产品使用推荐

  • 每个进程一个日志文件,每个进程需要单独配置日志
  • 使用异步日志打印
  • 定义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

相关推荐