[转]Android TCP长连接 心跳机制及实现

 

维护任何一个长连接都需要心跳机制,客户端发送一个心跳给服务器,服务器给客户端一个心跳应答, 
这样双方都知道他们之间的连接是没有断开。【客户端先发送给服务端】

如果超过一个时间的阈值,客户端没有收到服务器的应答,或者服务器没有收到客户端的心跳, 
那么对客户端来说则断开与服务器的连接重新建立一个连接,对服务器来说只要断开这个连接即可。

[背景知识]:

1.智能手机上的长连接心跳和在Internet上的长连接心跳有什么不同?

当一台智能手机连上移动网络时,其实并没有真正连接上Internet,运营商分配给手机的IP其实是运营商的内网IP,手机终端要连接上Internet还必须通过运营商的网关进行IP地址的转换,这个网关简称为NAT(NetWork Address Translation),简单来说就是手机终端连接Internet 其实就是移动内网IP,端口,外网IP之间相互映射。相当于在手机终端在移动无线网络这堵墙上打个洞与外面的Internet相连。

GGSN(GateWay GPRS Support Note 网关GPRS支持节点)模块就实现了NAT功能,由于大部分的移动无线网络运营商为了减少网关NAT映射表的负荷,如果一个链路有一段时间没有通信时就会删除其对应表,造成链路中断,正是这种刻意缩短空闲连接的释放超时,原本是想节省信道资源的作用,没想到让互联网的应用不得以远高于正常频率发送心跳来维护推送的长连接。

手机应用发送心跳的频率很短,既造成了信道资源的浪费,也造成了手机电量的快速消耗。

2.Android系统的推送和iOS的推送有什么区别:

没有长连接,服务端就无法主动向客户端推送.

iOS长连接是由系统来维护的,也就是说苹果的iOS系统在系统级别维护了一个客户端和苹果服务器的长链接,iOS上的所有应用上的推送都是先将消息推送到苹果的服务器然后将苹果服务器通过这个系统级别的长链接推送到手机终端上,这样的的几个好处为: 
* (1).在手机终端始终只要维护一个长连接即可,而且由于这个长链接是系统级别的不会出现被杀死而无法推送的情况。 
* (2).省电,不会出现每个应用都各自维护一个自己的长连接。 
* (3).安全,只有在苹果注册的开发者才能够进行推送,等等。 
不会被杀死,省电,安全.

由于Google的推送框架C2DM在中国境内不能使用,android的长连接是由每个应用各自维护一个长连接,如果都24小时在线,这种电量和流量的消耗是可想而知的。

3.几种推送的实现方式:

  1)轮询(Pull)方式:应用程序应当阶段性的与服务器进行连接并查询是否有新的消息到达,你必须自己实现与服务器之间的通信,例如消息排队等。而且你还要考虑轮询的频率,如果太慢可能导致某些消息的延迟,如果太快,则会大量消耗网络带宽和电池。

  2)SMS(Push)方式:在Android平台上,你可以通过拦截SMS消息并且解析消息内容来了解服务器的意图,并获取其显示内容进行处理。这是一个不错的想法,我就见过采用这个方案的应用程序。这个方案的好处是,可以实现完全的实时操作。但是问题是这个方案的成本相对比较高,我们需要向移动公司缴纳相应的费用。我们目前很难找到免费的短消息发送网关来实现这种方案。

  3)持久连接(Push)方式:这个方案可以解决由轮询带来的性能问题,但是还是会消耗手机的电池。iOS平台的推送服务之所以工作的很好,是因为每一台手机仅仅保持一个与服务器之间的连接,事实上C2DM也是这么工作的。Android操作系统允许在低内存情况下杀死系统服务,所以我们的推送通知服务很有可能就被操作系统Kill掉了。我们很难在手机上实现一个可靠的服务,目前也无法与iOS平台的推送功能相比。

[协议]:

1,XMPP简介:

Google官方的C2DM服务器底层也是采用XMPP协议进行的封装。 
XMPP(Extensible Messageing and Presence Protocol:可扩展消息与存在协议)是基于可扩展标记语言(XML)的协议,它用于即时消息(IM)以及在线探测。

基本网络结构: 
XMPP中定义了三个角色,客户端,服务器,网关。通信能够在这三者的任意两个之间双向发生。服务器同时承担了客户端信息记录,连接管理和信息的路由功能。网关承担着与异构即时通信系统的互联互通,异构系统可以包括SMS(短信),MSN,ICQ等。基本的网络形式是单客户端通过TCP/IP连接到单服务器,然后在之上传输XML。

XMPP通过TCP传输的是与即时通讯相关的指令。XMPP的核心部分就是一个在网络上分片断发送XML的流协议。这个流协议是XMPP的即时通讯指令的传递基础,也是一个非常重要的可以被进一步利用的网络基础协议。所以可以说,XMPP用TCP传的是XML流。

C:  <stream:stream>
C:  <presence/>
C:  <iq type="get">
           <query xmlns="jabber:iq:roster"/>
    </iq>
S:  <iq type="result">
           <query xmlns="jabber:iq:roster">
            <item jid="suke@skh.whu.edu.cn"xs/>
            <item jid="gmz@skh.whu.edu.cn"/>
            <item jid="beta@skh.whu.edu.cn"/>
        </query>
    </iq>
C:  <message from="suke@skh.whu.edu.cn" 
              to="beta@skh.whu.edu.cn">
           <body>Off with his head!</body>
    </message>
S:  <message from="lj@skh.whu.edu.cn"
              to="cyl@skh.whu.edu.cn ">
           <body>You are all pardoned.</body>
    </message>
C:  <presence type="unavailable"/>
C:     </stream:stream>

2, MQTT简介:

MQTT 协议主要解决的是机器与机器之间数据通信,其适用于如下但不限于这几点:

  • 即时传输的轻量级协议
  • 专门设计用于低带宽或者高昂的网络费用
  • 具备三种服务品质层级

MQTT 最引以为豪的就是最小的2 byte 头部传输开销.我们看下其他流行的协议的message format的设计:

XMPP 消息体xml:

|--------------------| | <stream> | |--------------------| | <presence> | | <show/> | | </presence> | |--------------------| | <message to='foo'> | | <body/> | | </message> | |--------------------| | <iq to='bar'> | | <query/> | | </iq> | |--------------------| | ... | |--------------------| | </stream> | |--------------------|
  • 1

HTTP

HTTP-message = Request | Response ; HTTP/1.1 messages
  • 1

还有很多协议,就不一样细说了,就举两个我比较了解的.就目前通用的协议来看很少有比MQTT 还要低的传输开销了.

*第一个byte 用于说明消息体的信息(Message Type 4bit|DUP flag |QoS level 2bit|RETAIN). 
第二个byte 用于传输我们需要传输的数据(Remaining Length, 8bit).*

3,移动端消息推送 xmpp 和 mqtt 哪个更费电?

使用XMPP协议(Openfire + Spark + Smack) 
简介:基于XML协议的通讯协议,前身是Jabber,目前已由IETF国际标准化组织完成了标准化工作。 
优点:协议成熟、强大、可扩展性强、目前主要应用于许多聊天系统中,且已有开源的Java版的开发实例androidpn。 
缺点:协议较复杂、冗余(基于XML)、费流量、费电,部署硬件成本高。

使用MQTT协议 
简介:轻量级的、基于代理的“发布/订阅”模式的消息传输协议。 
优点:协议简洁、小巧、可扩展性强、省流量、省电,目前已经应用到企业领域,且已有C++版的服务端组件rsmb。 
缺点:不够成熟、实现较复杂、服务端组件rsmb不开源,部署硬件成本较高。

MQTT相比XMPP 有几个优势: 
二进制,非常精简,适合做大量节点弱网络差的场景,非常适合现在移动互联网的基础设施;MQTT是天然的订阅发布系统,有权限的人都可以往里头发消息;开源的协议和实现;扩展方便且轻量级。

XMPP不适合移动网络有几个原因: 
协议虽然完整扩展性虽然好,它耗费网络流量很大,交互次数太多,跑起来比MQTT慢很多;另外有高达70%的流量是耗费在XMPP本身的标签和编解码上面。

MQTT是一个由 IBM 开发的传输协议,它被设计用于轻量级的发布/订阅式消息传输,旨在为低带宽和不稳定的网络环境中的设备提供可靠的网络服务。相比于XMPP等传统协议,MQTT 是专门针对移动互联网开发的轻量级传输协议,这种传输协议连接稳定、心跳数据包小,所以具备耗电量低、耗流量低的优势。推送服务的最佳协议!(纯属粘贴,未经验证...)

[心跳代码实现]:

基于TCP的socket编程 有三种(2011年),

  • 流式套接字(SOCK_STREAM),
  • 数据报套接字(SOCK_DGRAM),
  • 原始套接字(SOCK_RAW);

基于TCP的socket编程是采用的流式套接字。

服务器端编程的步骤: 
1:加载套接字库,创建套接字(WSAStartup()/socket()); 
2:绑定套接字到一个IP地址和一个端口上(bind()); 
3:将套接字设置为监听模式等待连接请求(listen()); 
4:请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept()); 
5:用返回的套接字和客户端进行通信(send()/recv()); 
6:返回,等待另一连接请求; 
7:关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup())。

客户端编程的步骤: 
1:加载套接字库,创建套接字(WSAStartup()/socket()); 
2:向服务器发出连接请求(connect()); 
3:和服务器端进行通信(send()/recv()); 
4:关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup())。

心跳是逻辑应用层的东西,需要自己实现,当socket空闲时,发送心跳包,报文件格式自定义. 
心跳检测需要以下步骤: 
1 客户端每隔一个时间间隔发生一个探测包给服务器 
2 客户端发包时启动一个超时定时器 
3 服务器端接收到检测包,应该回应一个包 
4 如果客户机收到服务器的应答包,则说明服务器正常,删除超时定时器 
5 如果客户端的超时定时器超时,依然没有收到应答包,则说明服务器挂了

[Demo]建立一个带有心跳检测的SocketDemo

此处存一个别人家的demo

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.ref.WeakReference;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Arrays;

import android.app.Service;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;

public class BackService extends Service {
    private static final String TAG = "BackService";
    private static final long HEART_BEAT_RATE = 3 * 1000;

    public static final String HOST = "192.168.1.101";// "192.168.1.21";//
    public static final int PORT = 9800;

    public static final String MESSAGE_ACTION="org.feng.message_ACTION";
    public static final String HEART_BEAT_ACTION="org.feng.heart_beat_ACTION";

    private ReadThread mReadThread;

    private LocalBroadcastManager mLocalBroadcastManager;

    private WeakReference<Socket> mSocket;

    // For heart Beat
    private Handler mHandler = new Handler();
    private Runnable heartBeatRunnable = new Runnable() {

        @Override
        public void run() {
            if (System.currentTimeMillis() - sendTime >= HEART_BEAT_RATE) {
                boolean isSuccess = sendMsg("");//就发送一个\r\n过去 如果发送失败,就重新初始化一个socket
                if (!isSuccess) {
                    mHandler.removeCallbacks(heartBeatRunnable);
                    mReadThread.release();
                    releaseLastSocket(mSocket);
                    new InitSocketThread().start();
                }
            }
            mHandler.postDelayed(this, HEART_BEAT_RATE);
        }
    };

    private long sendTime = 0L;
    private IBackService.Stub iBackService = new IBackService.Stub() {

        @Override
        public boolean sendMessage(String message) throws RemoteException {
            return sendMsg(message);
        }
    };

    @Override
    public IBinder onBind(Intent arg0) {
        return iBackService;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        new InitSocketThread().start();
        mLocalBroadcastManager=LocalBroadcastManager.getInstance(this);

    }
    public boolean sendMsg(String msg) {
        if (null == mSocket || null == mSocket.get()) {
            return false;
        }
        Socket soc = mSocket.get();
        try {
            if (!soc.isClosed() && !soc.isOutputShutdown()) {
                OutputStream os = soc.getOutputStream();
                String message = msg + "\r\n";
                os.write(message.getBytes());
                os.flush();
                sendTime = System.currentTimeMillis();//每次发送成数据,就改一下最后成功发送的时间,节省心跳间隔时间
            } else {
                return false;
            }
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    private void initSocket() {//初始化Socket
        try {
            Socket so = new Socket(HOST, PORT);
            mSocket = new WeakReference<Socket>(so);
            mReadThread = new ReadThread(so);
            mReadThread.start();
            mHandler.postDelayed(heartBeatRunnable, HEART_BEAT_RATE);//初始化成功后,就准备发送心跳包
        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void releaseLastSocket(WeakReference<Socket> mSocket) {
        try {
            if (null != mSocket) {
                Socket sk = mSocket.get();
                if (!sk.isClosed()) {
                    sk.close();
                }
                sk = null;
                mSocket = null;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    class InitSocketThread extends Thread {
        @Override
        public void run() {
            super.run();
            initSocket();
        }
    }

    // Thread to read content from Socket
    class ReadThread extends Thread {
        private WeakReference<Socket> mWeakSocket;
        private boolean isStart = true;

        public ReadThread(Socket socket) {
            mWeakSocket = new WeakReference<Socket>(socket);
        }

        public void release() {
            isStart = false;
            releaseLastSocket(mWeakSocket);
        }

        @Override
        public void run() {
            super.run();
            Socket socket = mWeakSocket.get();
            if (null != socket) {
                try {
                    InputStream is = socket.getInputStream();
                    byte[] buffer = new byte[1024 * 4];
                    int length = 0;
                    while (!socket.isClosed() && !socket.isInputShutdown()
                            && isStart && ((length = is.read(buffer)) != -1)) {
                        if (length > 0) {
                            String message = new String(Arrays.copyOf(buffer,
                                    length)).trim();
                            Log.e(TAG, message);
                            //收到服务器过来的消息,就通过Broadcast发送出去
                            if(message.equals("ok")){//处理心跳回复
                                Intent intent=new Intent(HEART_BEAT_ACTION);
                                mLocalBroadcastManager.sendBroadcast(intent);
                            }else{
                                //其他消息回复
                                Intent intent=new Intent(MESSAGE_ACTION);
                                intent.putExtra("message", message);
                                mLocalBroadcastManager.sendBroadcast(intent);
                            }
                        }
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

在Activity中发送以及接收数据:

import java.lang.ref.WeakReference;

import org.feng.sockettest.server.BackService;
import org.feng.sockettest.server.IBackService;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.v4.content.LocalBroadcastManager;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends Activity {
    private static final String TAG = "MainActivity";

    private IBackService iBackService;
    private ServiceConnection conn = new ServiceConnection() {

        @Override
        public void onServiceDisconnected(ComponentName name) {
            iBackService = null;

        }

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            iBackService = IBackService.Stub.asInterface(service);
        }
    };

    private TextView mResultText;
    private EditText mEditText;
    private Intent mServiceIntent;

    class MessageBackReciver extends BroadcastReceiver {
        private WeakReference<TextView> textView;

        public MessageBackReciver(TextView tv) {
            textView = new WeakReference<TextView>(tv);
        }

        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            TextView tv = textView.get();
            if (action.equals(BackService.HEART_BEAT_ACTION)) {
                if (null != tv) {
                    tv.setText("Get a heart heat");
                }
            } else {
                String message = intent.getStringExtra("message");
                tv.setText(message);
            }
        };
    }

    private MessageBackReciver mReciver;

    private IntentFilter mIntentFilter;

    private LocalBroadcastManager mLocalBroadcastManager;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mLocalBroadcastManager = LocalBroadcastManager.getInstance(this);

        mResultText = (TextView) findViewById(R.id.resule_text);
        mEditText = (EditText) findViewById(R.id.content_edit);

        mReciver = new MessageBackReciver(mResultText);

        mServiceIntent = new Intent(this, BackService.class);

        mIntentFilter = new IntentFilter();
        mIntentFilter.addAction(BackService.HEART_BEAT_ACTION);
        mIntentFilter.addAction(BackService.MESSAGE_ACTION);

    }

    @Override
    protected void onStart() {
        super.onStart();
        mLocalBroadcastManager.registerReceiver(mReciver, mIntentFilter);
        bindService(mServiceIntent, conn, BIND_AUTO_CREATE);
    }

    @Override
    protected void onStop() {
        super.onStop();
        unbindService(conn);
        mLocalBroadcastManager.unregisterReceiver(mReciver);
    }

    public void onClick(View view) {
        switch (view.getId()) {
        case R.id.send:
            String content = mEditText.getText().toString();
            try {
                boolean isSend = iBackService.sendMessage(content);//Send Content by socket
                Toast.makeText(this, isSend ? "success" : "fail",
                        Toast.LENGTH_SHORT).show();
                mEditText.setText("");
            } catch (RemoteException e) {
                e.printStackTrace();
            }
            break;

        default:
            break;
        }
    }
}

完整项目带服务器段代码的见: http://git.oschina.net/fengcunhan/SocketTest.git

摘录自链接: 
android长连接心跳机制, 2015.10.30 
Android实现推送方式解决方案, 2012.3.4 
Android 网络(五) 推送, 2016.9.14 
XMPP协议实现原理介绍, 2012.3.4 
xmpp协议详解一:xmpp基本概念, 2015.7.30 
MQTT 折腾笔记—-协议简读, 2013.4.25 
MQTT协议(一):理论篇,2016.3.8 
基于TCP的socket编程网络掉线重连,2011 
在Android上面如何使用带有心跳检测的Socket, 2013

再补充几篇文章: 
Android推送技术研究, 2016-3-6 
[包含代码实现]Android产品研发(十二)–>App长连接实现, 2016-6-17 
[包含socket实例]基于Java Socket的自定义协议,实现Android与服务器的长连接(一),2016-12-2 
Android进程保活详解:一篇文章解决你的所有疑问 
移动端IM实践:实现Android版微信的智能心跳机制 
移动端IM实践:WhatsApp、Line、微信的心跳策略分析

posted on 2018-05-28 01:24  曲进笑谈  阅读(7315)  评论(1编辑  收藏  举报