心跳機制是定時發送一個自定義的結構體(心跳包),讓對方知道自己還活着,以確保連接的有效性的機制。
應用場景:
在長連接下,有可能很長一段時間都沒有數據往來。理論上說,這個連接是一直保持連接的,但是實際情況中,如果中間節點出現什麼故障是難以知道的。更要命的是,有的節點(防火牆)會自動把一定時間之內沒有數據交互的連接給斷掉。在這個時候,就需要我們的心跳包了,用於維持長連接,保活
什麼是心跳機制?
就是每隔幾分鐘發送一個固定信息給服務端,服務端收到後回覆一個固定信息如果服務端幾分鐘內沒有收到客戶端信息則視客戶端斷開。
發包方:可以是客戶也可以是服務端,看哪邊實現方便合理。 心跳包之所以叫心跳包是因爲:它像心跳一樣每隔固定時間發一次,以此來告訴服務器,這個客戶端還活着。事實上這是爲了保持長連接,至於這個包的內容,是沒有什麼特別規定的,不過一般都是很小的包,或者只包含包頭的一個空包。心跳包主要也就是用於長連接的保活和斷線處理。一般的應用下,判定時間在30-40秒比較不錯。如果實在要求高,那就在6-9秒。
心跳包的發送,通常有兩種技術:
1.應用層自己實現的心跳包
由應用程序自己發送心跳包來檢測連接是否正常,服務器每隔一定時間向客戶端發送一個短小的數據包,然後啓動一個線程,在線程中不斷檢測客戶端的迴應, 如果在一定時間內沒有收到客戶端的迴應,即認爲客戶端已經掉線;同樣,如果客戶端在一定時間內沒有收到服務器的心跳包,則認爲連接不可用。
2.使用SO_KEEPALIVE套接字選項
在TCP的機制裏面,本身是存在有心跳包的機制的,也就是TCP的選項. 不論是服務端還是客戶端,一方開啓KeepAlive功能後,就會自動在規定時間內向對方發送心跳包, 而另一方在收到心跳包後就會自動回覆,以告訴對方我仍然在線。因爲開啓KeepAlive功能需要消耗額外的寬帶和流量,所以TCP協議層默認並不開啓默認的KeepAlive超時需要7,200,000 MilliSeconds, 即2小時,探測次數爲5次。對於很多服務端應用程序來說,2小時的空閒時間太長。因此,我們需要手工開啓KeepAlive功能並設置合理的KeepAlive參數
開啓KeepAlive選項後會導致的三種情況:
1、對方接收一切正常:以期望的ACK響應,2小時後,TCP將發出另一個探測分節
2、對方已崩潰且已重新啓動:以RST響應。套接口的待處理錯誤被置爲ECONNRESET,套接口本身則被關閉。
3、對方無任何響應:套接口的待處理錯誤被置爲ETIMEOUT,套接口本身則被關閉.
有關SO_KEEPALIVE的三個參數:
1.tcp_keepalive_intvl,保活探測消息的
發送頻率
。默認值爲75s。
發送頻率tcp_keepalive_intvl乘以發送次數tcp_keepalive_probes,就得到了從開始探測直到放棄探測確定連接斷開的時間,大約爲11min。
2.tcp_keepalive_probes,TCP發送保活探測消息以確定連接是否已斷開的
次數
。默認值爲9(次)。
3.tcp_keepalive_time,在TCP保活打開的情況下,最後一次數據交換到TCP發送第一個保活探測消息的時間,即允許的持續
空閒時間
。默認值爲7200s(2h)。
總結:
一個服務器通常會連接多個客戶端,因此由用戶在應用層自己實現心跳包,代碼較多 且稍顯複雜。用TCP/IP協議層爲內置的KeepAlive功能來實現心跳功能則簡單得多。心跳包在按流量計費的環境下增加了費用.但TCP得在連接閒置2小時後才發送一個保持存活探測段,所以通常的方法是將保持存活參數改小,但這些參數按照內核去維護,而不是按照每個套接字維護,因此改動它們會影響所有開啓該選項的套接字。
下面我們通過一個實例來展示心跳機制。
結構,一個客戶程序,和一個服務程序。
步驟:
服務器:
1.經過socket、bind、listen、後用accept獲取一個客戶的連接請求,爲了簡單直觀,這裏服務器程序只接收一個connect請求,我們用clifd來獲取唯一的一個連接。
2.爲clifd修改KeepAlive的相關參數,並開啓KeepAlive套接字選項,這裏我們把間隔時間設爲了5秒,閒置時間設置了5秒,探測次數設置爲5次。
3.將clifd加入select監聽的描述符號集
客戶:很簡單,只是連接上去,並停留在while死循環。
方式:
服務程序放到阿里雲服務器上,我們執行服務程序並將輸出結果重定向到一個日誌文件,目的是爲了將我們本地網絡連接斷開後,超過了keepalive閒置時間+重複發包探測的時間後,重新打開本地的網絡連接,並登錄服務器,通過該日誌文件的內容來查看程序的打印結果。
1 #include <iostream> 2 #include <sys/types.h> 3 #include <sys/socket.h> 4 #include <stdlib.h> 5 #include <strings.h> 6 #include <stdio.h> 7 #include <netinet/in.h> 8 #include <arpa/inet.h> 9 #include <errno.h> 10 #include <unistd.h> 11 using namespace std; 12 13 int main() 14 { 15 int skfd; 16 if ((skfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { 17 perror(""); 18 exit(-1); 19 } 20 21 struct sockaddr_in saddr; 22 bzero(&saddr, sizeof(saddr)); 23 saddr.sin_family = AF_INET; 24 saddr.sin_port = htons(9999); 25 saddr.sin_addr.s_addr = inet_addr("115.29.109.198"); 26 27 if (connect(skfd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0) { 28 perror(""); 29 exit(-1); 30 } 31 32 cout << "連接成功" << endl; 33 while(1); 34 return 0; 35 } 36 服務器 37 38 #include <iostream> 39 #include <sys/types.h> 40 #include <sys/socket.h> 41 #include <stdlib.h> 42 #include <strings.h> 43 #include <stdio.h> 44 #include <netinet/in.h> 45 #include <arpa/inet.h> 46 #include <errno.h> 47 #include <unistd.h> 48 #include <sys/select.h> 49 #include <netinet/tcp.h> 50 using namespace std; 51 52 #define LISTENNUM 5 53 54 int main() 55 { 56 int skfd; 57 if ((skfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { 58 perror(""); 59 exit(-1); 60 } 61 62 struct sockaddr_in saddr; 63 bzero(&saddr, sizeof(saddr)); 64 saddr.sin_family = AF_INET; 65 saddr.sin_port = htons(9999); 66 saddr.sin_addr.s_addr = inet_addr("115.29.109.198"); 67 68 if (bind(skfd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0) { 69 perror(""); 70 exit(-1); 71 } 72 73 if (listen(skfd, LISTENNUM) < 0) { 74 perror(""); 75 exit(-1); 76 } 77 78 int clifd; 79 if ((clifd = accept(skfd, NULL, NULL)) < 0) { 80 perror(""); 81 exit(-1); 82 } 83 cout << "有新連接" << endl; 84 85 //setsockopt 86 int tcp_keepalive_intvl = 5; //保活探測消息的發送頻率。默認值爲75s 87 int tcp_keepalive_probes = 5; //TCP發送保活探測消息以確定連接是否已斷開的次數。默認值爲9次 88 int tcp_keepalive_time = 5; //允許的持續空閒時間。默認值爲7200s(2h) 89 int tcp_keepalive_on = 1; 90 91 if (setsockopt(clifd, SOL_TCP, TCP_KEEPINTVL, 92 &tcp_keepalive_intvl, sizeof(tcp_keepalive_intvl)) < 0) { 93 perror(""); 94 exit(-1); 95 } 96 97 if (setsockopt(clifd, SOL_TCP, TCP_KEEPCNT, 98 &tcp_keepalive_probes, sizeof(tcp_keepalive_probes)) < 0) { 99 perror(""); 100 exit(-1); 101 } 102 103 if (setsockopt(clifd, SOL_TCP, TCP_KEEPIDLE, 104 &tcp_keepalive_time, sizeof(tcp_keepalive_time)) < 0) { 105 perror(""); 106 exit(-1); 107 } 108 109 if (setsockopt(clifd, SOL_SOCKET, SO_KEEPALIVE, 110 &tcp_keepalive_on, sizeof(tcp_keepalive_on))) { 111 perror(""); 112 exit(-1); 113 } 114 115 char buf[1025]; 116 int r; 117 int maxfd; 118 fd_set rset; 119 FD_ZERO(&rset); 120 sleep(5); 121 while (1) { 122 FD_SET(clifd, &rset); 123 maxfd = clifd + 1; 124 if (select(maxfd, &rset, NULL, NULL, NULL) < 0) { 125 perror(""); 126 exit(-1); 127 } 128 129 if (FD_ISSET(clifd, &rset)) { 130 r = read(clifd, buf, sizeof(buf)); 131 if (r == 0) { 132 cout << "接收到FIN" << endl; 133 close(clifd); 134 break; 135 } 136 else if (r == -1) { 137 if (errno == EINTR) { 138 cout << "errno: EINTR" << endl; 139 continue; 140 } 141 142 if (errno == ECONNRESET) { 143 cout << "errno: ECONNRESET" << endl; 144 cout << "對端已崩潰且已重新啓動" << endl; 145 close(clifd); 146 break; 147 } 148 149 if (errno == ETIMEDOUT) { 150 cout << "errno: ETIMEDOUT" << endl; 151 cout << "對端主機崩潰" << endl; 152 close(clifd); 153 break; 154 } 155 156 if (errno == EHOSTUNREACH) { 157 cout << "errno: EHOSTUNREACH" << endl; 158 cout << "對端主機不可達" << endl; 159 close(clifd); 160 break; 161 } 162 } 163 } 164 } 165 166 close(skfd); 167 return 0; 168 }
執行服務程序並重定向到日誌文件server.log,執行客戶程序,之後將網絡連接斷開
一段時間後(大於KeepAlive空閒時間+重複探測時間),重新打開網絡連接,用ssh登錄服務器,查看server.log文件.發現打印了ETIMEDOUT
,驗證了在客戶網絡斷開後,到達空閒時間時,服務器由於開啓了KeepAlive選項,會向客戶端發送探測包,幾次還沒收到客戶端的迴應,那麼select將返回套接字可讀的條件,並且read返回-1.設置相關錯誤,
而與之相反的情況是如果不開啓KeelAlive選項,那麼即使客戶端網絡斷開超過了整個的空閒和探測時間,服務端的select也不會返回可讀的條件,即應用程序無法得到通知。