[轉]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、微信的心跳策略分析


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM