基于MARS的移动APP网络通信开发实践
Mars简介
MARS作为优秀的跨平台网络层通信方案开源1年多了,github上收获过万的star,期间较为稳定更新并不频繁。基于内核socket MARS针对弱网络环境下的移动应用做了很多比较实用的优化,详细的优化点和原理在其开源项目的wiki里有很多文档说的比较清楚了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的客户端心跳,走 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。实际线上产品使用推荐
- 每个进程一个日志文件,每个进程需要单独配置日志
- 使用异步日志打印
- 定义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