android后台信息推送调研

来源:互联网 发布:淘宝上卖卫浴赚钱吗 编辑:程序博客网 时间:2024/04/28 01:59

前言

我们已经开发了一个应用,这里称为A应用,类似于天气weather那种。现在的任务就是如果这些A应用有新版本了,或者天气出现比较恶劣的状况,要及时在手机上进行消息的推送,提示有新的应用可以更新了,和天气将要变遭了,提醒用户需要注意的情况。及需要实现消息推送机制。

推送方式基础知识

  要获取服务器上不定时更新的信息,一般来说有两种方法:第一种是客户端使用Pull(拉)的方式,就是隔一段时间就去服务器上获取一下信息,看是否有更新的信息出现。第二种就是 服务器使用Push(推送)的方式,当服务器端有新信息了,则把最新的信息Push到客户端上。这样,客户端就能自动的接收到消息。 

  虽然Pull和Push两种方式都能实现获取服务器端更新信息的功能,但是明显来说Push方式比Pull方式更优越。因为Pull方式更费客户端的网络流量,更主要的是费电量,还需要我们的程序不停地去监测服务端的变化。  

在开发Android和iPhone应用程序时,我们往往需要从服务器不定的向手机客户端即时推送各种通知消息。我们只需要在Android或IPhone的通知栏处向下一拉,就展开了Notification Panel,可以集中一览各种各样通知消息。目前iOS平台上已经有了比较简单的和完美的推送通知解决方案,可是Android平台上实现起来却相对比较麻烦。

现行方案汇总

1.   方案一:C2DM云端推送功能——Google官方

在Android手机平台上,Google提供了C2DM(Cloudto Device Messaging)服务,起初我就是准备采用这个服务来实现自己手机上的推送功能,并将其带入自己的项目中。 

  Android Cloud to Device Messaging (C2DM)是一个用来帮助开发者从服务器向Android应用程序发送数据的服务。该服务提供了一个简单的、轻量级的机制,允许服务器可以通知移动应用程序直接与服务器进行通信,以便于从服务器获取应用程序更新和用户数据。C2DM服务负责处理诸如消息排队等事务并向运行于目标设备上的应用程序分发这些消息。

下面是C2DM操作过程示例图:


图1 C2DM操作过程示例图

经过一番研究发现,这个服务存在很大的问题:

  1)C2DM内置于Android的2.2系统上,无法兼容老的1.6到2.1系统;

  2)C2DM需要依赖于Google官方提供的C2DM服务器,由于国内的网络环境,这个服务经常不可用,如果想要很好的使用,我们的AppServer必须也在国外,这个恐怕不是每个开发者都能够实现的;

  3) 不像在iPhone中,他们把硬件系统集成在一块了。所以对于我们开发者来说,如果要在我们的应用程序中使用C2DM的推送功能,因为对于不同的这种硬件厂商平台,比如摩托罗拉、华为、中兴做一个手机,他们可能会把Google的这种服务去掉,尤其像在国内就很多这种,把Google这种原生的服务去掉。买了一些像什么山寨机或者是华为这种国产机,可能Google的服务就没有了。而像在国外出的那些可能会内置。

2.   方案二:利用MQTT协议,broker做代理服务器

MQTT是一个轻量级的消息发布/订阅协议,它是实现基于手机客户端的消息推送服务器的理想解决方案。但是随着用户的增多这个方案会有问题,因为broker的连接数有上限,到了一定程度后就无法连接了,这也就导致消息很难发送出去。

总之,连接数量有限制。


图2 MQTT协议架构图

3.   方案三:XMPP协议实现Android推送功能

基于XMPP协议,很多人都建议使用这个,谷歌官方的C2DM也是基于XMPP研发的,使用这个方案不会依赖android系统,也不依赖于谷歌服务器。

事实上Google官方的C2DM服务器底层也是采用XMPP协议进行的封装。XMPP(可扩展通讯和表示协议)是基于可扩展标记语言(XML)的协议,它用于即时消息(IM)以及在线探测。这个协议可能最终允许因特网用户向因特网上的其他任何人发送即时消息。androidpn是一个基于XMPP协议的Java开源Android push notification实现。它包含了完整的客户端和服务器端。

androidpn实现意图如下图所示:


图3 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请求。服务器架构如下:


图4 androidpn PushNotification Service示意图

  最上层包含四个组成部分,分别是SessionManager,Auth Manager,PresenceManager以及Notification Manager。SessionManager负责管理客户端与服务器之间的会话,Auth Manager负责客户端用户认证管理,Presence Manager负责管理客户端用户的登录状态,NotificationManager负责实现服务器向客户端推送消息功能。

  这个解决方案的最大优势就是简单,我们不需要象C2DM那样依赖操作系统版本,也不会担心某一天Google服务器不可用。利用XMPP协议我们还可以进一步的对协议进行扩展,实现更为完善的功能。 采用这个方案,我们目前只能发送文字消息,不过对于推送来说一般足够了,因为我们不能指望通过推送得到所有的数据,一般情况下,利用推送只是告诉手机端服务器发生了某些改变,当客户端收到通知以后,应该主动到服务器获取最新的数据,这样才是推送服务的完整实现。 XMPP协议书相对来说还是比较简单的,值得我们进一步研究。

  但是在经过一段时间的测试,我发现关于androidpn也存在一些不足之处:

  1. 比如时间过长时,就再也收不到推送的信息了。

  2. 性能上也不够稳定。

   3. 如果将消息从服务器上推送出去,就不再管理了,不管消息是否成功到达客户端手机上。

4.   方案四:使用第三方推送服务

通过嵌入SDK使用第三方提供的推送服务,目前主流的有百度云推送,极光推送,个推,PUBNUB,聚能推等。

优点:稳定,成熟,节省开发和探索时间,相对自己开发成本低,推送管理界面及统计程序完善。

缺点:有程序嵌入顾虑。

其中的极光推送的推送原理参阅:附件一。

5.   方案五:自己搭建一个推送平台

这不是一件轻松的工作,当然可以根据各自的需要采取合适的方案。

参考代码(客户端、服务端):附件二

附件一:

极光推送技术原理:移动无线网络长连接

移动互联网应用现状

因为手机平台本身、电量、网络流量的限制,移动互联网应用在设计上跟传统 PC 上的应用很大不一样,需要根据手机本身的特点,尽量的节省电量和流量,同时又要尽可能的保证数据能及时到达客户端。

为了解决数据同步的问题,在手机平台上,常用的方法有2种。一种是定时去服务器上查询数据,也叫Polling,还有一种手机跟服务器之间维护一个 TCP长连接,当服务器有数据时,实时推送到客户端,也就是我们说的 Push

从耗费的电量、流量和数据送达的及时性来说,Push都会有明显的优势,但 Push 的实现和维护成本相对较高。在移动无线网络下维护长连接,相对也有一些技术上的难度。本文试图给大家介绍一下我们极光推送Android 平台上是如何维护长连接。

移动无线网络的特点

因为 IP v4  IP 量有限,运营商分配给手机终端的 IP 是运营商内网的 IP,手机要连接 Internet,就需要通过运营商的网关做一个网络地址转换(NetworkAddress TranslationNAT)。简单的说运营商的网关需要维护一个外网 IP、端口到内网 IP、端口的对应关系,以确保内网的手机可以跟Internet 的服务器通讯。


NAT 功能由图中的 GGSN 模块实现。

大部分移动无线网络运营商都在链路一段时间没有数据通讯时,会淘汰 NAT 表中的对应项,造成链路中断。

Android 平台上长连接的实现

为了不让 NAT 表失效,我们需要定时的发心跳,以刷新 NAT 表项,避免被淘汰。

Android 上定时运行任务常用的方法有2种,一种方法用 Timer,另一种是AlarmManager

Timer

Android  Timer 类可以用来计划需要循环执行的任务,Timer的问题是它需要用 WakeLock  CPU 保持唤醒状态,这样会大量消耗手机电量,大大减短手机待机时间。这种方式不能满足我们的需求。

AlarmManager

AlarmManager  Android 系统封装的用于管理 RTC 的模块,RTC(Real Time Clock) 是一个独立的硬件时钟,可以在 CPU 休眠时正常运行,在预设的时间到达时,通过中断唤醒 CPU

这意味着,如果我们用AlarmManager 来定时执行任务,CPU 可以正常的休眠,只有在需要运行任务时醒来一段很短的时间。极光推送的 Android SDK就是基于这种技术实现的。

服务器设计

当有大量的手机终端需要与服务器维持长连接时,对服务器的设计会是一个很大的挑战。

假设一台服务器维护10万个长连接,当有1000万用户量时,需要有多达100台的服务器来维护这些用户的长连接,这里还不算用于做备份的服务器,这将会是一个巨大的成本问题。那就需要我们尽可能提高单台服务器接入用户的量,也就是业界已经讨论很久了的 C10K 问题。

C2000K

针对这个问题,我们专门成立了一个项目,命名为C2000K,顾名思义,我们的目标是单机维持200万个长连接。最终我们采用了多消息循环、异步非阻塞的模型,在一台双核、24G内存的服务器上,实现峰值维持超过300万个长连接。

后记

稳定维护长连接是推送平台的一个基础,极光推送团队将会在这方面长期投入,以保证用户能有效的节省电量、流量,同时数据能实时送达。

附件二:

android代码:

import org.apache.http.protocol.HTTP;

import org.json.JSONObject;

import com.taocaiku.gaea.common.TckApp;

import com.taocaiku.gaea.domain.context.Container;

importcom.taocaiku.gaea.service.SettingService;

import com.taocaiku.gaea.util.FileUtil;

import android.annotation.SuppressLint;

import android.os.Bundle;

import android.os.Message;

import android.util.Log;

 

/**

 * 运行Socket通信的线程

 *@author TCK-001

 *@version 1.0

 */

@SuppressLint("HandlerLeak")

public final class SocketThread extendsThread {

 

   public void run() {

       try {

           while (true) {

                if(!SocketManager.get().isStart()) {

                    return;

                }

                ServerSocket sever =SocketManager.get().getSocket();

                if (null == sever ||sever.isClosed()) {

                    return;

                }

                new ReadSocket(sever.accept()).start();

           }

       } catch (Exception e) {

           Log.e("AbstractActivity", "Socket已经被回收");

       }

    }

    

   /**

    * 读取Socket并解析

    * @author TCK-001

    * @version 1.0

    */

   public class ReadSocket extends Thread {

        

       private Socket socket;

        

       public ReadSocket(Socket socket) {

           this.socket = socket;

       }

        

       public void run() {

           try {

                String jsonStr =FileUtil.get().readFile(socket.getInputStream(), HTTP.UTF_8, 1, false);

                socket.close();

                JSONObject json = newJSONObject(jsonStr);

                if(json.getLong("memberId") == Container.member.getId() &&Container.socketKey.equals(json.getString("socketKey"))

                        &&SettingService.get().getSetting(SettingService.RECEIVE_MSG)) {

                    Bundle bundle = newBundle();

                   bundle.putInt("type", json.getInt("type"));

                    bundle.putString("data",json.getJSONObject("data").toString());

                    Message message = newMessage();

                    message.setData(bundle);

                   TckApp.get().getLastAct(null).socketHandler.sendMessage(message);

               }

           } catch (Exception e) {

               Log.e("AbstractActivity", "接收Socket消息,解析json",e);

           }

       }

        

    }

 

}

服务端代码:

  /**

    * 发送一个Socket通信消息

    * @param vo

    * @return

    */

   public static boolean sendSocket(SocketVo vo) {

       try {

           if (!pingServer(vo.getIp(), 2000)) {return false;}

           Socket socket = new Socket(vo.getIp(), vo.getPort());// 创建socket对象,指定服务器端地址和端口号

           OutputStream output = socket.getOutputStream();

           Writer writer = new OutputStreamWriter(output, CharEncoding.UTF_8);

           PrintWriter out = new PrintWriter(writer, true);// 获取 Client 端的输出流

           vo.setIp(null);vo.setPort(0);

           out.print(GsonUtils.toJson(vo));// 填充信息

           writer.flush();writer.close();

           out.flush();out.close();

           output.flush();output.close();

           socket.close();

           return true;

       } catch (Exception e) {

           log.error("发送Socket失败:" + e.getMessage());

           return false;

       }

    }

    

   /**

    * 能否ping通IP地址

    * @param server IP地址

    * @param timeout 超时时长

    * @return boolean

    */

   public static boolean pingServer(String server, int timeout) {

       try {

           Process process =Runtime.getRuntime().exec("ping " + server + " -n 1 -w " +timeout);

           if (null == process) {return false;}

           String info = FileUtil.readFile(process.getInputStream(), ENCODING_GBK,1, false);

           return ToolUtil.getStrCharNum(info, "ms") >= 3;

       } catch (Exception e) {

           return false;

       }

    }

0 0