轉(zhuǎn)載自:https://blog.csdn.net/qq_23167527/article/details/54290726
跳包之所以叫心跳包是因為:它像心跳一樣每隔固定時間發(fā)一次,以此來告訴服務(wù)器,這個客戶端還活著。事實上這是為了保持長連接,至于這個包的內(nèi)容,是沒有什么特別規(guī)定的,不過一般都是很小的包,或者只包含包頭的一個空包。
在TCP的機制里面,本身是存在有心跳包的機制的,也就是TCP的選項:SO_KEEPALIVE。系統(tǒng)默認是設(shè)置的2小時的心跳頻率。但是它檢查不到機器斷電、網(wǎng)線拔出、防火墻這些斷線。而且邏輯層處理斷線可能也不是那么好處理。一般,如果只是用于?;钸€是可以的。
心跳包一般來說都是在邏輯層發(fā)送空的echo包來實現(xiàn)的。下一個定時器,在一定時間間隔下發(fā)送一個空包給客戶端,然后客戶端反饋一個同樣的空包回來,服務(wù)器如果在一定時間內(nèi)收不到客戶端發(fā)送過來的反饋包,那就只有認定說掉線了。
其實,要判定掉線,只需要send或者recv一下,如果結(jié)果為零,則為掉線。但是,在長連接下,有可能很長一段時間都沒有數(shù)據(jù)往來。理論上說,這個連接是一直保持連接的,但是實際情況中,如果中間節(jié)點出現(xiàn)什么故障是難以知道的。更要命的是,有的節(jié)點(防火墻)會自動把一定時間之內(nèi)沒有數(shù)據(jù)交互的連接給斷掉。在這個時候,就需要我們的心跳包了,用于維持長連接,?;睢?br>
在獲知了斷線之后,服務(wù)器邏輯可能需要做一些事情,比如斷線后的數(shù)據(jù)清理呀,重新連接呀……當然,這個自然是要由邏輯層根據(jù)需求去做了。
總的來說,心跳包主要也就是用于長連接的?;詈蛿嗑€處理。一般的應(yīng)用下,判定時間在30-40秒比較不錯。如果實在要求高,那就在6-9秒。
心跳檢測步驟:
1 客戶端每隔一個時間間隔發(fā)生一個探測包給服務(wù)器
2 客戶端發(fā)包時啟動一個超時定時器
3 服務(wù)器端接收到檢測包,應(yīng)該回應(yīng)一個包
4 如果客戶機收到服務(wù)器的應(yīng)答包,則說明服務(wù)器正常,刪除超時定時器
5 如果客戶端的超時定時器超時,依然沒有收到應(yīng)答包,則說明服務(wù)器掛了
很多人會用boolean socketFlag = socket.isConnected() && socket.isClosed()來判斷就行了,但事實上這些方法都是訪問socket在內(nèi)存駐留的狀態(tài),當socket和服務(wù)器端建立鏈接后,即使socket鏈接斷掉了,調(diào)用上面的方法返回的仍然是鏈接時的狀態(tài),而不是socket的實時鏈接狀態(tài),所以這樣心跳用這個不靠譜,下面給出例子證明這一點。
服務(wù)器端:
package com.csc.server;
import java.net.*;
/**
* @description 從這里啟動一個服務(wù)端監(jiān)聽某個端口
* @author csc
*/
public class DstService {
public static void main(String[] args) {
try {
// 啟動監(jiān)聽端口 30000
ServerSocket ss = new ServerSocket(30000);
// 沒有連接這個方法就一直堵塞
Socket s = ss.accept();
// 將請求指定一個線程去執(zhí)行
new Thread(new DstServiceImpl(s)).start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
這里我設(shè)置了啟動新線程來管理建立的每一個socket鏈接,此處我們設(shè)置收到鏈接后10秒端來鏈接,代碼如下:
package com.csc.server;
import java.net.Socket;
/**
* @description 服務(wù)的啟動的線程類
* @author csc
*/
public class DstServiceImpl implements Runnable {
Socket socket = null;
public DstServiceImpl(Socket s) {
this.socket = s;
}
public void run() {
try {
int index = 1;
while (true) {
// 5秒后中斷連接
if (index > 10) {
socket.close();
System.out.println("服務(wù)端已經(jīng)關(guān)閉鏈接!");
break;
}
index++;
Thread.sleep(1 * 1000);//程序睡眠1秒鐘
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
以上是服務(wù)端代碼,下面寫一個客戶端代碼來測試:
package com.csc.client;
import java.net.*;
/**
* @description 客戶端打印鏈接狀態(tài)
* @author csc
*/
public class DstClient {
public static void main(String[] args) {
try {
Socket socket = new Socket("127.0.0.1", 30000);
socket.setKeepAlive(true);
socket.setSoTimeout(10);
while (true) {
System.out.println(socket.isBound());
System.out.println(socket.isClosed());
System.out.println(socket.isConnected());
System.out.println(socket.isInputShutdown());
System.out.println(socket.isOutputShutdown());
System.out.println("------------我是分割線------------");
Thread.sleep(3 * 1000);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
先運行服務(wù)端代碼,再運行客戶端代碼,我們會在客戶端代碼的控制臺看到如下信息:
true
false
true
false
false
------------我是分割線------------
從連接對象的屬性信息來看,連接是沒有中斷,但實際鏈接已經(jīng)在服務(wù)端建立鏈接10秒后斷開了。這說明了上述幾個方法是不能實時判斷出socket的鏈接狀態(tài),只是socket駐留在內(nèi)存的狀態(tài)。其實,此時如果調(diào)用流去讀取信息的話,就會出現(xiàn)異常。
其實,想要判斷socket是否仍是鏈接狀態(tài),只要發(fā)一個心跳包就行了,如下一句代碼:
socket.sendUrgentData(0xFF); // 發(fā)送心跳包
關(guān)于心跳包的理論可以去google一下,我給出點參考:心跳包就是在客戶端和服務(wù)器間定時通知對方自己狀態(tài)的一個自己定義的命令字,按照一定的時間間隔發(fā)送,類似于心跳,所以叫做心跳包。 用來判斷對方(設(shè)備,進程或其它網(wǎng)元)是否正常運行,采用定時發(fā)送簡單的通訊包,如果在指定時間段內(nèi)未收到對方響應(yīng),則判斷對方已經(jīng)離線。用于檢測TCP的異常斷開?;驹蚴欠?wù)器端不能有效的判斷客戶端是否在線,也就是說,服務(wù)器無法區(qū)分客戶端是長時間在空閑,還是已經(jīng)掉線的情況。所謂的心跳包就是客戶端定時發(fā)送簡單的信息給服務(wù)器端告訴它我還在而已。代碼就是每隔幾分鐘發(fā)送一個固定信息給服務(wù)端,服務(wù)端收到后回復(fù)一個固定信息如果服務(wù)端幾分鐘內(nèi)沒有收到客戶端信息則視客戶端斷開。 比如有些通信軟件長時間不使用,要想知道它的狀態(tài)是在線還是離線就需要心跳包,定時發(fā)包收包。發(fā)包方:可以是客戶也可以是服務(wù)端,看哪邊實現(xiàn)方便合理,一般是客戶端。服務(wù)器也可以定時發(fā)心跳下去。一般來說,出于效率的考慮,是由客戶端主動向服務(wù)器端發(fā)包,而不是服務(wù)器向客戶端發(fā)??蛻舳嗣扛粢欢螘r間發(fā)一個包,使用TCP的,用send發(fā),使用UDP的,用sendto發(fā),服務(wù)器收到后,就知道當前客戶端還處于“活著”的狀態(tài),否則,如果隔一定時間未收到這樣的包,則服務(wù)器認為客戶端已經(jīng)斷開,進行相應(yīng)的客戶端斷開邏輯處理!
既然找到了方法,我們就在測試一下,服務(wù)端代碼無需改動,客戶端代碼如下:
package com.csc.client;
import java.net.*;
/**
* @description 客戶端打印鏈接狀態(tài)
* @author csc
*/
public class DstClient {
public static void main(String[] args) {
try {
Socket socket = new Socket("127.0.0.1", 30000);
socket.setKeepAlive(true);
socket.setSoTimeout(10);
while (true) {
socket.sendUrgentData(0xFF); // 發(fā)送心跳包
System.out.println("目前處于鏈接狀態(tài)!");
Thread.sleep(3 * 1000);//線程睡眠3秒
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

服務(wù)端程序運行10秒后再當執(zhí)行“socket.sendUrgentData(0xFF);”語句時,socket鏈接斷開了,所以會拋出異常。
另外注意,心跳包只是用來檢測socket的鏈接狀態(tài),并不會作為socket鏈接的通信內(nèi)容,這點應(yīng)當注意。