前面的文章中介绍了通用的推送架构和技术,主要介绍了Web端,也讲了部分移动端App。这次则主要介绍移动端的技术原理,实现方法和编程实现。
一、技术原理
一旦服务器数据有更新或者服务器要下发通知给客户端只能等客户端连接的时候才能实现。这种方式使消息失去了实时性。
定义:推送功能最早是被用于Email中,用来提示我们新的信息。在移动互联网普及之后,手机终端成为了消息推送的主战场,例如生活服务类的优惠券推送,咨询类的新闻推送,电商类的购物推送等等,在业务用户触达上起到了至关重要的作用,那我们就来揭开一下推送这个隐藏在业务背景之下的技术实现。减少信息过载。
难点:
- 如何唯一确定一个设备和App也就是device token(一般是设备号加App包名);
- 在没有唯一公网IP时,无法通过直接创建连接进行通信;
- 如何保证效率,不浪费网络资源;
实现技术:
- Pull:客户端使用Pull(拉)的方式,就是隔一段时间就去服务器上获取一下信息,看是否有更新的信息出现。
- Push:服务器使用Push(推送)的方式,当服务器端有新信息了,则把最新的信息Push到客户端上。这样,客户端就能自动的接收到消息。
虽然Pull和Push两种方式都能实现获取服务器端更新信息的功能,但是明显来说Push方式比Pull方式更优越。因为Pull方式更费客户端的网络流量,带宽,更主要的是费电量,还需要我们的程序不停地去监测服务端的变化。
级别:
- 系统级
- 应用级
1.1 系统架构及模块
这是一个比较完整的推送业务架构图,分为三个部分:业务层、通道层和客户端常驻服务,一般来说客户端常驻服务和通道层维持一个长连接通道实现数据的双向传递,而业务层实现的是基于推送业务形态的展示,例如推送的定时任务推送,接口推送,以及消息类型定义等等。
1.1.1 通道层
推送后端接入:这一层是业务的接入层,一般来说是对内网开放,通常采取的是RPC的远程调用实现,效率更高。
存储:依赖进行消息数据统计,以及离线消息信息存储,待终端网络开启之后再实施推送。
消息分发:依据消息到达进行就近机房选择,消息体封装等。
推送前端接入:和客户端进行长连接维持。
1.1.2 客户端
鉴权及防伪服务:进行消息体格式校验,消息防伪造验证
状态适配服务:识别当前终端所处环境和状态,例如微信所做的状态适配服务区分为:活跃态、次活跃态、自适应计算态、后台稳定态以及空闲态等几种。选择不同的状态会传送给心跳服务采取不同的心跳时间间隔。
心跳服务:为了应对NAT断连、DHCP租期失效、连接探测,需要有一个心跳服务进行维持,而心跳服务的选择策略是长连接维持的一个重要优化点。
后端感知:主要是为了应对DNS劫持以及就近流量访问所出现的一个服务。
对一个推送的基本架构和业务模块有一个初步了解之后我们可以接下来谈一谈关于实现这个系统的几个关键技术难点:
心跳机制优化
前面我们已经讨论了长连接里面一个非常重要的优化点就是心跳机制优化。我们先看下现实场景下有什么问题会导致一定需要长连接维持及优化。长连接需要维持那么肯定是有一些原因会导致长连接失效,总结一下有如下几个场景:
NAT断连
因为 IPv4 的IP量有限,运营商分配给手机终端的IP是运营商内网的IP,手机要连接 Internet,就需要通过运营商的网关做一个网络地址转换(Network Address Translation,NAT)。简单的说运营商的网关需要维护一个外网 IP端口到内网 IP端口的对应关系,以确保内网的手机可以跟 Internet 的服务器通讯。大部分移动无线网络运营商都在链路一段时间没有数据通讯时,会淘汰 NAT 表中的对应项,造成链路中断。下表列出一些已测试过的网络的NAT超时时间(更多数据由于测试条件所限没有测到):
安卓DHCP的租期(lease time)问题
长连接心跳间隔必须要小于NAT超时时间(aging-time),如果超过aging-time不做心跳,TCP长连接链路就会中断,Server就无法发送Push给手机,只能等到客户端下次心跳失败后,重建连接才能取到消息。
目前测试发现安卓系统对DHCP的处理有Bug:DHCP租期到了不会主动续约并且会继续使用过期IP,详细描述见http://www.net.princeton.edu/android/android-stops-renewing-lease-keeps-using-IP-address-11236.html。这个问题导致的问题表象是,在超过租期的某个时间点(没有规律)会导致IP过期,老的TCP连接不能正常收发数据。并且系统没有网络变化事件,只有等应用判断主动建立新的TCP连接才引起安卓设备重新向DHCP Server申请IP租用。
流程优化
基于以上两种因素的考虑,长连接就很有可能会出现断开,那么具体选择多久进行一次心跳探测呢?首先可以肯定的一点不能太久,要不能连接早已被断开或者说消息接收不及时,那太频繁的进行心跳检测显然也有一个问题,那就是功耗会加大,如何平衡这个选择呢?通常做法就是依据状态选择智能选择心跳时隔,这里的状态分为两类:网络状态及应用所处场景状态(例如前面所描述的微信所区分的几类状态)。
1、 按场景状态选择心跳区间
2、 通过心跳增加步长(网络质量良好)和心跳减少步长(网络质量差)来逐渐逼近网络最优心跳区间
心跳区间选择流程如下:
消息重复接收问题优化
如果消息是单次发送及反馈,那么在网络条件不好的时候很容易出现消息重复接收问题,而如何解决消息重复接收呢?
可以通过消息序列标注解决法,具体的做法是发一批带***的消息下发,客户端定期反馈当前接收到的消息序列,如果序列和当前发送不匹配就从反馈的***开始重新发送(模仿TCP)。
消息协议选择
首先需要确认的是这个协议是一个基于4层之上的应用层协议,在4层还是采取的是TCP/IP协议。那么我们来看下这个应用层协议如何选择:
文本协议:XMPP(xml)、SIP(类http协议)
这些协议有如下共同特征,可读性强、开源组件多、协议较复杂、冗余、费流量。
二进制数据协议:protocol buffer(PB)、MQTT
这些协议的特征是:可读性差,流程可自定义(MQTT自定义限制较大)、协议简单,轻量、编解码速度快,可大量节约流量,测试可节约50%~70%的流量。
所以具体那种协议的选择需要看业务场景的使用而定。
DNS劫持及就近流量接入
这个就涉及我们刚才看到的客户端出现的后端感知服务的功能,一般来说可以通过IP来和后端服务建立长连接,一旦DNS劫持之后IP连接不通,那么就可以在服务端维护一个后端服务IP列表,在客户端通过hash散列的方式去随机挑选其中一个IP连接,如果发现不通更新这个IP列表信息,这个功能还可以实现流量就近接入,例如通过把所有IP测试一遍,选择连接效率最高的那个IP就行建立长连接。
鉴权防伪
当一个恶意APP伪造透传消息内容体来拉起业务行为并打开恶意网站就有可能导致用户信息泄露,那么这里就有涉及一个消息防伪问题,一般做法就是通过消息MD5加签同时进行可逆加密,到达客户端之后进行反向解密校验就可防止消息伪造的行为。
二、实现方法
1)轮询(Pull)方式:应用程序应当周期性的与服务器进行连接并查询是否有新的消息到达,你必须自己实现与服务器之间的通信,例如消息排队等。而且你还要考虑轮询的频率,如果太慢可能导致某些消息的延迟,如果太快,则会大量消耗网络带宽和电池。
个推研发了能够根据网络状况动态调整心跳间隔的自适应算法,以最小的网络代价实现最稳定的联网质量。目前个推SDK空载流量消耗每月仅有0.8M-1.2M,不会对用户的钱袋造成损失。
优点:实现简单。
缺点:
- 成本大,需要自己实现与服务器之间的通信,例如消息排队等;
- 到达率不确定,考虑轮询的频率:太低可能导致消息的延迟;太高,更费客户端的资源(CPU资源、网络流量、系统电量)和服务器资源(网络带宽)
2)SMS信令(Push)方式:在Android平台上,你可以通过拦截SMS消息并且解析消息内容来了解服务器的意图,并获取其显示内容进行处理。这是一个不错的想法。这个方案的好处是,可以实现完全的实时操作。但是问题是这个方案的成本相对比较高,我们需要向移动公司缴纳相应的费用。目前很难找到免费的短消息发送网关来实现这种方案。
优点:
- 可实现完全的实时操作
缺点:
- 成本高(主要是短信资费的支出)
3)长连接(Push)方式:这个方案可以解决由轮询带来的性能问题,但是还是会消耗手机的电池。IOS平台的推送服务之所以工作的很好,是因为每一台手机仅仅保持一个与服务器之间的连接,事实上C2DM也是这么工作的。不过刚才也讲了,这个方案存在着很多的不足之处,就是我们很难在手机上实现一个可靠的服务,该App或服务一定被系统kill,也就无法维持长连接,目前也无法与IOS平台的推送功能相比。需要心跳包维持连接。
除了维持连接的心跳,不会产生额外的流量,但服务端需要维持连接,当客户端数量庞大的时候,服务器的资源消耗也会很大。本文后面提到的几个框架,都借助Java的NIO特性,缓解了服务端的压力和资源消耗,但毕竟是有连接,在性能上还是无法跟传统的HTTP无连接服务端比较。
Android操作系统允许在低内存情况下杀死系统服务,所以我们的推送通知服务很有可能就被操作系统Kill掉了。 轮询(Pull)方式和SMS(Push)方式这两个方案也存在明显的不足。至于持久连接(Push)方案也有不足,不过我们可以通过良好的设计来弥补,以便于让该方案可以有效的工作。毕竟,我们要知道GMail,GTalk以及GoogleVoice都可以实现实时更新的。
2.1 Android
Android系统提供的GCM只能在Android2.2以上才能使用,3.0以下必须要安装Googleplay并登陆了Google账号才能支持。而国内发行的手机大多是阉割掉了google 服务的。因此,对于Android系统来说,各家app只能各显神通,开发自己的专用长连接通道了。然而这时候他们遇到了app的天敌:手机管家和安全卫士。前文说了,app想要及时收到服务器推送的消息,关键在于自己与服务器的长连接通道不被关闭,也就是自己的后台服务可以一直在后台运行,而管家和卫士的一键清理功能就是专治这种“毒瘤”的。道高一尺魔高一丈,app在与管家和斗士的长期斗争中,总结了一系列躲避被清理掉的方法,什么定时自启能力、什么相互唤醒、什么前台进程等等。
2.2 IOS
APNs中,ios开通了一条系统级别的长连接通道,通道的一端是手机的所有app,另一端是苹果的服务器。app的服务器如果有新的消息需要推送的话,先把消息发送到苹果的服务器上,再利用苹果的服务器通过长连接通道发送到用户手机,然后通知具体的app。这样就做到了即使手机安装了100个app,也只需要向一条通道里发送心跳。此时不需要每一个app建立一条长连接通道。
应用通过观察者模式向 ios 系统注册关注的消息,系统收到 APNs Server 消息后转发到相应的应用程序,整个过程很清晰,并且所有 APP 都共用同一个系统级的连接。
三、编程实现
实现的时候可以基于上述技术原理去编程实现;也可以利用现有的协议的去实现;还可以利用第三方平台去实现。
目前第三方推送服务厂商包括极光、个推、信鸽,友盟。云推送服务有百度云等。
3.1 C2DM云端推送功能PUSH
Google推出的是Android系统级别的消息推送服务(云端推送)。
在Android手机平台上,Google提供了C2DM(Cloudto Device Messaging)服务。Android C2DM是一个用来帮助开发者从服务器向Android应用程序发送数据的服务。该服务提供了一个简单的、轻量级的机制,允许服务器可以通知移动应用程序直接与服务器进行通信,以便于从服务器获取应用程序更新和用户数据。C2DM服务负责处理诸如消息排队等事务并向运行于目标设备上的应用程序分发这些消息。
下面是C2DM操作过程示例图:
优点:
1)C2DM提供了一个简单的、轻量级的机制,允许服务器可以通知移动应用程序直接与服务器进行通信,以便于从服务器获取应用程序更新和用户数据。
缺点:
1)C2DM内置于Android的2.2系统上,无法兼容老的1.6到2.1系统;
2)C2DM需要依赖于Google官方提供的C2DM服务器,由于国内的网络环境,这个服务经常不可用,如果想要很好的使用,我们的App Server必须也在国外,这个恐怕不是每个开发者都能够实现的;
3) 不像在iPhone中,他们把硬件系统集成在一块了。所以对于我们开发者来说,如果要在我们的应用程序中使用C2DM的推送功能,因为对于不同的这种硬件厂商平台,比如摩托罗拉、华为、中兴做一个手机,他们可能会把Google的这种服务去掉,尤其像在国内就很多这种,把Google这种原生的服务去掉。买了一些像什么山寨机或者是华为这种国产机,可能Google的服务就没有了。而像在国外出的那些可能会内置。
3.2 MQTT协议实现Android推送功能。
采用MQTT协议实现Android推送功能也是一种解决方案。MQTT是一个轻量级的消息发布/订阅协议,它是实现基于手机客户端的消息推送服务器的理想解决方案。一项异步消息传输协议。
wmqtt.jar 是IBM提供的MQTT协议的实现。我们可以从这里(https://github.com/tokudu/AndroidPushNotificationsDemo)下载该项目的实例代码,并且可以找到一个采用PHP书写的服务器端实现(https://github.com/tokudu/PhpMQTTClient)。
协议架构如下图所示:
3.3 RSMB实现推送功能
Really Small Message Broker (RSMB) ,他是一个简单的MQTT代理,同样由IBM提供,其查看地址是:http://www.alphaworks.ibm.com/tech/rsmb。缺省打开1883端口,应用程序当中,它负责接收来自服务器的消息并将其转发给指定的移动设备。
SAM是一个针对MQTT写的PHP库。我们可以从这个http://pecl.php.net/package/sam/download/0.2.0地址下载它.
send_mqtt.php是一个通过POST接收消息并且通过SAM将消息发送给RSMB的PHP脚本。
3.4 XMPP协议实现Android推送功能
这是我希望在项目中采用的方案,因为目前它是开源的,对于其简单的推送功能它还是能够实现的。我们可以修改其源代码来适应我们的应用程序。
- eXtensible Messageing and Presence Protocol,可扩展消息与存在协议,是基于可扩展标记语言(XML)的协议,是目前主流的四种IM协议之一
其他三种:
即时信息和空间协议(IMPP)
空间和即时信息协议(PRIM)
即时通讯和空间平衡扩充的进程开始协议SIP(SIMPLE)
事实上Google官方的C2DM服务器底层也是采用XMPP协议进行的封装。XMPP(可扩展通讯和表示协议)是基于可扩展标记语言(XML)的协议,它用于即时消息(IM)以及在线探测。这个协议可能最终允许因特网用户向因特网上的其他任何人发送即时消息。
-
XMPP中定义了三个角色,分别是客户端、服务器和网关
客户端- 通过 TCP/IP与XMPP 服务器连接,然后在之上传输与即时通讯相关的指令(XML);
- 解析组织好的 XML 信息包;
- 理解消息数据类型。
- XMPP的核心:XML流传输协议(在网络上分片断发送XML的流协议),也是即时通讯指令的传递基础,即XMPP用TCP传的是XML流
- 与即时通讯相关的指令,在以前要么用2进制的形式发送(比如QQ),要么用纯文本指令加空格加参数加换行符的方式发送(比如MSN)。
- XMPP传输的即时通讯指令的逻辑与以往相仿,只是协议的形式变成了XML格式的纯文本。
服务器
- 监听客户端连接,并直接与客户端应用程序通信(客户端信息记录)
- 与其他 XMPP 服务器通信;
网关:与异构即时通信系统进行通信
异构系统包括SMS(短信),MSN,ICQ等
通信能够在这三者的任意两个之间双向发生。
原理流程
-
优点
- 开源:可通过修改其源代码来适应我们的应用程序。
- 简单:XML易于解析和阅读;将复杂性从客户端转移到了服务器端
- 可拓展性强:继承了在XML环境中灵活的发展性,可进一步对协议进行扩展,实现更为完善的功能。
GTalk、QQ、IM等都用这个协议
-
缺点
如果将消息从服务器上推送出去,则不管消息是否成功到达客户端手机上。 -
源码实例:有一个很棒的基于XMPP协议的java开源Android push notification:Androidpn。
androidpn是一个基于XMPP协议的java开源Android push notification实现。它包含了完整的客户端和服务器端。经过源代码研究我发现,该服务器端基本是在另外一个开源工程openfire基础上修改实现的。
这是androidpn的项目主页:http://sourceforge.net/projects/androidpn/
androidpn实现意图如下图所示:
androidpn 客户端需要用到一个基于java的开源XMPP协议包asmack,这个包同样也是基于openfire下的另外一个开源项目smack,不过我们不需要自己编译,可以直接把androidpn客户端里面的asmack.jar拿来使用。客户端利用asmack中提供的XMPPConnection类与服务器建立持久连接,并通过该连接进行用户注册和登录认证,同样也是通过这条连接,接收服务器发送的通知。
androidpn服务器端也是java语言实现的,基于openfire开源工程,不过它的Web部分采用的是spring框架,这一点与 openfire是不同的。Androidpn服务器包含两个部分,一个是侦听在5222端口上的XMPP服务,负责与客户端的 XMPPConnection类进行通信,作用是用户注册和身份认证,并发送推送通知消息。另外一部分是Web服务器,采用一个轻量级的HTTP服务器, 负责接收用户的Web请求。服务器架构如下:
最上层包含四个组成部分,分别是SessionManager,Auth Manager,PresenceManager以及Notification Manager。SessionManager负责管理客户端与服务器之间的会话,Auth Manager负责客户端用户认证管理,Presence Manager负责管理客户端用户的登录状态,NotificationManager负责实现服务器向客户端推送消息功能。
这个解决方案的最大优势就是简单,我们不需要象C2DM那样依赖操作系统版本,也不会担心某一天Google服务器不可用。利用XMPP协议我们还可以进一步的对协议进行扩展,实现更为完善的功能。 采用这个方案,我们目前只能发送文字消息,不过对于推送来说一般足够了,因为我们不能指望通过推送得到所有的数据,一般情况下,利用推送只是告诉手机端服务器发生了某些改变,当客户端收到通知以后,应该主动到服务器获取最新的数据,这样才是推送服务的完整实现。 XMPP协议相对来说还是比较简单的,值得我们进一步研究。
但是在经过一段时间的测试,我发现关于androidpn也存在一些不足之处:
- 比如时间过长时,就再也收不到推送的信息了。
- 性能上也不够稳定。
- 如果将消息从服务器上推送出去,就不再管理了,不管消息是否成功到达客户端手机上。
- XMPP协议繁杂,对于推送来说大材小用,适用于IM。
等等,总之,androidpn也有很多的缺点。如果我们要使用androidpn,则还需要做大量的工作。
3.5 使用第三方平台
推送主要考虑的是消息触达效果。
国内第三方推送平台,如极光、百度、腾讯等推送都做的很成熟了,而且只需要简单的集成推送平台的SDK就快速的实现推送功能,而且有些服务也是免费的。官网上都有详细的集成指南。第三方平台有商用的也有免费的,我们可以根据实现情况使用。
关于MQTT的方案,国内最近出现基于 mqtt 的第三方解决方案。云巴 (http://yunba.io),据了解是原 极光推送 CTO 创办的,有兴趣可以研究下。
关于国外的第三方平台:http://www.push-notification.org/。有兴趣的朋友可以查阅相关信息。使用第三方平台就需要使用别人的服务器,主要问题是安全问题,收费问题、保密问题、服务质量问题、扩展问题等等。
- 手机厂商类:小米推送、华为推送。系统级别。
- 第三方平台类:友盟推送、极光推送、云巴(基于MQTT)。“保活”和“互拉”。
- BAT大厂的平台推送:阿里云移动推送、腾讯信鸽推送、百度云推送
参考:https://www.jianshu.com/p/d77eaca4e52a
3.6 自己搭建一个推送平台。
这不是一件轻松的工作,当然可以根据各自的需要采取合适的方案。
好了,以上是关于在Android中实现推送方式的基础知识及相关解决方案。
四、总结
(1)app和后台的连接方式主要有两种。
一种叫pull,也叫轮询,就是定期的不断向后台请求,缺点是耗电,浪费流量和带宽。
另一种叫push,app和后台一直维持了一条通信通道,两端不定期的进行通信。缺点是要维持一条长连接通道,这条通道容易被其他程序杀死,要多想复活办法。可以是系统级或应用级的。
其他的就是一些特殊方法了,比如通过SMS,Email等方法。
(2)推送方法总结
个人建议使用第三方平台推送,一般免费,成本低+抵达率高。
(3) 端内推送和端外推送
通常大厂的App都会区分端内推送和端外推送(端指的是客户端),具体说来:
-
当App在前台运行的时候,这时的推送称为端内推送。端内推送一般是走App自己实现的一套推送系统:推送服务器是自己的,客户端维护一条长连接连到自己的推送服务器,不依赖任何第三方的推送系统。
-
当App从前台退到后台,在短时间内App未被杀死前,App自己的长连接仍然有效。这时的推送可以仍然走App自己的推送系统。所谓的“Android进程保活”,就是为了尽量延长这段在后台存活的时间。
-
当App在后台运行足够长的时间后,App进程由于被清理或者其它原因,App自己的长连接断开。这时的推送就称为端外推送了,只能走某个第三方推送平台了。
大厂的App的推送策略可以概括为:优先使用自己的推送,实在不行再走第三方推送平台。因为自己的推送系统更快、更有保障:
-
更快,是因为你交给第三方推送平台的推送消息要跟很多其它家App的消息一起排队。如果某家App突然在短时间内发送大量推送消息给推送平台(推广活动,或者程序bug),那么这个推送平台上的其它App就有可能受到牵连,推送延迟变得很大。这样的情况是很可能会发生的。比如,在某个推送平台的技术交流群里,不定期地就会看到有人在喊:“是不是推送又堵了啊......”
-
更有保障。大厂通常有专门的队伍维护推送相关的服务,有问题可以快速推进优化。
(4)消息类型
- 通知栏消息,在被送达用户的设备后,直接以系统通知的形式展示给用户。它不会继续被传递到App。
- 透传消息,在被送达用户的设备后,还会继续路由到App,通过回调App的某个BroadcastReceiver的形式将消息传递到App内部。然后由App决定如何处理和显示这个消息。透传消息在整个消息传递过程中比通知栏消息多了一步,因此就增加一些被系统限制的概率。所以说,通知栏消息比透传消息应该能提供更好的送达率。
(5) 腾讯 Mars 框架 和美团 Shark 体系 等业内主流长连接方案。
(6)开源框架
Android手机应用,信息推送的资料大多都是关于androidpn的,这是一个基于XMPP协议的Java开源信息推送方案,包括完整的服务端和客户端。服务端有Tomcat和Jetty两个版本,下载下来后配置一下数据库连接参数和IP端口就可以跑起来了。客户端则可以参考它的示例,把Android代码拿过来用即可。所以这是一个针对android应用的高度定制版,如果要对服务端进行修改调整,动的地方比较多。
Openfire则适用的范围更广,是基于XMPP协议的开源实时协作服务器,可以通过它简单地搭建一个IM平台。Androidpn是在Openfire上做了简化,只针对Android的消息推送。从这里也可看出,就消息推送来说,用XMPP协议有点杀鸡用牛刀。
MINA(封装了NIO异步长连接)和Netty是Socket框架,是同一个作者的,架构差别不大。MINA归Apache管,Openfire和Androidpn都是用的MINA;Netty则归JBOSS管,从我检索到的资料来看,更多偏向于Netty,大致是认为Netty的性能稍优,文档与例子更完整。
Netty是一个异步的,事件驱动的网络编程框架和工具,是一个基于NIO的客户,服务器端编程框架,使用Netty可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户,服务端应用。Netty相当简化和流线化了网络应用的编程开发过程,例如,TCP和UDP的socket服务开发。
(7)消息推送服务(GCM C2DM 百度云推送)
谷歌的GCM,Google Cloud Messaging,是C2DM的升级版,Android终端用的话最合适。百度和其他的一些公司,也提供云推送服务,好像没有免费的。谷歌应该比较郁闷吧,Android上的搜索、地图、邮件、推送等等,在国内都是为他人作嫁衣裳。
(8)websocket是h5的内容,不太适合于App。类似 Socket 的 TCP 长连接的通讯模式,是一种新的应用层协议,websocket协议是为了提供web应用程序和服务端全双工通信而专门制定的一种应用层协议。但不能够脱离http而单独存在,需要通过http协议建立websocket握手。通常它表示为:ws://echo.websocket.org/?encoding=text HTTP/1.1
(9)如何选择pull和push需要根据实际情况进行选择,譬如有这样一个app,实时性要求不高,每天只要能获取10次最新数据就能满足要求了,这种情况显然轮询更适合一些,推送显得太浪费,而且更耗电。
github上的的讨论帖:https://github.com/android-cn/topics/issues/4
五、细说TCP长连接与心跳
什么是长连接? 定时发送心跳, 这和轮询有什么区别? 心跳是干什么的? 同样是定期和服务器沟通,为什么长连接就比轮询更加优秀? 手机休眠了TCP连接不会断掉吗?
什么是长连接
先说短连接, 短连接是通讯双方有数据交互时就建立一个连接, 数据发送完成后,则断开此连接.
<>
Persistent connection:在HTTP中叫做持久连接(keep alive)。长连接就是大家建立连接之后,不主动断开。双方互相发送数据,发完了也不主动断开连接,之后有需要发送的数据就继续通过这个连接发送。
TCP连接在默认的情况下就是所谓的长连接, 也就是说连接双方都不主动关闭连接,这个连接就应该一直存在。TCP连接一旦建立可以多次相互发送数据。
但是网络中的情况是复杂的,这个连接可能会被切断。比如客户端到服务器的链路因为故障断了,或者服务器宕机了,或者是你家网线被人剪了,这些都是一些莫名其妙的导致连接被切断的因素, 还有几种比较特殊的:
NAT超时
因为IPv4地址不足,或者我们想通过无线路由器上网,我们的设备可能会处在一个NAT设备的后面,生活中最常见的NAT设备是家用路由器。NAT设备会在IP封包通过设备时修改源/目的IP地址。对于家用路由器来说,使用的是网络地址端口转换(NAPT),它不仅改IP,还修改TCP和UDP协议的端口号(NAT网络地址转换只修改IP,但一般总称为NAT技术), 这样就能让内网中的设备共用同一个外网IP。举个例子, NAPT维护一个类似下表的NAT表:通过将不同连接关联到一个公网IP的不同端口上,从而与其他主机通信。一般防火墙路由器都具有NAT功能。
内网地址 | 外网地址 |
---|---|
192.168.0.2:5566 | 120.132.92.21:9200 |
192.168.0.3:7788 | 120.132.92.21:9201 |
192.168.0.3:8888 | 120.132.92.21:9202 |
NAT设备会根据NAT表对出去和进来的数据做修改, 比如将192.168.0.3:8888
发出去的封包改成120.132.92.21:9202
, 外部就认为他们是在和120.132.92.21:9202
通信. 同时NAT设备会将120.132.92.21:9202
收到的封包的IP和端口改成192.168.0.3:8888
, 再发给内网的主机, 这样内部和外部就能双向通信了, 但如果其中192.168.0.3:8888 == 120.132.92.21:9202
这一映射因为某些原因被NAT设备淘汰了, 那么外部设备就无法直接与192.168.0.3:8888
通信了。
我们的设备经常是处在NAT设备的后面, 比如在大学里的校园网,查一下自己分配到的IP, 其实是内网IP,表明我们在NAT设备后面,如果我们在寝室再接个路由器, 那么我们发出的数据包会多经过一次NAT。
国内移动无线网络运营商在链路上一段时间内没有数据通讯后,会淘汰NAT表中的对应项,造成链路中断。
网络状态切换
手机网络和WIFI网络切换, 网络断开和连上等情况, 也会使长连接断开. 这里原因可能比较多, 但结果无非就是IP变了, 或者被系统通知连接断了.
DHCP的租期
目前测试发现安卓系统对DHCP的处理有Bug, DHCP租期到了不会主动续约并且会继续使用过期IP, 这个问题会造成TCP长连接偶然的断连。
IDS/FW等安全设备的控制
为了安全考虑,阻断超时网络连接。
服务器客户端本身的功能控制
为了省电或休眠等断开主动切断网络连接。
心跳包的作用
TCP长连接本质上不需要心跳包来维持, 大家可以试一试, 让两台电脑连上同一个wifi, 然后让其中一台做服务器, 另一台用一个普通的没有设置KeepAlive的Socket连上服务器, 只要两台电脑别断网, 路由器也别断电,DHCP正常续租,就这么放着,过几个小时再用其中一台电脑通过之前建立的TCP连接给另一台发消息,另一台肯定能收到。
那为什么要有心跳包呢? 其实主要是为了防止上面提到的NAT超时,既然一些NAT设备判断是否淘汰NAT映射的依据是一定时间没有数据, 那么客户端就主动发一个数据。
当然, 如果仅仅是为了防止NAT超时, 可以让服务器来发送心跳包给客户端,不过这样做有个弊病就是,万一连接断了,服务器就再也联系不上客户端了。所以心跳包必须由客户端发送,客户端发现连接断了,还可以尝试重连服务器。
所以心跳包的主要作用是防止NAT超时, 其次是探测连接是否断开。
链路断开, 没有写操作的TCP连接是感知不到的, 除非这个时候发送数据给服务器, 造成写超时, 否则TCP连接不会知道断开了. 主动kill掉一方的进程, 另一方会关闭TCP连接, 是系统代进程给服务器发的FIN。TCP连接就是这样, 只有明确的收到对方发来的关闭连接的消息(收到RST也会关闭), 或者自己意识到发生了写超时, 否则它认为连接还存在.
心跳包的时间间隔
既然心跳包的主要作用是防止NAT超时, 那么这个间隔就大有文章了.
发送心跳包势必要先唤醒设备, 然后才能发送, 如果唤醒设备过于频繁, 或者直接导致设备无法休眠, 会大量消耗电量, 而且移动网络下进行网络通信, 比在wifi下耗电得多. 所以这个心跳包的时间间隔应该尽量的长, 最理想的情况就是根本没有NAT超时, 比如刚才我说的两台在同一个wifi下的电脑, 完全不需要心跳包. 这也就是网上常说的长连接, 慢心跳.
现实是残酷的, 根据网上的一些说法,中移动2/3G下,NAT超时时间为5分钟,中国电信3G则大于28分钟,理想的情况下,客户端应当以略小于NAT超时时间的间隔来发送心跳包。
wifi下, NAT超时时间都会比较长,据说宽带的网关一般没有空闲释放机制,GCM有些时候在wifi下的心跳比在移动网络下的心跳要快, 可能是因为wifi下联网通信耗费的电量比移动网络下小。
关于如何让心跳间隔逼近NAT超时的间隔, 同时自动适应NAT超时间隔的变化, 可以参看Android微信智能心跳方案。
心跳包和轮询的区别
心跳包和轮询看起来类似, 都是客户端主动联系服务器, 但是区别很大。
- 轮询是为了获取数据, 而心跳是为了保活TCP连接。
- 轮询得越频繁, 获取数据就越及时, 心跳的频繁与否和数据是否及时没有直接关系。
- 轮询比心跳能耗更高,因为一次轮询需要经过TCP三次握手, 四次挥手, 单次心跳不需要建立和拆除TCP连接。轮询是单个连接,心跳是同一个连接。
TCP唤醒Android
大家有没有想过, 手机的短信功能和微信的功能差不多, 为什么微信会比短信耗电这么多? 当然不是因为短信一条0.1元. 手机短信是通过什么获取推送的呢?
首先Android手机有两个处理器, 一个叫Application Processor(AP), 一个叫Baseband Processor(BP). AP是ARM架构的处理器,用于运行Android系统; BP用于运行实时操作系统(RTOS), 通讯协议栈运行于BP的RTOS之上. 非通话时间, BP的能耗基本上在5mA左右,而AP只要处于非休眠状态, 能耗至少在50mA以上, 执行图形运算时会更高. 另外LCD工作时功耗在100mA左右, WIFI也在100mA左右. 一般手机待机时, AP, LCD, WIFI均进入休眠状态, 这时Android中应用程序的代码也会停止执行.
Android为了确保应用程序中关键代码的正确执行, 提供了Wake Lock的API, 使得应用程序有权限通过代码阻止AP进入休眠状态. 但如果不领会Android设计者的意图而滥用Wake Lock API, 为了自身程序在后台的正常工作而长时间阻止AP进入休眠状态, 就会成为待机电池杀手.
完全没必要担心AP休眠会导致收不到消息推送. 通讯协议栈运行于BP,一旦收到数据包, BP会将AP唤醒, 唤醒的时间足够AP执行代码完成对收到的数据包的处理过程. 其它的如Connectivity事件触发时AP同样会被唤醒. 那么唯一的问题就是程序如何执行向服务器发送心跳包的逻辑. 你显然不能靠AP来做心跳计时. Android提供的Alarm Manager就是来解决这个问题的. Alarm应该是BP计时(或其它某个带石英钟的芯片,不太确定,但绝对不是AP), 触发时唤醒AP执行程序代码. 那么Wake Lock API有啥用呢? 比如心跳包从请求到应答, 比如断线重连重新登陆这些关键逻辑的执行过程, 就需要Wake Lock来保护. 而一旦一个关键逻辑执行成功, 就应该立即释放掉Wake Lock了. 两次心跳请求间隔5到10分钟, 基本不会怎么耗电. 除非网络不稳定. 频繁断线重连, 那种情况办法不多.
上面所说的通信协议, 我猜应该是无线资源控制协议(Radio Resource Control), RRC应该工作在OSI参考模型中的第三层网络层, 而TCP, UDP工作在第四层传输层, 上文说的BP, 应该就是手机中的基带, 也有叫Radio的。
移动网络下, 每一个TCP连接底层都应该是有RRC连接, 而RRC连接会唤醒基带, 基带会唤醒CPU处理TCP数据, 这是我个人的理解。
上面说了这么多, 其实意思就是TCP数据包能唤醒手机. 至于UDP, 我不确定.
而推送中最重要的部分就是让手机尽量休眠, 只有在服务器需要它处理数据时才唤醒它, 这正好符合我们的要求.
移动网络下的耗电
Google在Optimizing Downloads for Efficient Network Access中提到了一个叫Radio State Machine的东西.
mobile radio state machine
说的应该就是基带的工作状态, 在Radio Standby下几乎不耗电, 但是一旦有需要处理的事情, 比如手机里某个app要访问网络(从上一节可以推测: 收到RRC指令也会导致唤醒), 就会进入到Radio Full Power中, 由Standby转为Full Power这一唤醒过程很耗电, Full Power下基带空闲后5s进入Radio Low Power, 如果又空闲12s才进入Standby. 主要的意思就是不要频繁的唤醒基带去请求网络, 因为只要一唤醒, 就至少会让基带在Full Power下工作5s, 在Low Power下工作12s, 而且唤醒过程很耗电. 所以在移动网络下, 心跳需要尽量的慢才好, 不过以当前这种情况, 想慢下来几乎不可能.
不过这也带来另外一个问题, 假如手机里有10个应用, 每个应用都发送心跳包, 每个应用的服务器都可能唤醒手机, 那手机还休不休眠了?
实际实现遇到的问题
了解完了我就开始动手做demo, 服务器使用Apache的Mina, 客户端也用这个
Mina
一个是Android端发一个汉字给服务器, 服务器filter崩溃, 发超过一个汉字, 客户端filter崩溃, 写个IoFilter做一下编解码就好了. 另外User Guide里面的代码也有错误. 第二个是IoSessionConfig的写超时设置了完全不起作用.
小米手机的神奇Socket
后来又发现客户端只要在后台超过一定时间, 对socket的写操作就会变得非常诡异, 表现为socket把数据吞了, 告知应用数据已经被对方接收, 但是服务器什么都没收到, 而且服务器发送的消息客户端也收不到. 只要让app进到前台, 之前消失的数据会一股脑发给服务器, 客户端会收到服务器重传的消息.
我开始还以为是Android的休眠机制把wifi断了, 我把各种WifiLock
, WakeLock
都持有了, 还是出这种情况. 后来无意间发现小米针对每个app都有个后台运行时允许联网
的开关, 我把它打开了, 果然好了一阵子, 后来又开始重复之前的情况, 我还以为是Mina的IO线程被kill了还是怎么, 用DDMS看了线程信息没问题. 不放心, 又用纯Socket实现了客户端, 还是有问题, 再在之前的基础上加上1分钟的心跳, 还是有问题.
小米手机的神奇bug
这次真是我运气好, 我又看了一眼后台运行时允许联网
的开关, 发现demo app的这个开关刚刚还被我打开了, 这下又关上了, 我怀疑是小米的这个功能有bug, 我是记得有小米员工提到这东西有服务器下发白名单的, 我认为是服务器下发数据把我的改动给覆盖了, 我把几个app的后台联网关了, 重启手机之后, 他们又开了.
最后我改了个10s的心跳间隔, 在心跳的时候, 把后台允许联网关掉, 复现了那个神奇的socket行为, 大概确定是MIUI的bug.
睡了一觉起来, MIUI的工程师联系了我, 确认是bug. 顺便提醒一下用小米做测试机的开发者和用户, 这个bug的临时解决方案是: 用神隐模式里的自定义配置, 把自己想改的设置好就行.
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 超时
当手机连接上网络时,网关会给我们分配一个 IP 地址,这个其实是内网 IP,此时还未真正连接上公网,也连接不上服务器;如果想要连接公网,需要运营商将我们的内网 IP 映射成一个公网 IP,有了公网 IP,服务器就能与我们建立连接了。NAT 指的就是这个映射过程。
也就是说,运营商会给每台设备分配一个公网 IP,类似一张通信证。不过,随着连接网络的设备不断增多,网关负载也会不断加大,这时,运营商就会对一些不太活跃的设备进行公网 IP 回收了,如果下次这个设备需要连网,那就重新分配一个 IP 即可。
看似没问题,但实际上,如果我们的 App 在一段时间不活跃,发生了 NAT 超时,便会导致我们的公网 IP 失效,长连接也随之失效了。
4. DHCP 租期
DHCP 租期过期,如果没有及时续约,同样会导致 IP 地址失效。
综合而言,长连接在正常情况下是不会断开的,但是,一旦手机的 IP 地址失效,这时就不得不重新建立连接了。
IV. 如何建立稳定长连接?
上面我们提到了多种长连接断开的原因,那我们应该如何进行优化,尽可能保证长连接不断开,或者及时断开了,也要尽快重连呢?
1. Mars 长连接独立进程
为了减少进程被杀的几率,在 Mars 的 Demo 代码 里我们可以看到,它将长连接逻辑单独提取到了一个独立的进程里。这个进程只做网络交互,消耗的内存等资源自然较少,从而减少了被系统回收的概率。
2. 长连接进程复活
进程被杀难以避免,不过可以通过 AlarmReceiver、 ConnectReceiver、BootReceiver达到进程的及时唤醒。
当然,进程保活是一个比较大的话题,而且不恰当的进程保活也会对系统体验造成危害。这里就不深究了。
3. 心跳机制
对于心跳包很多人误以为只是用来定期告诉服务端我们的状态,实际并非如此。
上面我们提到了 NAT 超时,即如果 App 一段时间内不活跃,会导致运营商那里删除我们的公网 IP 映射关系,这会导致我们的 TCP 长连接断开。因此,我们需要通过心跳机制来保证 App 的活跃度,防止发生 NAT 超时。
4. 断开重连
在线上运行时,长连接很有可能会由于网络切换之类的原因断开。这时,我们需要 尽快发现
长连接断开,并 立即重连
。一般有下面几种做法:
- 监控服务端心跳包回包,如果连续 5 次没有收到回包,则认为长连接已经失效;
- 设置心跳包超时限制,如果超过时间还没有收到心跳回包,则重连,这种方式比较耗电;
- 创建 Receiver,监控网络状态,如果网络发生切换则立即重连;
- 等 socket IO 异常抛出,不过耗时太长,需要 15s 左右才能发现。
V. Mars 智能心跳机制
1. 固定心跳机制
上面我们说了,心跳机制主要是为了防止 NAT 超时,外网 IP 地址失效。因此,一般的做法就是在 NAT 失效前,保证有心跳包发出。或者说,客户端应当以略小于 NAT 超时时间的间隔来发送心跳包。
包的内容,是没有什么特别规定的,不过一般都是很小的包,或者只包含包头的一个空包。 在TCP机制里面,本身是存在有心跳包机制的,也就是TCP选项:SO_KEEPALIVE. 系统默认是设置的2小时的心跳频率。
心跳机制是定时发送一个自定义的结构体(心跳包),让对方知道自己还活着,以确保连接的有效性的机制。
早期的微信的心跳是 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. 长连接通道建设及容灾
上面讲了长连接的优势,那我们该如何搭建整个长连接通道呢?这里我们以美团的长连接通道为例子进行说明,各大厂的方案也是类似的。
上面是一个简图,大体流程如下:
- 客户端与代理长连服务器建立长连接,代理服务器可全国多地部署,在建立长连时可以选择最近的服务器 IP 就近接入;
- 长连接建立好后,客户端对要发送的二进制数据进行加密并传输;
- 代理服务器收到后,可以通过内部专线或普通 Http 请求来访问业务服务器;
- 如果长连接出现问题导致不可用,为保障客户端运行,需要立即降级成普通 Http 短连或者 UDP 通道。