1. 程式人生 > >android長連線心跳機制

android長連線心跳機制

在寫之前,我們首先了解一下為什麼android維護長連線需要心跳機制,首先我們知道,維護任何一個長連線都需要心跳機制,客戶端傳送一個心跳給

伺服器,伺服器給客戶端一個心跳應答,這樣就形成客戶端伺服器的一次完整的握手,這個握手是讓雙方都知道他們之間的連線是沒有斷開,客戶端是線上

的。如果超過一個時間的閾值,客戶端沒有收到伺服器的應答,或者伺服器沒有收到客戶端的心跳,那麼對客戶端來說則斷開與伺服器的連線重新建立一個

連線,對伺服器來說只要斷開這個連線即可。那麼在智慧手機上的長連線心跳和在Internet上的長連線心跳有什麼不同的目的呢?原因就在於智慧手機使用的

是移動無線網路,那麼我們在講長連線之前我們首先要了解無線行動網路的特點。

1.無線行動網路的特點:

        當一臺智慧手機連上行動網路時,其實並沒有真正連線上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.安全,只有在蘋果註冊的開發者才能夠進行推送,等等。

       android的長連線是由每個應用各自維護的,但是google也推出了和蘋果技術架構相似的推送框架,C2DM,雲端推送功能,但是由於google的伺服器不在中國境內,其他的原因你懂的。所以導致這個推送無法使用,android的開發者不得不自己去維護一個長連結,於是每個應用如果都24小時線上,那麼都得各自維護一個長連線,這種電量和流量的消耗是可想而知的。雖然國內也出現了各種推送平臺,但是都無法達到只維護一個長連線這種消耗的級別。

3.推送的實現方式:

一:客戶端不斷的查詢伺服器,檢索新內容,也就是所謂的pull 或者輪詢方式

二:客戶端和伺服器之間維持一個TCP/IP長連線,伺服器向客戶端push

三:伺服器又新內容時,傳送一條類似簡訊的信令給客戶端,客戶端收到後從伺服器中下載新內容,也就是SMS的推送方式

蘋果的推送系統和googleC2DM推送系統其實都是在系統級別維護一個TCP/IP長連線,都是基於第二種的方式進行推送的。第三種方式由於運營商沒有免費開放這種信令導致了這種推送在成本上是無法接受的,雖然這種推送的方式非常的穩定,高效和及時。

如果想了解android中各種推送方式請參考這個連結:Android實現推送方式解決方案 這篇部落格已經介紹的非常好了。

所謂的心跳包就是客戶端定時放送簡單的資訊給伺服器端,告訴它我還在而已。程式碼就是每隔幾分鐘傳送一個固定資訊給伺服器端,伺服器端回覆一個固定資訊。如果伺服器端幾分鐘後沒有收到客戶端資訊則視客戶端斷開。比如有些通訊軟體長時間不適用,要想知道它的狀態是線上還是離線,就需要心跳包,定時發包收包。

心跳包之所以叫心跳包是因為:它像心跳一樣每隔固定時間發一次,以此來告訴伺服器,這個客戶端還活在。事實上這是為了保持長連線,至於這個包的內容,是沒有什麼特別規定的,不過一般都是很小的包,或者只包含包頭的一個空包。

在TCP機制裡面,本身是存在有心跳包機制的,也就是TCP選項:SO_KEEPALIVE. 系統預設是設定的2小時的心跳頻率。


Socket長連線+心跳檢測:

http://blog.csdn.net/zh724738989/article/details/42007099

心跳包的機制,其實就是傳統的長連線。或許有的人知道訊息推送的機制,訊息推送也是一種長連線 ,是將資料有伺服器端推送到客戶端這邊從而改變傳統的“拉”的請求方式。下面我來介紹一下安卓和客戶端兩個資料請求的方式

       1、push  這個也就是有伺服器推送到客戶端這邊  現在有第三方技術 比如極光推送。

       2、pull   這種方式就是客戶端向伺服器傳送請求資料(http請求)

一、首先伺服器和客戶端有一次“握手”

public void connect()
      {
        LogUtil.e(TAG, "準備連結...");
        InetAddress serverAddr;
        try {
            socket = new Socket(Config.Host, Config.SockectPort);
            _connect = true;
            mReceiveThread = new ReceiveThread();
            receiveStop = false;
            mReceiveThread.start();
            LogUtil.e(TAG, "連結成功.");

        } catch (Exception e) {
            LogUtil.e(TAG, "連結出錯." + e.getMessage().toString());
            e.printStackTrace();
        }
    }
二、下面就要開啟一個執行緒  去不斷讀取伺服器那邊傳過來的資料  採用Thread去實現
private class ReceiveThread extends Thread {
        private byte[] buf;
        private String str = null;

        @Override
        public void run() {
            while (true) {
                try {
                    // LogUtil.e(TAG, "監聽中...:"+socket.isConnected());
                    if (socket!=null && socket.isConnected()) {

                        if (!socket.isInputShutdown()) {
                            BufferedReader inStream = new BufferedReader(
                                    new InputStreamReader(
                                            socket.getInputStream()));
                            String content = inStream.readLine();                            
                            if (content == null)
                                continue;
                            LogUtil.e(TAG, "收到資訊:" + content);
                            LogUtil.e(TAG, "資訊長度:"+content.length());
                            if (!content.startsWith("CMD:"))
                                continue;
                            int spacePos = content.indexOf(" ");
                            if (spacePos == -1)
                                continue;
                            String cmd = content.substring(4, spacePos);
//                            String body = StringUtil.DecodeBase64(content
//                                    .substring(spacePos));
                            String body = content.substring(spacePos).trim();
                            LogUtil.e(TAG, "收到資訊(CMD):" + cmd);
                            LogUtil.e(TAG, "收到資訊(BODY):" + body);
                            if (cmd.equals("LOGIN"))
                           {
                                // 登入
                                ReceiveLogin(body);
                                continue;
                            }
                              if (cmd.equals("KEEPLIVE")) {
                                if (!body.equals("1")) {
                                    Log.e(TAG, "心跳時檢測到異常,重新登入!");
                                    socket = null;
                                    KeepAlive();
                                } else {
                                    Date now = Calendar.getInstance().getTime();
                                    lastKeepAliveOkTime = now;
                                }
                                continue;
                            }
                        }
                    } else {
                        if(socket!=null)
                            LogUtil.e(TAG, "連結狀態:" + socket.isConnected());
                    }

                } catch (Exception e) {
                    LogUtil.e(TAG, "監聽出錯:" + e.toString());
                    e.printStackTrace();
                }
            }
        }

三 、 Socket 是否斷開了  斷開了 需要重新去連線
public void KeepAlive()
        {
        // 判斷socket是否已斷開,斷開就重連
        if (lastKeepAliveOkTime != null) {
            LogUtil.e(
                    TAG,
                    "上次心跳成功時間:"
                            + DateTimeUtil.dateFormat(lastKeepAliveOkTime,
                                    "yyyy-MM-dd HH:mm:ss"));
            Date now = Calendar.getInstance().getTime();
            long between = (now.getTime() - lastKeepAliveOkTime.getTime());// 得到兩者的毫秒數
            if (between > 60 * 1000) {
                LogUtil.e(TAG, "心跳異常超過1分鐘,重新連線:");
                lastKeepAliveOkTime = null;
                socket = null;
            }

        } else {
            lastKeepAliveOkTime = Calendar.getInstance().getTime();
        }

        if (!checkIsAlive()) {
            LogUtil.e(TAG, "連結已斷開,重新連線.");
            connect();
            if (loginPara != null)
                Login(loginPara);
        }

    //此方法是檢測是否連線
      boolean checkIsAlive() {
        if (socket == null)
            return false;
        try {
            socket.sendUrgentData(0xFF);
        } catch (IOException e) {
            return false;
        }
        return true;

    }
   //然後傳送資料的方法
    public void sendmessage(String msg) {
        if (!checkIsAlive())
            return;
        LogUtil.e(TAG, "準備傳送訊息:" + msg);
        try {
            if (socket != null && socket.isConnected()) {
                if (!socket.isOutputShutdown()) {
                    PrintWriter outStream = new PrintWriter(new BufferedWriter(
                            new OutputStreamWriter(socket.getOutputStream())),
                            true);

                    outStream.print(msg + (char) 13 + (char) 10);
                    outStream.flush();
                }
            }
            LogUtil.e(TAG, "傳送成功!");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }



最近做專案用到心跳輪詢到主動到伺服器取訊息,為了做推送。坑了個爹的,極光百度推送限制多不能滿足需求,只能自己寫…手機主動到Service取資料,也就意味著你的手機要有一個服務,一直在後臺執行,在特定的時間去伺服器詢問有沒有訊息,如果有訊息則取回客戶端。 
當然還可以用像什麼XMPP(當然為了一個訊息推送,動用那麼大而又笨重的東西,很明顯不明智),簡訊通知等等一下方式。

這裡主要講在android主動取資料: 
其實實現後臺推送訊息給客戶端可以分為主動取,和主動推兩種。

主動取:就是我們上面說的輪詢伺服器取訊息。 
主動推:伺服器推送訊息給客戶端,這裡必須客戶端和伺服器保持長連線。 
兩種形式各有利弊,“主動取”不能保證訊息的實時性;“主動推”能保證訊息的實時性,但是不能保證android端的這個連結不會被kill掉。

實現輪詢

  • 原理 
    其原理在於在android端的程式中,讓一個SERVICE一直跑在後臺,在規定時間之內呼叫伺服器介面進行資料獲取。

    這裡的原理很簡單,當然實現起來也不難;

    然後,這個類之中肯定要做網路了資料請求,所以我們在Service中建立一個執行緒(因為在android系統中網路請求屬於長時間操作,不能放主執行緒,不然會導致異常),線上程中和伺服器進行通訊。

    最後,這個邏輯寫完後,我們需要考慮一個問題,如何進行在規定時間內呼叫該伺服器,當然可以用Thread+Handler(這個不是那麼穩定),也可以使用AlamManager+Thread(比較穩定),因為我們需要其在後臺一直執行,所以可以依靠系統的Alammanager這個類來實現,Alammanager是屬於系統的一個鬧鐘提醒類,通過它我們能實現在規定間隔時間呼叫,並且也比較穩定,這個service被殺後會自己自動啟動服務。


出於最近對im研究的興趣,看到smack裡有個30s傳送一個空訊息的執行緒,瞭解了下關於心跳包,keepalive的知識。 TCP的socket本身就是長連線的,那麼為什麼還要心跳包呢?

搜尋到的資料解釋如下:

一:內網機器如果不主動向外發起連線,外網機沒法直連內網的,這也是內網機安全的原因之一吧,又因為路由器會把這個關係記錄下來,但是過一段時間這個記錄可能會丟失 ,所有每一個客戶端每隔一定時間就會向伺服器傳送訊息,以保證伺服器可以隨時找到你,這東西被稱為心跳包。

二:理論上說,這個連線是一直保持連線的,但是實際情況中,如果中間節點出現什麼故障是難以知道的。更要命的是,有的節點(防火牆)會自動把一定時間之內沒有資料互動的連線給斷掉。在這個時候,就需要我們的心跳包了,用於維持長連線,保活。在獲知了斷線之後,伺服器邏輯可能需要做一些事情,比如斷線後的資料清理呀,重新連線呀……當然,這個自然是要由邏輯層根據需求去做了。總的來說,心跳包主要也就是用於長連線的保活和斷線處理。一般的應用下,判定時間在30-40秒比較不錯。如果實在要求高,那就在6-9秒。

三:

<span style="font-size: 18px;"><span style="color:#333333;">如果不主動關閉socket的話,系統不會自動關閉的,除非當前程序掛掉了,作業系統把佔用的socket回收了才會關閉。為什麼需要心跳連線主要是判斷當前連線是否是有效的、可被使用的。在實際應用中假設一段時間沒有資料傳輸時候理論上說應該連線是沒有問題的,但是網路複雜,中途出現問題也是常見的,網線被掐斷了、對方程序掛掉了、頻繁丟包等,這時候TCP連線是不可使用的,但是對於應用層並不知道,如果需知道網路情況則要很複雜的超時進行了解,TCP從底層就實現了這樣的功能。</span><span style="color:#009900;"><strong>心跳機制</strong></span><span style="color:#333333;">是TCP在一段時間間隔後傳送確定連線端是否還存在,如果存在的話就會回傳一個包確定網路有效,如果心跳包有問題,則通知上層應用當前網路有問題了。</span></span>
<span style="font-size: 18px;"><span style="font-family: arial, 宋體, sans-serif; line-height: 24px;"></span></span><pre class="reply-text mb10" id="content-886462114" name="code" style="white-space: pre-wrap; word-wrap: break-word; margin-top: 0px; margin-bottom: 10px; padding: 0px; font-family: Arial; zoom: 1;"><span style="font-size: 18px;">這取決於你的server端的超時配置, 每個socket連線都是長連線,它是一個相當佔用系統資源的通訊管道, 如果這個長連線什麼事也沒幹硬是要佔著資源,則server端可以選擇關閉這個連線,以省下資源讓更多的使用者連線進來。
所以,即便客戶端的是採用死迴圈while(true)方式連到服務端,對於特定的客戶端和服務端型別來說也需要一定時間間隔的心跳(告訴服務端,我還活著,雖然我沒幹活也沒說話,但別把我關了)。</span>
<span style="font-size: 18px;">記得以前開發手機遊戲時,索愛有一款手機有強制要求,客戶端如果超過三分鐘無訊息發向網路服務端,則會在客戶端自動地強制把socket關斷。因為socket長連線相對於手機這樣資源少的裝置來說是寶貴的資源。  (這個強制是指客戶端系統自動關的,不是我們程式碼close的) </span>

這3個原因都是挺有道理。如果你有更好的解釋歡迎交流啊。改天我做個程式測試一下不發心跳包和發心跳包的連線情況。

來源:1.http://blog.csdn.net/qgjava/article/details/5745776

2.http://topic.csdn.net/u/20081009/13/abd12947-e78e-43ba-9a43-ce690ecb8ac2.html

3.http://zhidao.baidu.com/question/349886234.html