android长连接的所有方式及分析

文章目录

    • 实现长连接的方式
    • 方案优缺点比较
    • push原理及实现
    • 主流app常用方案:
      • 1. 使用第三方的长连接服务
      • 2. 使用NIO等方案实现长连接服务
      • 3. 使用MINA等第三方框架实现长连接
      • 4.websocket实现及才坑记录

实现长连接的方式

  1. http发送心跳包轮训的方式
  2. xmpp
  3. websocket

方案优缺点比较

  1. 选择用http,这种实现方式是通过轮询来实现的,或者算是心跳包,不过也会影响一定的效果,不是完全实时。

  2. XMPP比较灵活,基于XML的东东,最主要是他可以实现实时聊天。

  3. Websocket是一个持久化的协议,WebSocket不是HTTP协议,HTTP只负责建立WebSocket连接。实现了服务端与客户端双向数据交互,也可以说是http协议一个补丁包。

从耗费的电量、流量和数据延迟性各方面来说,Push有明显的优势。但是使用Push的缺点是:
  对于客户端:实现和维护相对成本高,在移动无线网络下维护长连接,相对有一些技术上的开发难度。
  对于服务器:如何实现多核并发,cpu作业调度,数量庞大的长连接并发维护等技术,仍存在开发难点。

push原理及实现

极光推送原理
在讲述Push方案的原理前,我们先了解一下移动无线网络的特点。
移动无线网络的特点:
因为 IP v4 的 IP 量有限,运营商分配给手机终端的 IP 是运营商内网的 IP,手机要连接 Internet,就需要通过运营商的网关做一个网络地址转换(Network Address Translation,NAT)。简单的说运营商的网关需要维护一个外网 IP、端口到内网 IP、端口的对应关系,以确保内网的手机可以跟 Internet 的服务器通讯

GGSN(Gateway GPRS Support Node 网关GPRS支持结点)模块就实现了NAT功能。
因为大部分移动无线网络运营商都是为了减少网关的NAT映射表的负荷,所以如果发现链路中有一段时间没有数据通讯时,会删除其对应表,造成链路中断。(关于NAT的作用及其原理可以查看我的另一篇博文:关于使用UDP(TCP)跨局域网,NAT穿透的心得)

Push在Android平台上长连接的实现:

既然我们知道我们移动端要和Internet进行通信,必须通过运营商的网关,所以,为了不让NAT映射表失效,我们需要定时向Internet发送数据,因为只是为了不然NAT映射表失效,所以只需发送长度为0的数据即可。

这时候就要用到定时器,在android系统上,定时器通常有一下两种:
1.java.util.Timer
2.android.app.AlarmManager

分析:
Timer:可以按照计划或者时间周期来执行相关的任务。但是Timer需要用WakeLock来让CPU保持唤醒状态,才能保证任务的执行,这样子会消耗大量流量;当CPU处于休眠的时候,就不能唤醒执行任务,所以应用于移动端明显是不合适。

AlarmManager:AlarmManager类是属于android系统封装好来管理RTC模块的管理类。这里就涉及到RTC模块,要更好地了解两者的区别,就要明白两者真正的区别。

RTC(Real- Time Clock)实时闹钟在一个嵌入式系统中,通常采用RTC来提供可靠的系统时间,包括时分秒和年月日等;而且要求在系统处于关机状态下它也能够正常工作(通常采用后备电池供电),它的外围也不需要太多的辅助电路,典型的就是只需要一个高精度的32.768KHz晶体和电阻电容等。(如果对这方面感兴趣,可以自己查阅相关资料,这里就说个大概)好了,回来正题。所以,AlarmManager又称全局定时闹钟。这意味着,当我用使用AlarmManager来定时执行任务,CPU可以正常地休眠,只有在执行任务是,才唤醒CPU,这个过程是很短时间的。
下面简单来说明其使用:
1.类似于Timer功能:
//获得闹钟管理器
AlarmManager am = (AlarmManager)getSystemService(ALARM_SERVICE);
//设置任务执行计划
am.setRepeating(AlarmManager.ELAPSED_REALTIME, firstTime, 51000, sender);//从firstTime才开始执行,每隔5秒再执行
2.实现全局定时功能:
//获得闹钟管理器
AlarmManager am = (AlarmManager)getSystemService(ALARM_SERVICE);
//设置任务执行计划
am.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, firstTime, 5
1000, sender);//从firstTime才开始执行,每隔5秒再执行

总结:在android客户端使用Push推送时,应该使用AlarmManager来实现心跳功能,使其真正实现长连接。

主流app常用方案:

一般而言长连接已经是App的标配了,推送功能的实现基础就是长连接,当然了我们也可以通过轮训操作实现推送功能,但是轮训一般及时性比较差,而且网络消耗与电量销毁比较多,因此一般推送功能都是通过长连接实现的。
那么如何实现长连接呢?现在一般有这么几种实现方式:

使用第三方的长连接服务;
通过NIO等方案实现长连接服务;
通过MINA等第三方框架实现长连接;

几种长连接服务的具体实现,以及各自的优缺点。

1. 使用第三方的长连接服务

介绍:这是最简单的方式,我们可以通过接入极光推送,百度推送,友盟等第三方服务实现长连接,通过接入第三方的API我们可以很方便的接入第三方的长连接,推送服务,但是这种方式定制化程度不太好,如果对长连接服务不是要求特别高,对定制化要求不是很高的话基本可以考虑这种方式(目前主流的App都是使用第三方的长连接服务)
优势:简单,方便
劣势:定制化程度不高

2. 使用NIO等方案实现长连接服务

介绍:通过NIO的方式实现长连接,这种方式对技术要求程度比较高,基本都是通过java API实现长连接,实现心跳包,实现异常情况的容错等操作,可以说通过NIO实现长连接对技术要求很高,一般如果没有成行的技术方案比建议这么做,就算实现了长连接,后期连接的维护,对电量,流量的损耗等都需要持续的优化。
优势:定制化比较高
劣势:技术要求高,需要持续的维护

3. 使用MINA等第三方框架实现长连接

介绍:MINA是一个第三方的NIO框架,该框架实现了一整套的长连接机制,包括长连接的建立,心跳包的实现,异常机制的容错等。使用MINA实现长连接可以定制化的实现一些特有的功能,并且比NIO方案较为简单,因为其已经封装了一些长连接的特有机制,比如心跳包,容错等。
优势:可定制,较NIO方法简单
劣势:也需要一定的技术储备
长连接具体实现
在我们的Android客户端中长连接的实现机制采用–MINA方式。这里多说一句,一开始的长连接采用的是NIO方案,但是采用这种方案之后踩了很多坑,包括心跳,容错等机制都是自己写的,所以耗费了大量的时间,而且对手机电量的消耗很大,最后决定使用MINA NIO框架重新实现一遍长连接,后来经过实测,长连接的稳定性还有耗电量,流量的消耗等指标方面有了很大的提高。
下面我将简单的介绍一下通过NIO实现长连接的具体流程:

引入MINA jar包,在App启动页面,登录页面启动长连接;
创建后台服务,在服务中创建MINA长连接;
实现心跳包,重写一些容错机制;
实现长连接断了之后的重连机制,并且重连次数有限制不能一直重连;
长连接断了之后实现轮训操作,这里的轮训服务只有在长连接断了之后才启动,在长连接恢复之后关闭;

在刚开始接触Mina的时候呢,看到了郭神在imooc上的andorid推送的教学视频,也是采用了Mina框架。
简单说下Mina的优势:

1、非常适合C/S架构的通信项目
2、Apache出品的开源项目,值得信赖
3、官网上有详细的资料供开发者学习
4、使用起来非常简单,降低学习成本。

4.websocket实现及才坑记录

一、导包如下

  //compile 'com.github.nkzawa:socket.io-client:0.3.0'//已过时,仅用于聊天Socket连接(js变种版websocket)
    compile ('io.socket:socket.io-client:0.8.3') {//最新包
        exclude group: 'org.json', module: 'json' //处理: WARNING: Dependency org.json:json:20090211 is ignored
    }

PS:配置参考地址:https://socket.io/blog/native-socket-io-and-android/
二、发送消息

  /**
     * Emits an event. When you pass {@link Ack} at the last argument, then the acknowledge is done.
     *
     * @param event an event name.
     * @param args data to send.
     * @return a reference to this object.
     */
    public Emitter emit(final String event, final Object... args){
        ...
    }

上面的方法一些正常,下面还以一个带Ack监听

/**
     * Emits an event with an acknowledge.
     *
     * @param event an event name
     * @param args data to send.
     * @param ack the acknowledgement to be called
     * @return a reference to this object.
     */
    public Emitter emit(final String event, final Object[] args, final Ack ack) {
        ...
    }

在demo是一切正常,连接正常,登录正常,登录有回调的。但是正式的出问题了。

 //Socket绑定监听
        mSocket.on(Socket.EVENT_CONNECT, onConnect);
        mSocket.on(Socket.EVENT_DISCONNECT, onDisconnect);
        mSocket.on(Socket.EVENT_CONNECT_ERROR, onConnectError);
        mSocket.on(Socket.EVENT_CONNECT_TIMEOUT, onConnectError);
        //mSocket.io().on(Manager.EVENT_TRANSPORT, trans);//网上说SSL出问题,但是问题没有得到处理
        mSocket.on(EVENT_MSG, msg);
        mSocket.connect();

1、流程:
①、在onConnect连接成功,②、emit(event, objects[], ack)发送登录信息msg
2、问题:
发送的登录信息msg用户名是英文没有任何问题,奇葩来了,如果含有中文则调onDisconnect监听断开连接,打印args[0].toString():transport error。
  关键的是接口同事打印,收到并解析 登录信息 成功,也给回包了。
3、处理bug:
首先想到的包的问题是否过时,替换最新的依然是:外甥打灯笼—照舅。
  然后就开始百度生涯:android io.socket disconnect transport error

不负有心人,找到如下几个当时认为可用的,确实也收到启发。
https://stackoverflow.com/questions/29073746/socket-io-disconnect-event-transport-close-client-namespace-disconnect
https://stackoverflow.com/questions/37093221/using-socket-io-on-android-always-returns-xhr-poll-error
官网:https://socket.io/docs/client-api/
但问题没有解决,已经到快到十点了,还是明天处理。换一个思路打断点监听onDisconnect,找出的问题发生的时间点。

打断点查看一步步的流程:
又发生了奇葩的问题,打断点时候有时能收到登录成功的回调,有时收不到。查找源码

   /**
     * Emits an event with an acknowledge.
     *
     * @param event an event name
     * @param args data to send.
     * @param ack the acknowledgement to be called
     * @return a reference to this object.
     */
    public Emitter emit(final String event, final Object[] args, final Ack ack) {
        EventThread.exec(new Runnable() {
            @Override
            public void run() {
                List _args = new ArrayList() {{
                    add(event);
                    if (args != null) {
                        addAll(Arrays.asList(args));
                    }
                }};
                
                JSONArray jsonArgs = new JSONArray();
                for (Object _arg : _args) {
                    jsonArgs.put(_arg);
                }
                int parserType = HasBinary.hasBinary(jsonArgs) ? Parser.BINARY_EVENT : Parser.EVENT;
                Packet packet = new Packet(parserType, jsonArgs);

                logger.fine(String.format("emitting packet with ack id %d", ids));
                Socket.this.acks.put(ids, ack);
                packet.id = ids++;

                Socket.this.packet(packet);
            }
        });
        return this;
    }

有时走到add(event);有时走到addAll(Arrays.asList(args));断开连接onDisconnect,又一次偶尔停顿了一下,Ack 有反应,收到的回包。预感问题找到了。又跑一次,debug慢点,Ack 收到登录回包;再次验证,Ack 都有回包。狂喜,问题找到了,哈哈哈!!!
这属于线程间的异步通信问题,(PS:volatile原子性问题,32位的不会出现,但是64位的就会出现。这就解释了字母正常,汉字不正常)
处理方法如下:
new Thread(){
       @Override
       public void run() {
             try {
                Thread.sleep(400);//延迟发送
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            mSocket.emit(EVENT, new Object[]{loginMsg()},  ack);
        }
}.start();
 
  

PS:正如前面所说有两种发送消息的方法,带Ack监听要特殊处理一下。

public Emitter emit(final String event, final Object... args)
public Emitter emit(final String event, final Object[] args, final Ack ack)

你可能感兴趣的:(android基础总结)