1. 程式人生 > >心跳機制tcp keepalive的討論、應用及“斷網”、"斷電"檢測的C代碼實現(Windows環境下)

心跳機制tcp keepalive的討論、應用及“斷網”、"斷電"檢測的C代碼實現(Windows環境下)

astar har 心跳 存在 假設 clu ali clean struct

版權聲明:本文為博主原創文章,轉載時請務必註明本文地址, 禁止用於任何商業用途, 否則會用法律維權。 https://blog.csdn.net/stpeace/article/details/44162349

說明: 1. 本文的討論和實驗都以Windows為例, 其實在linux上也大同小異。

2. 在第一次寫此博文時, 我對某些地方有一些誤解, 現予以更正, 對文章結構做了較大調整,也歡迎大家提出質疑。

3. 在做實驗玩代碼的時候, 意料之中地發現騰訊QQ也在玩心跳, 不清楚具體怎麽實現的, 但有點意思哈技術分享圖片

很多網友都問過一個類似這樣的問題: tcp連接ok後,網絡如果斷了, 怎麽檢測斷網技術分享圖片對於這個問題, 我曾經給出了一個比較武斷的定論: 我說, 斷網斷電後, tcp是死連接, 客戶端和服務端無法感知,必須借助心跳機制。後來, 經過了更多的詳細實驗和深入思考, 我發現,事實並非完全如此。

tcp通道建立後, 如果斷網斷電, 兩側是否會有感知呢? 其實, 這個問題取決於我們的網絡結構, 下面, 我以如下網絡結構為例進行詳細說明。 網絡結構為:

技術分享圖片

先說說這幅圖, 總體來說, 應該還算比較性感技術分享圖片。 其中, pc1做客戶端, ip地址是192.168.1.101, pc2做服務端, ip地址是192.168.1.102, 都是dhcp接入的. 請註意: 在做實驗的過程中, 每次實驗後, 都要關閉服務端和客戶端, 且要回復拆掉的線, 斷掉的電, 免得影響下次做實驗。

確保網絡連接良好, 我們來看pc2服務端程序:

  1. #include <stdio.h>
  2. #include <winsock2.h> // winsock接口
  3. #pragma comment(lib, "ws2_32.lib") // winsock實現
  4. int main()
  5. {
  6. WORD wVersionRequested; // 雙字節,winsock庫的版本
  7. WSADATA wsaData; // winsock庫版本的相關信息
  8. wVersionRequested = MAKEWORD(1, 1); // 0x0101 即:257
  9. // 加載winsock庫並確定winsock版本,系統會把數據填入wsaData中
  10. WSAStartup( wVersionRequested, &wsaData );
  11. // AF_INET 表示采用TCP/IP協議族
  12. // SOCK_STREAM 表示采用TCP協議
  13. // 0是通常的默認情況
  14. unsigned int sockSrv = socket(AF_INET, SOCK_STREAM, 0);
  15. SOCKADDR_IN addrSrv;
  16. addrSrv.sin_family = AF_INET; // TCP/IP協議族
  17. addrSrv.sin_addr.S_un.S_addr = inet_addr("0.0.0.0"); // socket對應的IP地址
  18. addrSrv.sin_port = htons(8888); // socket對應的端口
  19. // 將socket綁定到某個IP和端口(IP標識主機,端口標識通信進程)
  20. bind(sockSrv,(SOCKADDR*)&addrSrv, sizeof(SOCKADDR));
  21. // 將socket設置為監聽模式,5表示等待連接隊列的最大長度
  22. listen(sockSrv, 5);
  23. // sockSrv為監聽狀態下的socket
  24. // &addrClient是緩沖區地址,保存了客戶端的IP和端口等信息
  25. // len是包含地址信息的長度
  26. // 如果客戶端沒有啟動,那麽程序一直停留在該函數處
  27. SOCKADDR_IN addrClient;
  28. int len = sizeof(SOCKADDR);
  29. unsigned int sockConn = accept(sockSrv,(SOCKADDR*)&addrClient, &len);
  30. while(1); // 卡住
  31. closesocket(sockConn);
  32. closesocket(sockSrv);
  33. WSACleanup();
  34. return 0;
  35. }

我們再看pc1客戶端程序:

  1. #include <winsock2.h>
  2. #include <stdio.h>
  3. #pragma comment(lib, "ws2_32.lib")
  4. #define SIO_KEEPALIVE_VALS _WSAIOW(IOC_VENDOR, 4)
  5. // tcp keepalive結構體
  6. typedef struct tcp_keepalive
  7. {
  8. u_long onoff;
  9. u_long keepalivetime;
  10. u_long keepaliveinterval;
  11. }TCP_KEEPALIVE;
  12. // 通信的socket
  13. SOCKET sockClient = 0;
  14. // 監測線程
  15. DWORD WINAPI monitorThread(LPVOID pM)
  16. {
  17. while(1)
  18. {
  19. char szRecvBuf[10] = {0};
  20. int nRet = recv(sockClient, szRecvBuf, 1, MSG_PEEK); // 註意, 最後一個參數必須是MSG_PEEK, 否則會影響主線程接收信息
  21. if(nRet <= 0) // 實際上, 等於0表示服務端主動關閉通信socket
  22. {
  23. printf("監測到啦: nRet is %d\n", nRet);
  24. closesocket(sockClient);
  25. break;
  26. }
  27. Sleep(200);
  28. }
  29. return 0;
  30. }
  31. int main()
  32. {
  33. WORD wVersionRequested;
  34. WSADATA wsaData;
  35. wVersionRequested = MAKEWORD(1, 1);
  36. WSAStartup( wVersionRequested, &wsaData );
  37. sockClient = socket(AF_INET, SOCK_STREAM, 0);
  38. SOCKADDR_IN addrSrv;
  39. addrSrv.sin_addr.S_un.S_addr = inet_addr("192.168.1.102");
  40. addrSrv.sin_family = AF_INET;
  41. addrSrv.sin_port = htons(8888);
  42. connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));
  43. // 開啟監測線程
  44. HANDLE handle = CreateThread(NULL, 0, monitorThread, NULL, 0, NULL);
  45. while(1); // 卡住
  46. CloseHandle(handle);
  47. closesocket(sockClient);
  48. WSACleanup();
  49. return 0;
  50. }

下面, 我們來做幾組實驗:

實驗一:

先啟動服務端, 再啟動客戶端, 建立tcp連接。 用netstat -nao | findstr 8888查看兩側的socket狀態, 發現是已經建立連接了。

情形1:

斷掉下行網線2, 用netstat -nao | findstr 8888查看兩側的socket狀態, 發現客戶端socket狀態變了, 且“監測到啦: nRet is -1”打印, 但服務端的socket狀態沒有變化。 這說明:客戶端有感知, 但服務端沒有感知。 此時, 服務端是死連接。

情形2:

斷掉下行網線3, 用netstat -nao | findstr 8888查看兩側的socket狀態, 發現客戶端socket狀態未變, 且沒有“監測到啦: nRet is -1”打印, 但服務端的socket狀態有變化。 這說明:客戶端沒有感知, 但服務端有感知。此時, 客戶端是死連接。

情形3:

斷掉路由器上行網線1, 用netstat -nao | findstr 8888查看兩側的socket狀態, 發現客戶端socket狀態未變, 且沒有“監測到啦: nRet is -1”打印, 且服務端的socket狀態也沒有變化。 而且這個時候, tcp連接並不是死連接, 還是活的, 還可以正常通信。 有意思的是, 此時, 我pc1上的QQ和pc2上的QQ過了一段時間都各自斷了, 說明騰訊QQ客戶端也有心跳機制技術分享圖片。註意, pc1上的QQ和pc2上的QQ不直接通信哈。

情形4:

斷掉路由器電源4, 用netstat -nao | findstr 8888查看兩側的socket狀態, 發現客戶端socket狀態變化, 且有“監測到啦: nRet is -1”打印, 服務端的socket狀態也變化。 說明這個時候, 客戶端有感知, 服務端也有感知, 兩側都不存在死連接。

情形5:

直接對pc1的電源線5進行斷電(當然, 也要把筆記本pc1的電源拔出來才算數),客戶端肯定就沒了啊。 此時, 服務端socket狀態並沒有變化, 說明服務端是沒有感知的, 服務端是死連接。

情形6:

直接對pc2的電源線6進行斷電(當然, 也要把筆記本pc2的電源拔出來才算數),服務端肯定就沒了啊。 此時,客戶端socket狀態並沒有變化, 且沒有“監測到啦: nRet is -1”打印, 說明客戶端是沒有感知的, 客戶端是死連接。

我們看一下, 除了情形3外, tcp的正常連接都受到了影響, 而且死連接無法感知, 這顯然不符合我們的期望。 那麽, 怎麽檢測tcp死連接呢? 這就是本文要深入討論的話題------心跳機制技術分享圖片

首先自然會問: 什麽是心跳機制? 為什麽需要心跳機制? 怎麽來實現它? 在本文中, 我會和大家一起來學習一下。

想一下, 當tcp連接被破壞後, 如果是死連接了, 服務端和客戶端怎樣才能知道信息能不能到達對方呢? 很自然的想法是, 不斷地給對方發探測信號, 看有沒有回應, 這就是心跳機制的直白原理。 所謂的心跳即是數據包, 發心跳就是一方向另一方發送的數據包, 不斷地發送, 如果收不到回應, 那麽就有理由認為是tcp連接出了問題。 那為什麽要叫心跳呢? 你摸一下你的心, 你看它是不是均勻在跳? 理解了吧, 均勻發出去的數據包就類似於均勻的心跳信號。 所以, 我要說: 心跳就是(探測性的)數據包。

到此為主, 我們算是搞懂了什麽是心跳機制, 為什麽需要心跳機制這兩個問題。

下面, 我們會更深入地討論心跳機制, 並在最後會寫個帶心跳機制的客戶端程序來實戰感受一下。

從原理上來講, 服務端的心跳機制和客戶端的心跳機制完全一致, 而且彼此獨立。 服務端的心跳只能用來檢測服務端的死連接, 客戶端的心跳只能檢測客戶端的死連接。

由於服務端和客戶端的心跳原理是基本一致的, 所以為了簡便起見, 我們僅僅在客戶端啟用心跳機制, 然後讓客戶端去檢測一下死連接。

雖然我們說心跳就是數據包, 且我們也可以抓包看到, 但其實這個包的報文段是不含有任何數據的, 因此, 即使你用recv函數, 也不會接收到什麽值, 也就是說,如果沒有應用層數據通信的話, 即使有循環心跳發送接收, recv也會阻塞在那裏, 靜靜地等待。

既然說到心跳, 我們就不得不說說心跳發送的頻率, 根據RFC的定義, TCP/IP協議棧需要等待的默認時間間隔是2小時。 但是, 對於大多數應用程序來說說, 2個小時後才能檢測到死連接又有什麽意義呢? 我就不明白了, RFC的作者難道傻麽技術分享圖片 為什麽要定義這麽長的一個時間? 翻閱資料後才得知: 原來, RFC作者是為了弱化用戶使用心跳機制。關於心跳機制, 一直存在這麽兩派爭論, 支持派:可以簡化應用程序的設計, 讓客戶端或者服務端檢測到斷網。 反對派:心跳機制浪費了帶寬, 而且可能會拆掉某個相對良好的tcp連接/通道。

好吧, 現在要解決問題, 要檢測死連接, 我們還是要繼續介紹心跳機制, 好在, 是有接口可以改變心跳參數的。 讓我稍微有點不太樂意的是技術分享圖片: 為什麽心跳機制檢測死連接後, 不指定一個回調的函數接口呢? 不過, 也沒關系, 既然你不提供, 那我就開個線程來檢測。

當客戶端將心跳發給服務端後, 眼巴巴地期望得到服務端的反饋, 如果沒有收到反饋, 協議棧自然有理由認為客戶端是死連接了(於是, 客戶端會發RST包重置鏈接, 也就是說, 這鏈接時無效的了),則之後客戶端的任何I/O操作或者待處理的I/O操作都將失敗。 所以, 自然可以用recv去檢測啊, 用recv函數去偷窺接收的內核緩沖區中的數據, 如果反饋-1, 那就表明通信斷了(請註意, 實際上, 在此處,recv函數的目的不是為了去獲取數據, 也不是為了去探測什麽數據, 而是簡單地執行一個io操作, 一旦啟動心跳機制,協議棧檢測到網絡異常後,io操作就會自然失敗。之所以選擇recv, 並把最後一個參數置為MSG_PEEK, 是因為我們要找到一個不影響主線程通信的io操作函數 )。 順便說一句, 之前說過, 如果服務端主動關閉通信的socket, 客戶端的recv函數會返回0, 所以, 綜合起來說, 為了檢測出連接的異常, 我們用<=0進行判斷。

也啰嗦不少了, 下面給出帶有心跳機制的客戶端代碼吧(說明, 在本文中, 我們認為檢測監測是同義詞):

  1. #include <winsock2.h>
  2. #include <stdio.h>
  3. #pragma comment(lib, "ws2_32.lib")
  4. #define SIO_KEEPALIVE_VALS _WSAIOW(IOC_VENDOR, 4)
  5. // tcp keepalive結構體
  6. typedef struct tcp_keepalive
  7. {
  8. u_long onoff;
  9. u_long keepalivetime;
  10. u_long keepaliveinterval;
  11. }TCP_KEEPALIVE;
  12. // 通信的socket
  13. SOCKET sockClient = 0;
  14. // 監測線程
  15. DWORD WINAPI monitorThread(LPVOID pM)
  16. {
  17. while(1)
  18. {
  19. char szRecvBuf[10] = {0};
  20. int nRet = recv(sockClient, szRecvBuf, 1, MSG_PEEK); // 註意, 最後一個參數必須是MSG_PEEK, 否則會影響主線程接收信息
  21. if(nRet <= 0) // 實際上, 等於0表示服務端主動關閉通信socket
  22. {
  23. printf("監測到啦: nRet is %d\n", nRet);
  24. closesocket(sockClient);
  25. break;
  26. }
  27. Sleep(200);
  28. }
  29. return 0;
  30. }
  31. int main()
  32. {
  33. WORD wVersionRequested;
  34. WSADATA wsaData;
  35. wVersionRequested = MAKEWORD(1, 1);
  36. WSAStartup( wVersionRequested, &wsaData );
  37. sockClient = socket(AF_INET, SOCK_STREAM, 0);
  38. // 啟用tcp keepalive機制
  39. #if 1
  40. // 設置SO_KEEPALIVE
  41. int iKeepAlive = 1;
  42. int iOptLen = sizeof(iKeepAlive);
  43. setsockopt(sockClient, SOL_SOCKET, SO_KEEPALIVE, (char *)&iKeepAlive, iOptLen);
  44. TCP_KEEPALIVE inKeepAlive = {0, 0, 0};
  45. unsigned long ulInLen = sizeof(TCP_KEEPALIVE);
  46. TCP_KEEPALIVE outKeepAlive = {0, 0, 0};
  47. unsigned long ulOutLen = sizeof(TCP_KEEPALIVE);
  48. unsigned long ulBytesReturn = 0;
  49. // 設置心跳參數
  50. inKeepAlive.onoff = 1; // 是否啟用
  51. inKeepAlive.keepalivetime = 1000; // 在tcp通道空閑1000毫秒後, 開始發送心跳包檢測
  52. inKeepAlive.keepaliveinterval = 500; // 心跳包的間隔時間是500毫秒
  53. /*
  54. 補充上面的"設置心跳參數":
  55. 當沒有接收到服務器反饋後,對於不同的Windows版本,客戶端的心跳嘗試次數是不同的,
  56. 比如, 對於Win XP/2003而言, 最大嘗試次數是5次, 其它的Windows版本也各不相同。
  57. 當然啦, 如果是在Linux上, 那麽這個最大嘗試此時其實是可以在程序中設置的。
  58. */
  59. // 調用接口, 啟用心跳機制
  60. WSAIoctl(sockClient, SIO_KEEPALIVE_VALS,
  61. &inKeepAlive, ulInLen,
  62. &outKeepAlive, ulOutLen,
  63. &ulBytesReturn, NULL, NULL);
  64. #endif
  65. SOCKADDR_IN addrSrv;
  66. addrSrv.sin_addr.S_un.S_addr = inet_addr("192.168.1.102");
  67. addrSrv.sin_family = AF_INET;
  68. addrSrv.sin_port = htons(8888);
  69. connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));
  70. // 開啟監測線程
  71. HANDLE handle = CreateThread(NULL, 0, monitorThread, NULL, 0, NULL);
  72. while(1); // 卡住
  73. CloseHandle(handle);
  74. closesocket(sockClient);
  75. WSACleanup();
  76. return 0;
  77. }

我們重做實驗一, 也就是如下的實驗二:

先啟動服務端, 再啟動有心跳機制的客戶端, 建立tcp連接。 用netstat -nao | findstr 8888查看兩側的socket狀態, 發現是已經建立連接了。

情形1:

斷掉下行網線2, 用netstat -nao | findstr 8888查看兩側的socket狀態, 發現客戶端socket狀態變了, 且“監測到啦: nRet is -1”打印, 但服務端的socket狀態沒有變化。 這說明:客戶端有感知, 但服務端沒有感知。 此時, 服務端是死連接。 (因為服務端沒有心跳, 所以還是檢測不了服務端的死連接)

情形2:

斷掉下行網線3, 用netstat -nao | findstr 8888查看兩側的socket狀態, 發現客戶端socket狀態變了, 且有“監測到啦: nRet is -1”打印, 且服務端的socket狀態有變化。 這說明:客戶端的心跳感知到了死連接, 而且服務端地自己本身的異常也是有感知的(不是借助心跳機制)。

情形3:

斷掉上行網線1, 用netstat -nao | findstr 8888查看兩側的socket狀態, 發現客戶端socket狀態未變, 且沒有“監測到啦: nRet is -1”打印, 且服務端的socket狀態也沒有變化。 而且這個時候, tcp連接並不是死連接, 還是活的, 還可以正常通信。 (此時, 通信ok, 沒有死連接, 所以心跳機制不會檢測到什麽死連接)。 有意思的是, 此時, 我pc1上的QQ和pc2上的QQ過了一段時間都各自斷了, 說明騰訊QQ客戶端也有心跳機制技術分享圖片。註意, pc1上的QQ和pc2上的QQ不直接通信哈。

情形4:

斷掉路由器電源4, 用netstat -nao | findstr 8888查看兩側的socket狀態, 發現客戶端socket狀態變化, 且有“監測到啦: nRet is -1”打印, 服務端的socket狀態也變化。 說明這個時候, 客戶端有感知, 服務端也有感知, 兩側都不存在死連接。 (不要心跳機制都能檢測到啊, 何況有了心跳機制)

情形5:

直接對pc1的電源線5進行斷電(當然, 也要把筆記本pc1的電源拔出來才算數),客戶端肯定就沒了啊。 此時, 服務端socket狀態並沒有變化, 說明服務端是沒有感知的, 服務端是死連接。(因為服務端沒有心跳, 所以還是檢測不了服務端死連接)

情形6:

直接對pc2的電源線6進行斷電(當然, 也要把筆記本pc2的電源拔出來才算數),服務端肯定就沒了啊。 此時,客戶端socket狀態有變化, 且有“監測到啦: nRet is -1”打印, 說明客戶端的心跳對死連接是有感知的。

看來, 心跳機制確實生效了, 以上介紹的主要是tcp協議棧自身提供的心跳機制, 當然, 我們也可以自己在應用層寫寫自己的心跳機制, 代碼會相對復雜一些, 但靈活度也會更大。 從作用上來講, 殊途同歸。總之, 借助心跳機制, 可以檢測到tcp連接的異常技術分享圖片

最後, 我們來簡要說說另外一種網絡結構, 假設把pc1和pc2直接用網線相連, 建立起世界最小局域網, 並形成tcp連接。如果在客戶端和服務端都沒有心跳機制,那麽實驗結果如下

1. 如果斷掉其中的網線, 客戶端和服務端都沒有感知。

2. 客戶端突然斷電, 則服務端沒有感知。

3.服務端突然斷電, 則客戶端沒有感知。

有興趣的朋友可以驗證一下上述結果。

好了, 心跳機制的介紹到此為止。 未來, 路漫漫, 但必將繼續勇敢前行!技術分享圖片

--------------------- 本文來自 stpeace 的CSDN 博客 ,全文地址請點擊:https://blog.csdn.net/stpeace/article/details/44162349?utm_source=copy

心跳機制tcp keepalive的討論、應用及“斷網”、"斷電"檢測的C代碼實現(Windows環境下)