心跳包是什么?有什么用?

心跳一般是指客戶端(也可以是服務(wù)器端)向?qū)Ψ矫扛粢欢螘r(shí)間發(fā)送一個(gè)應(yīng)用層的自定義指令,以確保連接的有效性。因?yàn)槭枪潭ㄩg隔,同時(shí)是檢測存活,就像人的心跳一樣,顧名思義,稱為心跳包。一般是用于長連接,對消息實(shí)時(shí)性要求比較高的服務(wù)中,比如IM服務(wù),推送服務(wù)。

長連接有什么用?

在即時(shí)通訊領(lǐng)域和推送服務(wù)中,對消息的實(shí)時(shí)性和可用性要求非常高,建立長連接,可以有效節(jié)省DNS解釋時(shí)間,TCP/IP三次握手時(shí)間,同時(shí)為了保證連接是可用的,不至于經(jīng)常發(fā)了消息對方無法收到,必須要有一種機(jī)制檢測連接的有效性。TCP是一個(gè)基于連接的協(xié)議,連接是由一個(gè)狀態(tài)機(jī)進(jìn)行維護(hù),當(dāng)連接建立成功后,雙方都處于established ,除非我們進(jìn)行主動調(diào)用,否則狀態(tài)一直不會變化,即使中間路由已經(jīng)崩潰,網(wǎng)線已經(jīng)被剪斷。TCP有一種KeepAlive機(jī)制,TCP層在定時(shí)時(shí)間發(fā)送相應(yīng)的KeepAlive探針以確保連接的可用性,默認(rèn)每7200秒發(fā)送一次,超過75秒沒有返回就超時(shí),超時(shí)后重試10次,雖然可以修改默認(rèn)值,但仍然無法滿足要求。尤其是考慮到一種特殊情況,TCP連接存活,但是主機(jī)不處于存活狀態(tài),比如CPU負(fù)載到100%,無法響應(yīng)任何請求。這時(shí)候,就需要客戶端主動切斷連接,主動切換到其他備用機(jī)。

移動端面臨的挑戰(zhàn)

通常,我們一個(gè)家庭里面只接入一根網(wǎng)線,所有設(shè)備通過路由器共用一個(gè)出口IP,路由器就是一個(gè)NAT設(shè)備,NAT設(shè)備在IP封包流過設(shè)備的時(shí)候,自動修改源和目標(biāo)地址,家用路由器甚至基于NAPT修改端口號,路由器內(nèi)部會維護(hù)一個(gè)NAT映射表
比如內(nèi)網(wǎng)里面的172.1.1.2:7777 對應(yīng)外網(wǎng)221.22.2.1:8888等。我們的手機(jī)接入的蜂窩網(wǎng)絡(luò)后,運(yùn)營商就會給我們分配一個(gè)內(nèi)網(wǎng)IP(類似10.2.2.3),由運(yùn)營商的網(wǎng)管維護(hù)一個(gè)NAT的映射表,確保手機(jī)能接入互聯(lián)網(wǎng)。大部分運(yùn)營商會在手機(jī)一段時(shí)間沒有數(shù)據(jù)通訊的時(shí)候,會把設(shè)備從NAT表中剔除,造成了連接中斷,但是對TCP連接的雙方是不可感知的,服務(wù)端就無法給客戶端發(fā)送消息。像中國移動和中國聯(lián)通的NAT超時(shí)時(shí)間是5分鐘,國際上運(yùn)營商普遍都是大于28分鐘。

實(shí)現(xiàn)方案

合理間隔

心跳太短保證不了可靠性,太頻繁會帶來高耗電和大量的流量消耗,這在移動設(shè)備上面是不可接受的。最合理的解決方案是設(shè)定一個(gè)合理的間隔,一般可以根據(jù)程序狀態(tài)進(jìn)行調(diào)整,逐步拉長心跳間隔,5分鐘,10分鐘,甚至15分鐘。服務(wù)端進(jìn)行可靠性判斷的時(shí)候也可以放寬標(biāo)準(zhǔn),只有N次超時(shí)才被認(rèn)為是連接已經(jīng)斷開。心跳的周期以最后一條指令為準(zhǔn),而非固定間隔。

自定義應(yīng)用層實(shí)現(xiàn)協(xié)議

在DEMO中,雙方約定一個(gè)協(xié)議,發(fā)送方先對管道寫入一個(gè)8位的byte值,接收方只要一接收到數(shù)據(jù),馬上按照byte類型標(biāo)準(zhǔn)讀取前8位,通過這一個(gè)字節(jié)的值來確定對方現(xiàn)在發(fā)過來的是什么類型的數(shù)據(jù)。為什么要選擇byte呢?因?yàn)閎yte足夠短,只占用一個(gè)字節(jié),盡量減少數(shù)據(jù)傳輸量,可以通過一個(gè)字節(jié)表達(dá)256種情況。當(dāng)然根據(jù)實(shí)際業(yè)務(wù)需求,選擇int,long類型也是完全沒問題的。
在這個(gè)例子中,我們約定byte的值是1的話,那么我們解釋為心跳包,后面不再有數(shù)據(jù),直接在屏幕中打印收到客戶端的心跳包,byte的值是2的話,我們知道對方要發(fā)一個(gè)字符串過來,那么需要進(jìn)一步處理,再次調(diào)用readUTF方法,讀取一個(gè)UTF-8字符串

下面是一個(gè)用JAVA實(shí)現(xiàn)的心跳包DEMO,主要用了多線程和Socket

服務(wù)端代碼

服務(wù)端建立一個(gè)類,采用同步多線程模式,主類負(fù)責(zé)接收socket請求,子線程Worker類負(fù)責(zé)處理業(yè)務(wù)邏輯

public class Server {    public static void main(String[] args) {        try {
            ServerSocket serverSocket = new ServerSocket(30000); //實(shí)例化ServerSocket,綁定監(jiān)聽本機(jī)的30000端口
            while(true){  
                Socket socket = serverSocket.accept();  //這個(gè)是阻塞方法,只有監(jiān)聽到客戶端連接過來了,才會繼續(xù)往下走。
                System.out.println(socket.getInetAddress().getHostName()+"連接到服務(wù)器...");                //Worker線程啟動代碼
                Worker worker = new Worker(socket);                new Thread(worker).start();
            }
        }catch (Exception e){
            System.out.println("主線程拋出異常");
            e.printStackTrace();
        }
    }
}

Worker線程

class Worker implements Runnable{    private Socket socket;    private InputStream in;    private OutputStream out;    private ObjectInputStream ois;    private boolean flag = true;    public Worker(Socket socket){        try{            this.socket = socket; //要獲得一個(gè)從主線程傳過來的客戶端socket實(shí)例,每個(gè)客戶端都不一樣
            in = socket.getInputStream(); //從客戶端實(shí)例中,獲取輸入流實(shí)例
            out = socket.getOutputStream(); //獲取輸出流實(shí)例
            ois = new ObjectInputStream(in); //實(shí)例化ObjectInputStream
        }catch (Exception e){
            System.out.println("worker構(gòu)造函數(shù)拋出異常");
            e.printStackTrace();
        }
    }    public void run(){        try{            while(flag) {                //協(xié)議的第一位是數(shù)字,先讀取第一位
                int type = ois.readByte();                if(type == 1){                    //第一位是1的話,就直接當(dāng)心跳包處理
                    System.out.println("收到"+socket.getInetAddress().getHostAddress()+"發(fā)送過來的心跳包");
                }else if(type == 2){                    //第一位是2的話,我們可以知道,對方發(fā)過來的是UTF-8格式的String,所以可以調(diào)用readUTF方法繼續(xù)讀取
                    System.out.println(socket.getInetAddress().getHostAddress()+"說:"+ois.readUTF());
                }
            }
        }catch (EOFException e){
            System.out.println("對方已關(guān)閉連接");
            flag = false;
        } catch (IOException ioe) {
            ioe.printStackTrace();
        }finally {            if(socket != null) {                try{
                    socket.close();
                }catch (Exception e){
                    ;
                }
            }
        }
    }
}

客戶端代碼

public class Client {    private static final String host = "127.0.0.1";  //目標(biāo)地址,這里是本機(jī)
    private static final int port = 30000; //目標(biāo)端口

    public static void main(String[] args) {
        Socket socket = new Socket();       try{
           socket.connect(new InetSocketAddress(host,port)); //建立socket連接
           OutputStream out = socket.getOutputStream(); //從socket中獲取讀取流的實(shí)例
           ObjectOutputStream oos = new ObjectOutputStream(out); //實(shí)例化ObjectOutputStream ,用于自定義的傳輸協(xié)議
           BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));  //用來讀取鍵盤輸入,用了緩沖類
           TimeStore timeStore = new TimeStore(); //建立一個(gè)類來存儲數(shù)據(jù)最后發(fā)送時(shí)間

           new Thread(new SendHeartbeat(oos,timeStore)).start(); //啟動心跳業(yè)務(wù)線程

           String line = new String();           //下面幾行代碼用于獲取用戶輸入
           while((line = bufferedReader.readLine()) != null){
               oos.writeByte(2); //要發(fā)送的是自定義協(xié)議的字符串,先寫入一個(gè)2,告訴服務(wù)端,準(zhǔn)備發(fā)送字符串?dāng)?shù)據(jù)
               oos.writeUTF(line); //寫入一個(gè)UTF字符串到流中
               oos.flush();
               timeStore.setLastSendTime(System.currentTimeMillis()); //記錄最后的寫入時(shí)間到時(shí)間存儲類
           }
           oos.close();
           bufferedReader.close();
       }catch (IOException e){
           System.out.println("數(shù)據(jù)寫入IO異常");
       }finally {           try {
               socket.close();
           }catch (IOException e2){
               e2.printStackTrace();
           }
       }
    }
}

發(fā)送心跳的邏輯

心跳專門開一條線程來發(fā)送,這樣不受主線程業(yè)務(wù)的堵塞代碼影響

class SendHeartbeat implements Runnable{    private ObjectOutputStream oos;    private TimeStore timeStore;    public SendHeartbeat(ObjectOutputStream oos,TimeStore timeStore){        this.oos = oos;        this.timeStore = timeStore;
    }    public void run(){        try{            while(true){
                Thread.sleep(1000); //死循環(huán),每秒啟動一次
                 //當(dāng)上次發(fā)送時(shí)間是在10秒或之前,才發(fā)送心跳
                if((System.currentTimeMillis() - timeStore.getLastSendTime()) >= 10*1000){  
                    //寫入1,告訴服務(wù)端發(fā)送的是心跳包
                    oos.writeByte(1);
                    oos.flush();                    //記錄時(shí)間
                    timeStore.setLastSendTime(System.currentTimeMillis());
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

時(shí)間存儲類,用于記錄最后發(fā)送的時(shí)間

class TimeStore{    private long lastSendTime;    
    //多線程下讀取需要加鎖
    public synchronized long getLastSendTime() {        return lastSendTime;
    }    //同樣,多線程下寫入需要加鎖
    public synchronized void setLastSendTime(long lastSendTime) {        this.lastSendTime = lastSendTime; //把時(shí)間放到私有屬性
        System.out.println("最后一次發(fā)包時(shí)間"+ new java.text.SimpleDateFormat("dd/MM/yyyy HH:mm:ss").format(new java.util.Date(lastSendTime))); //把發(fā)包時(shí)間打印到屏幕上
    }
}

效果

seo優(yōu)化培訓(xùn),網(wǎng)絡(luò)推廣培訓(xùn),網(wǎng)絡(luò)營銷培訓(xùn),SEM培訓(xùn),網(wǎng)絡(luò)優(yōu)化,在線營銷培訓(xùn)

心跳包的挑戰(zhàn):信令風(fēng)暴

2013年,中國移動曾把刀口指向了微信,正是因?yàn)樾奶赡軙鸬男帕铒L(fēng)暴,微信占用了中移動60%的信令資源,但僅帶來10%的移動數(shù)據(jù)流量。每次發(fā)送心跳包,都需要移動通信網(wǎng)絡(luò)為用戶分配資源,分配的過程體現(xiàn)在信令的發(fā)送和接收上。一次心跳包的發(fā)送過程,牽涉的信令多達(dá)幾十條。后來微信對心跳間隔進(jìn)行了優(yōu)化才暫時(shí)平息了這場風(fēng)波。微信采用的方案是當(dāng)微信處于前臺活躍狀態(tài)時(shí),使用固定心跳。微信進(jìn)入后臺(或者前臺關(guān)屏)時(shí),先用幾次最小心跳維持長鏈接。然后進(jìn)入后臺自適應(yīng)心跳計(jì)算。這樣做的目的是盡量選擇用戶不活躍的時(shí)間段,來減少心跳計(jì)算可能產(chǎn)生的消息不及時(shí)收取影響。詳看微信心跳包優(yōu)化方案

http://www.cnblogs.com/jaychan/p/7168869.html