极光推送技术原理:无线网络长连接
因为手机平台本身、电量、网络流量的限制,移动互联网应用在设计上跟传统 PC 上的应用很大不一样,需要根据手机本身的特点,尽量的节省电量和流量,同时又要尽可能的保证数据能及时到达客户端。
为了解决数据同步的问题,在手机平台上,常用的方法有2种。一种是定时去服务器上查询数据,也叫Polling,还有一种手机跟服务器之间维护一个 TCP 长连接,当服务器有数据时,实时推送到客户端,也就是我们说的 Push。
从耗费的电量、流量和数据送达的及时性来说,Push 都会有明显的优势,但 Push 的实现和维护成本相对较高。在移动无线网络下维护长连接,相对也有一些技术上的难度。本文试图给大家介绍一下我们极光推送在 Android 平台上是如何维护长连接。
因为 IP v4 的 IP 量有限,运营商分配给手机终端的 IP 是运营商内网的 IP,手机要连接 Internet,就需要通过运营商的网关做一个网络地址转换(Network Address Translation,NAT)。简单的说运营商的网关需要维护一个外网 IP、端口到内网 IP、端口的对应关系,以确保内网的手机可以跟 Internet 的服务器通讯。
图片源自 cisco.com.
NAT 功能由图中的 GGSN 模块实现。
大部分移动无线网络运营商都在链路一段时间没有数据通讯时,会淘汰 NAT 表中的对应项,造成链路中断。
为了不让 NAT 表失效,我们需要定时的发心跳,以刷新 NAT 表项,避免被淘汰。
Android 上定时运行任务常用的方法有2种,一种方法用 Timer,另一种是AlarmManager。
Android 的 Timer 类可以用来计划需要循环执行的任务,Timer 的问题是它需要用 WakeLock 让 CPU 保持唤醒状态,这样会大量消耗手机电量,大大减短手机待机时间。这种方式不能满足我们的需求。
AlarmManager 是 Android 系统封装的用于管理 RTC 的模块,RTC (Real Time Clock) 是一个独立的硬件时钟,可以在 CPU 休眠时正常运行,在预设的时间到达时,通过中断唤醒 CPU。
这意味着,如果我们用 AlarmManager 来定时执行任务,CPU 可以正常的休眠,只有在需要运行任务时醒来一段很短的时间。极光推送的 Android SDK 就是基于这种技术实现的。
当有大量的手机终端需要与服务器维持长连接时,对服务器的设计会是一个很大的挑战。
假设一台服务器维护10万个长连接,当有1000万用户量时,需要有多达100台的服务器来维护这些用户的长连接,这里还不算用于做备份的服务器,这将会是一个巨大的成本问题。那就需要我们尽可能提高单台服务器接入用户的量,也就是业界已经讨论很久了的 C10K 问题。
针对这个问题,我们专门成立了一个项目,命名为C2000K,顾名思义,我们的目标是单机维持200万个长连接。最终我们采用了多消息循环、异步非阻塞的模型,在一台双核、24G内存的服务器上,实现峰值维持超过300万个长连接。
http://docs.jpush.cn/pages/viewpage.action?pageId=3309821
稳定维护长连接是推送平台的一个基础,极光推送团队将会在这方面长期投入,以保证用户能有效的节省电量、流量,同时数据能实时送达。
以上是极光推送官方的文章,但是看了之后不免有几个疑问。
1)他们号称最高峰值可以达到300W的长连接,但是活跃链接的处理最高是多少呢?
2)消息的平均长度和限制各是多少?
3)消息的及时性怎么样,延时是多少?
4)不知道现在,极光推送的用户大概有多少,所以这个峰值是在生产环境,还是测试环境的数值?
5)我想不通的是为什么,客户端要用AlarmManager来做推送消息的获取?这个消息获取还及时吗?鄙人结识android也有N载。
6)我感兴趣的是,不是极光方案一个月推送了几百万条数据,而是几秒钟或者一分钟可以处理多少。
===========================================================================================================
Android 消息推送方案
当我们开发需要与服务器交互的应用程序时,基本上都需要获取服务器端的数据。要获取服务器上不定时更新的信息,一般来说有两种方法:第一种是客 户端使用pull(拉)的方式,隔一段时间就去服务器上获取一下信息,看是否有更新的信息出现;第二种就是服务器使用push(推送)的方式,当服务器端 有更新,则将最新的信息push到客户端上,如此以来,客户端就能自动地接收到消息。
虽然pull和push两种方式都能实现获取服务器端数据更新的功能,但pull方式的弊端很明显:费流量、费电,需要我们的程序不停地去监测服务器端的更新。
首先可以来了解一下iOS和Android平台的推送机制:
iOS 系统的推送(APNS,即 Apple Push Notification Service)依 托一个或几个系统常驻进程运作,是全局的(接管所有应用的消息推送),所以可看作是独立于应用之外,而且是设备和苹果服务器之间的通讯,而非应用的提供商 服务器。比如腾讯 QQ 的服务器(Provider)会给苹果公司对应的服务器(APNs)发出通知,然后再中转传送到你的设备(Devices)之上。当你接收到通知,打开应 用,才开始从腾讯服务器接收数据,跟你之前看到通知里内容一样,但却是经由两个不同的通道而来。
Android的推送有两种形式,一种是后台Service,一种是GCM。
不过这里要指出的是,Android的这种形式和传统电脑还是有区别的,电脑的后台是保持程序进程在后台运行,而Android是在程序后台Service区域添加一个新的程序专门接收数据。
要说的话,Android的后台推送机制更加复杂,但是因为可操控部分更多,加上GCM是在2.X之后才加上的而且有可能在系统中并不存在,所以很多软件都是使用Service这种形式。
本质上,APNs 与 GCM 是类似的技术实现原理:即系统层有一个常驻的 TCP 长连接,一直保持的长连接,即使手机休眠的时候也在保持的长连接。这里对于大部分人来说,最不理解的就是,休眠时候都保持在那里的 TCP 长连接,不会耗电很厉害么?
答案是:不会。这是手机的设计来做到的。TCP长连接有个心跳的时间,在国外可以很长比如30分钟,在国内 则因为网络环境复杂一般10分钟。客户端发起的心跳,会短暂地消耗手机电能,但在这个心跳间隔期间,则消耗电能是很少的。当在心跳期间服务器端有推送信息 过来时,客户端可以收到并做处理。
这里有篇文章以 Android 为例做原理解释:极光推送技术原理:无线网络长连接。
再说 APNs 的设计成功处。
iOS 为了真正地为用户体验负责,不允许应用在后台活动。有了这个限制,但是对于终端设备,应用又是有必要“通知”到达用户的,随时与用户主动沟通起来的(典型的如聊天应用)。
这就是 APNs 的逻辑所在:iOS 自己做个长驻后台保持连接。所有应用,有必要(申请)并且被允许(用户可以改设置)的话,可以通过 APNs 中转到达用户。
Android 因为后台可以长驻,尤其是国内的 Android 的手机上 Google自家的推送服务 GCM 处于基本不可用的状态。所以,各App各显神通。聊天类应用的话,大多数直接借用 XMPP 规范里的一些成果。少量如微信有IM底子的,自己开发协议。这些在实现原理上与 APNs / GCM 没有本质的区别,但有一定的技术门槛。而大多数普遍应用,要使用推送的话,则使用轮询的方式简单实现。
其实,国外如 Urban Airship 自己实现了 Android 上的第三方提供的推送平台。国内如极光推送也实现了第三方的推送平台(技术与微信、GCM、APNs类似)。理论上,如果一个 Android 设备上多款应用都使用极光推送这种第三方推送平台的话,也可以如 APNs 一样达到节省电量、流量消耗的效果。
另可参考Android实现推送方式解决方案。
在开发Android的过程中,我们经常用到的WIFI在休眠情况下默认是会不连接的,这个时候当我们需要保持连接时,该如何解决?
不少人说可以在系统设置的WIFI高级选项中将连接设为休眠保持连接,这个办法的确可行,对于开发者来说很容易办到,但是对于用户来说他们一般不会知道这么设置,这个时候该怎么办呢?可以使用如下代码解决:
[java]
public void WifiNeverDormancy(Context mContext)
{
ContentResolver resolver = mContext.getContentResolver();
int value = Settings.System.getInt(resolver, Settings.System.WIFI_SLEEP_POLICY, Settings.System.WIFI_SLEEP_POLICY_DEFAULT);
final SharedPreferences prefs=PreferenceManager.getDefaultSharedPreferences(mContext);
Editor editor = prefs.edit();
editor.putInt(mContext.getString(R.string.wifi_sleep_policy_default), value);
editor.commit();
if(Settings.System.WIFI_SLEEP_POLICY_NEVER != value)
{
Settings.System.putInt(resolver, Settings.System.WIFI_SLEEP_POLICY, Settings.System.WIFI_SLEEP_POLICY_NEVER);
}
System.out.println("wifi value:"+value);
}
public void WifiNeverDormancy(Context mContext)
{
ContentResolver resolver = mContext.getContentResolver();
int value = Settings.System.getInt(resolver, Settings.System.WIFI_SLEEP_POLICY, Settings.System.WIFI_SLEEP_POLICY_DEFAULT);
final SharedPreferences prefs=PreferenceManager.getDefaultSharedPreferences(mContext);
Editor editor = prefs.edit();
editor.putInt(mContext.getString(R.string.wifi_sleep_policy_default), value);
editor.commit();
if(Settings.System.WIFI_SLEEP_POLICY_NEVER != value)
{
Settings.System.putInt(resolver, Settings.System.WIFI_SLEEP_POLICY, Settings.System.WIFI_SLEEP_POLICY_NEVER);
}
System.out.println("wifi value:"+value);
}上面这个函数,会自动修改我们WIFI设置中的高级选项,将其设置为一直保持连接。不用使用其他控件就可以解决。
需要注意的是此函数在调用时必须现在AndroidManifest.xml中声明权限
===========================================================================================================
在开发Android应用中,有很大一部分app是要求手机一直处于网络环境中的。在Android 设置--> WLAN -->点 击菜单键 选择 高级 -->休眠状态下保持WLAN连接的下拉列表{始终、仅限充电时、从不(会增加数据流量)}这个设置项中,很多用户并不知道 这个选项的存在,如果设置不为始终,那么我们锁屏休眠后,程序将会处于无网络状态,相应的app用户会一直处于离线模式,那么带来的效果可想而知。
那么我们开发中,应对这种情况,我们可以在程序首先开启时,读取这个值,保存起来,待程序退出时,将原先保存的值还原,这样可以很好的解决上述这个问题。
在Android API中这样描述:
public static final String WIFI_SLEEP_POLICY
Since: API Level 3
The policy for deciding when Wi-Fi should go to sleep (which will in turn switch to using the mobile data as an Internet connection).
Set to one of WIFI_SLEEP_POLICY_DEFAULT, WIFI_SLEEP_POLICY_NEVER_WHILE_PLUGGED, or WIFI_SLEEP_POLICY_NEVER.
Constant Value:"wifi_sleep_policy"
那么我们在开启应用时,首先取出这个值,上代码:
private void setWifiDormancy()
{
int value = Settings.System.getInt(getContentResolver(), Settings.System.WIFI_SLEEP_POLICY, Settings.System.WIFI_SLEEP_POLICY_DEFAULT);
final SharedPreferences prefs = getSharedPreferences(getString(R.string.wifi_sleep_policy), Context.MODE_PRIVATE);
Editor editor = prefs.edit();
editor.putInt(getString(R.string.wifi_sleep_policy_default), value);
editor.commit();
if(Settings.System.WIFI_SLEEP_POLICY_NEVER != value)
{
Settings.System.putInt(getContentResolver(), Settings.System.WIFI_SLEEP_POLICY, WIFI_SLEEP_POLICY_NEVER);
}
}
在应用退出我们需将这个设置的值还原:
private void restoreWifiDormancy()
{
final SharedPreferences prefs = getSharedPreferences(getString(R.string.wifi_sleep_policy), Context.MODE_PRIVATE);
int defaultPolicy = prefs.getInt(getString(R.string.wifi_sleep_policy_default), Settings.System.WIFI_SLEEP_POLICY_DEFAULT);
Settings.System.putInt(getContentResolver(), Settings.System.WIFI_SLEEP_POLICY, defaultPolicy);
}
这样初始设置,退出还原我们可以让应用一直处于wifi模式下(不过前提是有wifi网络哦)。
from: http://blog.sina.com.cn/s/blog_a85b30ff0101ahzd.html
===========================================================================================================
首先看一下Android Powermanager Class Overview,对Android的几种不同的休眠模式有个大致了解。
如果不进行特别的设置,Android会在一定时间后屏幕变暗,在屏幕变暗后一定时间内,约几分钟,CPU也会休眠,大多数的程序都会停止运行,从而节省电量。但你可以在代码中通过对Powmanager API的调用来设置不同的休眠模式。
Flag Value CPU Screen Keyboard
PARTIAL_WAKE_LOCK On* Off Off
SCREEN_DIM_WAKE_LOCK On Dim Off
SCREEN_BRIGHT_WAKE_LOCK On Bright Off
FULL_WAKE_LOCK On Bright Bright
如上表,最高等级的休眠是屏幕,键盘等,cpu都全部休眠。可以设置不同的模式,让其产生不同的休眠,比如让cpu保持运行。
设置代码如下:
PowerManagerpm =(PowerManager)getSystemService(Context.POWER_SERVICE);
PowerManager.WakeLockwl =pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK,"My Tag");
wl.acquire();
..screen will stay on during thissection..
wl.release();
我曾经遇到的几个坑点及解决:
1.向服务器轮询的代码不执行。
曾经做一个应用,利用Timer和TimerTask,来设置对服务器进行定时的轮询,但是发现机器在某段时间后,轮询就不再进行了。查了很久才发 现是休眠造成的。后来解决的办法是,利用系统的AlarmService来执行轮询。因为虽然系统让机器休眠,节省电量,但并不是完全的关机,系统有一部 分优先级很高的程序还是在执行的,比如闹钟,利用AlarmService可以定时启动自己的程序,让cpu启动,执行完毕再休眠。
2.后台长连接断开。
最近遇到的问题。利用Socket长连接实现QQ类似的聊天功能,发现在屏幕熄灭一段时间后,Socket就被断开。屏幕开启的时候需进行重连,但 每次看Log的时候又发现网络是链接的,后来才发现是cpu休眠导致链接被断开,当你插上数据线看log的时候,网络cpu恢复,一看网络确实是链接的, 坑。最后使用了PARTIAL_WAKE_LOCK,保持CPU不休眠。
3.调试时是不会休眠的。
让我非常郁闷的是,在调试2的时候,就发现,有时Socket会断开,有时不会断开,后来才搞明白,因为我有时是插着数据线进行调试,有时拔掉数据线,这 时Android的休眠状态是不一样的。而且不同的机器也有不同的表现,比如有的机器,插着数据线就会充电,有的不会,有的机器的设置的充电时屏幕不变暗 等等,把自己都搞晕了。其实搞明白这个休眠机制,一切都好说了。