Android 架构之长连接技术

本文首发于小专栏《Android 架构之长连接技术 》,更多 Android 架构文章欢迎关注《 亿级 Android 架构

上一篇文章 《Android 架构之网络框架(上)》 中,我们谈过了网络框架 OkHttp、网络加速方案如 HttpDNS、数据压缩与序列化等技术点。本文我们结合 腾讯 Mars 框架 美团 Shark 体系 等业内主流长连接方案,谈一谈长连接技术的各个方面。

本文会包括下面的技术点:

  • 长连接与 Http 短连接、Keep-Alive 傻傻分不清
  • 你为什么需要长连接
  • 长连接何时会断开
  • 如何建立稳定长连接
  • Mars 智能心跳机制
  • 长连接数据协议及加密
  • 长连接通道建设及容灾

除了大家常用的 Http 短连接,大型 App 几乎都会搭建一套完整的 TCP 长连接 网络通道。我们先来看下 美团 Shark 长连接 的线上数据:

图片来源 《美团点评移动网络优化实践

上面两张图片对比了长 / 短连接的成功率和网络延时数据,这两个是网络模块最重要的衡量指标。可以看出,无论是成功率,还是网络延时,长连接都明显优于短连接。

另外,大家都知道微信的消息收发非常即时,这便归功于背后稳定高可用的长连接系统。实际上,微信除了消息收发,其他的小数据通信都是通过长连接来实现的。

下面我们来讲解一些长连接的一些核心技术点。

I. 长连接与 Http 短连接、Keep-Alive 傻傻分不清

为防止大家对于长连接和短连接混淆,这里先简单说明下几点区别。

长连接 vs Http 短连接

这两者分别对应的是 TCP 协议层长连接 短连接

大家都知道,TCP 会通过三次握手,建立与服务端的连接,然后传递数据,只不过 短连接 在数据传输完后,会主动关闭连接,而 长连接 会继续保持这条连接,后续的数据读写继续使用这条连接。

长连接 vs Http 的 Keep-Alive

上一篇文章中提到了 连接复用 ,通过 Http1.1 的Keep Alive 字段,我们可以让一条 Http 连接保持不被立即关闭。有些同学这时就疑惑了,是不是长连接就是 Keep Alive 呢?

其实不是的。长连接我们也叫 TCP 长连接,它是架设在 TCP 协议上的,而上面说的Keep Alive 是 Http 协议的内容,连协议都不同,两者自然不是一个东西。

开启了 Keep Alive 是 Http 连接,我们也称之为 持久连接,和长连接并不同。感兴趣可参考此文:《TCP 进阶》

TCP 的 Keep-Alive vs Http 的Keep-Alive

提到 Keep Alive,有些同学就会问了,TCP 协议里也有一个Keep Alive,它和 Http 协议里的Keep Alive 有什么区别吗?

二者的用处并不同。Http 协议在完成一个请求后,服务器会自动关闭连接。这时,可以在请求里带上一个 Keep Alive 给服务器,告诉 服务器不要立即关闭连接 ,我还想继续复用这条连接;而对 TCP 协议层而言,是不会自动断开的,但这也带来了一个问题,万一由于某些外部原因导致连接断开,那我如何知道连接已失效呢?TCP 会在 2 个小时间隔后,自动发送一个Keep Alive 数据包给服务端,探测一下服务器是否还在响应。它的功能类似心跳包,只是间隔太长,不适合做真正的心跳包。

II. 你为什么需要长连接

那么,相比 Http 短连接,长连接技术能带来什么好处呢?

1. 不同域名的请求可以复用同一个长连接通道

以前我们不同域名的请求,需要做对应的 DNS 请求,然后建立对应的 Http 连接。上篇文章里说的 Http 连接池 在不同域名下不可复用,需要重新建立连接。这些都是一些资源开销,但是如果通过长连接通道,那域名只是这个请求里的一个字段,可以直接复用同一条长连接通道。

2. 不依赖 DNS,无 DNS 耗时和劫持等问题

上文中我们提到了HttpDNS,虽然它比系统 DNS 更优,但终归还是要做 DNS 操作。而长连接都是 IP 直接连接,因此没有 DNS 相关的开销和耗时。

3. 如果有大量网络请求,可以明显减少网络延时,节省带宽

对于大型 App 而言,存在繁多密集的网络请求,这中间就会存在非常多次的 Http 断开和重新连接,浪费了很多时间和带宽。而通过长连接通道的话,则没有这部分耗时,直接传输二进制数据即可,节省了每次连接里 Header 之类的带宽开销。

4. 服务端主动 Push 数据到客户端

对于上面提到的微信消息接收等场景,如果需要客户端主动去轮询,则会频繁发起请求,对于服务器会产生很大的负载压力,浪费带宽流量。而通过长连接,服务端可以主动把消息下发给客户端,做到最高实时性,且节省流量。

III. 长连接何时会断开?

正常而言,长连接是不会断开的。大家可以自己试一试,两个 socket 建立连接,只要网络不变、一切正常,那么这两个 socket 可以一直互相传送数据,不会断开。

但是,在移动网络下,网络状态复杂多变,比如网络线路被切断、服务器宕机等,都会导致长连接中断。除了这些线路异常外,我们需要关注下面几个长连接断开原因:

1. 长连接所在进程被杀

这个很容易理解,如果我们的 App 切换到后台,那么系统随时可能将我们的 App 杀掉,这时长连接自然也就随之断开。

2. 用户切换网络

比如手机网络断开,或者发生 Wi-Fi 和蜂窝数据切换,这时会导致手机 IP 地址变更。而我们知道,TCP 连接是基于 IP + Port 的,一旦 IP 变更,TCP 连接自然也就失效了,或者说长连接也就相当于断开了。

3. 系统休眠等导致 NAT 超时

这里对 NAT 简单解释下,方便有的同学不太了解。当手机连接上网络时,网关会给我们分配一个 IP 地址,这个其实是内网 IP,此时还未真正连接上公网,也连接不上服务器;如果想要连接公网,需要运营商将我们的内网 IP 映射成一个公网 IP,有了公网 IP,服务器就能与我们建立连接了。NAT 指的就是这个映射过程。

也就是说,运营商会给每台设备分配一个公网 IP,类似一张通信证。不过,随着连接网络的设备不断增多,网关负载也会不断加大,这时,运营商就会对一些不太活跃的设备进行公网 IP 回收了,如果下次这个设备需要连网,那就重新分配一个 IP 即可。

看似没问题,但实际上,如果我们的 App 在一段时间不活跃,发生了 NAT 超时,便会导致我们的公网 IP 失效,长连接也随之失效了。

4. DHCP 租期

DHCP 租期过期,如果没有及时续约,同样会导致 IP 地址失效。

综合而言,长连接在正常情况下是不会断开的,但是,一旦手机的 IP 地址失效,这时就不得不重新建立连接了。

IV. 如何建立稳定长连接?

上面我们提到了多种长连接断开的原因,那我们应该如何进行优化,尽可能保证长连接不断开,或者及时断开了,也要尽快重连呢?

1. Mars 长连接独立进程

为了减少进程被杀的几率,在 Mars 的 Demo 代码 里我们可以看到,它将长连接逻辑单独提取到了一个独立的进程里。这个进程只做网络交互,消耗的内存等资源自然较少,从而减少了被系统回收的概率。

图片来自《Android 版微信后台保活实战分享(进程保活篇)》

2. 长连接进程复活

进程被杀难以避免,不过可以通过 AlarmReceiver、 ConnectReceiver、BootReceiver,达到进程的及时唤醒。

当然,进程保活是一个比较大的话题,而且不恰当的进程保活也会对系统体验造成危害。这里就不深究了。

3. 心跳机制

对于心跳包很多人误以为只是用来定期告诉服务端我们的状态,实际并非如此。

上面我们提到了 NAT 超时,即如果 App 一段时间内不活跃,会导致运营商那里删除我们的公网 IP 映射关系,这会导致我们的 TCP 长连接断开。因此,我们需要通过心跳机制来保证 App 的活跃度,防止发生 NAT 超时。

4. 断开重连

在线上运行时,长连接很有可能会由于网络切换之类的原因断开。这时,我们需要 尽快发现 长连接断开,并 立即重连。一般有下面几种做法:

  • 创建 Receiver,监控网络状态,如果网络发生切换则立即重连;
  • 监控服务端心跳包回包,如果连续 5 次没有收到回包,则认为长连接已经失效;
  • 设置心跳包超时限制,如果超过时间还没有收到心跳回包,则重连,这种方式比较耗电;
  • 等 socket IO 异常抛出,不过耗时太长,需要 15s 左右才能发现。

V. Mars 智能心跳机制

1. 固定心跳机制

上面我们说了,心跳机制主要是为了防止 NAT 超时,外网 IP 地址失效。因此,一般的做法就是在 NAT 失效前,保证有心跳包发出。或者说,客户端应当以略小于 NAT 超时时间的间隔来发送心跳包。

NAT 超时时间

早期的微信的心跳是 4.5 分钟发送一次心跳,可以不错的运行。

2. Mars 智能心跳策略

在尽量不影响用户收消息及时性的前提下,根据网络类型自适应的找出保活信令 TCP 连接的尽可能大的心跳间隔,从而达到减少安卓微信因心跳引起的空中信道资源消耗,减少心跳 Server 的负载,以及减少部分因心跳引起的耗电。

自适应心跳

因此,在固定心跳机制下,微信又研究了一套动态计算心跳的方案,动态的探测最大的 NAT 超时时间,然后选定合适的心跳间隔区间去发送心跳包。这里说一下大致思路:

首先,如果心跳间隔越久,产生的负载和消耗也会越小。因此微信采用了 自适应心跳:当找到一个有效心跳间隔后,我们主动去加大这个间隔,然后测试是否能成功,如果不能,则使用比上一次成功间隔稍短的时间作为间隔;否则继续加大间隔,直到找到可用的有效间隔。

那么,如何判断一个心跳间隔有效呢?微信采用的方案是使用固定短心跳直到满足三次连续短心跳成功,则认为这个间隔有效。

探测过程大致为:60 秒短心跳,连续发 3 次后开始探测,90,120,150,180,210,240,270

前后台策略

另外,考虑到 App 在前后台对于长连接的需求是不同的。因此当微信在前台活跃态时,采用了 固定心跳 机制;在前台熄屏态或者后台活跃态(进入后台 10 分钟内)时,先用几次最小心跳维持长连接,然后进入 自适应心跳 机制;在后台稳定态(超过 10 分钟),则采用自适应心跳计算出来的最大心跳作为固定值。

如果在运行过程中,发生了心跳失败,则进行重连。同时将心跳间隔调整为断线前间隔减去 20s,重新走自适应心跳;如果连续 5 次均失败,则以初始心跳 180s 继续测试。

Alarm 对齐策略

对于 Android 系统而言,为了减少频繁唤醒系统导致的电量损耗,提供了 Alarm 对齐唤醒 机制:把一定时间段内的多次 Alarm 唤醒合并成一次,减少系统被唤醒次数,增加待机时间。

而我们的心跳包就是需要在定时结束后自动触发一次心跳包的发送,因此,在 Mars 里面的心跳时间也是按照 Alarm 对齐时间来做心跳间隔,减少电量损耗。

其他

对于微信心跳策略感兴趣的话可以阅读文末的参考文献,代码可以参考smart_heartbeat

VI. 长连接数据协议及加密

长连接传递的是二进制数据,前后端可以自行协商每个字节要存放的内容即可。当然,也可以考虑采用一些通用协议:比如 SMTP、ProtoBuf 等序列化方案。

参考文章:《一个基于 TCP/WebSockets 的超级精简的长连接消息协议》.

另外,在数据加密方面,可以结合非对称加密算法 RSA 和对称加密算法 AES 来对数据进行加密传输。

这一点不是本文的重点,不做过多赘述。

VII. 长连接通道建设及容灾

上面讲了长连接的优势,那我们该如何搭建整个长连接通道呢?这里我们以美团的长连接通道为例子进行说明,各大厂的方案也是类似的。
美团长连通道
上面是一个简图,大体流程如下:

  1. 客户端与代理长连服务器建立长连接,代理服务器可全国多地部署,在建立长连时可以选择最近的服务器 IP 就近接入;
  2. 长连接建立好后,客户端对要发送的二进制数据进行加密并传输;
  3. 代理服务器收到后,可以通过内部专线或普通 Http 请求来访问业务服务器;
  4. 如果长连接出现问题导致不可用,为保障客户端运行,需要立即降级成普通 Http 短连或者 UDP 通道。

小结

本文结合了国内大厂如腾讯、美团等长连接框架,针对长连接这个技术点做了完整的介绍和剖析,如有不对或疑问,欢迎留言。


谢谢。
wingjay

参考:

《移动端 IM 实践:实现 Android 版微信的智能心跳机制》
《Android 端消息推送总结:实现原理、心跳保活、遇到的问题等》
《美团点评移动网络优化实践
《Android 版微信后台保活实战分享(网络保活篇)》
《移动 APP 网络优化概述》
《高效 保活长连接:手把手教你实现 自适应的心跳保活机制》
《一种 Android 端 IM 智能心跳算法的设计与实现探讨》
《HTTP 长连接说明》
《TCP 进阶》