MARS 作为优秀的跨平台网络层通信方案开源 1 年多了,github 上收获过万的 star,期间较为稳定更新并不频繁。基于内核 socket MARS 针对弱网络环境下的移动应用做了很多比较实用的优化,详细的优化点和原理在其开源项目的 wiki 里有很多文档说的比较清楚了 Mars wiki(https://github.com/Tencent/mars/wiki)。 本人刚好参与了多款具有 IM 功能的应用开发,底层网络通信集成了 MARS,该底层通讯模块已经稳定服务于 Android/Ios/windows 平台上多款产品。网上有关 MARS 使用的实践经验还比较少见,这里总结一下供大家参考。
MARS 支持长连接的同时也支持短链接,短链接主要映射成有限制的 http 连接。短连接不是 MARS 的长处,不在本文涉猎,后面提到的所有连接如无特指均为长连接。
读完文档就能把 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 提供的接口就比较复杂了,这边也放一张总结图感受一下。
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; // 统计上报所用,可忽略。
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
接口同样可以实现心跳。
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,十多年从事移动通信网络协议栈开发和系统设计工作。