一個 TCP 連接在完成上述的三次握手之後便建立完畢;此後,連接的兩端即可進行信息的相互傳遞。因此,TCP 連接可以認爲是以兩端 IP 地址和端口進行標識的一個通信信道,而 TCP 連接的建立就是向通信雙方進行上述通信信道註冊的過程。TCP 連接一旦建立,只要通信雙方之間的中間結點(包括網關和交換機、路由器等網絡設備)工作正常,那麼在通信雙方中的任何一方主動關閉連接之前,TCP 連接都將被一直保持下去。
TCP 連接的這種特性,使得一個長期不交換任何信息的空閒連接可以長期保持數小時、數天甚至數月。中間路由器可以崩潰、重啓,網線可以被掛斷再連通,只要兩端的主機沒有被重啓,TCP 連接就可以被一直保持下來。
對於一個 TCP 連接兩端的主機而言,創建 TCP 連接需要耗費一定的系統資源。如果不再使用某個連接,那麼我們總是希望進行通信的兩個主機能夠主動關閉相應的連接,以便釋放所佔用的系統資源。然而,如果由於客戶端出現異常 ( 例如崩潰或異常重啓 ) 而導致連接未能正常關閉,這將導致服務器端的連接斷連。
探測 TCP 連接是否斷連或是工作正常的原理比較簡單:定期向連接的遠程通信節點發送一定格式的信息並等待遠程通信節點的反饋,如果在規定時間內收到來自遠程節點的正確 的反饋信息,那麼該連接就是正常的,否則該連接已經斷連。依據該原理,目前常用的探測方法有以下三種。
1,最常用的探測方法就是採用 TCP 協議層提供的保活探測功能即 TCP 連接保活定時器。儘管該功能並不是 RFC 規範的一部分,但是幾乎所有的類 Unix 系統均實現了該功能,所以使得該探測方法被廣泛使用。
2,此種方法就是在服務節點上安裝相應的第三方應用程序來探測該節點上所有的 TCP 連接是否正常或是已經斷連。該方法最大的不足就是需要所有支持探測的客戶端能夠識別來自該探測應用的數據報文,因此,實際應用中比較少見。
3,應用程序本身附帶探測其自身建立的 TCP 連接的功能。這種方法具有極大的靈活性,可以依據應用本身的特點選擇相應的探測機制和功能實現。然而,實際應用中,大部分應用程序均沒有附帶自我探測的功能。
下面對第一種進行研究。
在Linux下面默認的是沒有保活機制的,即當一個TCP的socket,客戶端和服務器沒有通信時,連接會一直保持。可以通過setsockopt設置SO_KEEPALIVE即可。setsockopt的函數原型如下所示:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen); 成功後回0,失敗返回-1,並設置errno
默認情況下Linux環境下TCP的keepalive參數如下:root@ubuntu:/home/xgx# sysctl -A |grep keep
net.ipv4.tcp_keepalive_time = 7200
net.ipv4.tcp_keepalive_probes = 9
net.ipv4.tcp_keepalive_intvl = 75
三個參數意義爲:
tcp_keepalive_time單位爲秒,即默認的爲2小時,表示當keepalive打開的情況下,TCP發送keepalive消息的頻率。
tcp_keepalive_probes:爲放棄一個TCP連接之前,需要進行多少次重試。RFC規定最低的爲3.
tcp_keepalive_intvl:探測消息發送的頻率,乘以tcp_keepalive_probes就得到對於從開始探測到連接殺除的時間。
對我們自己定義的socket,我們可以通過setsockopt來重新設置這三個值(默認的值比較大)。下面的來測試。
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/tcp.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
int socket_set_keepalive (int fd)
{
int ret, error, flag, alive, idle, cnt, intv;
/* Set: use keepalive on fd */
alive = 1;
if (setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &alive,sizeof alive) != 0)
{
fprintf(stderr,"Set keepalive error: %s.\n", strerror (errno));
return -1;
}
/* 10秒鐘無數據,觸發保活機制,發送保活包 */
idle = 60;
if (setsockopt (fd, SOL_TCP, TCP_KEEPIDLE, &idle, sizeof idle) != 0)
{
fprintf(stderr,"Set keepalive idle error: %s.\n", strerror (errno));
return -1;
}
/* 如果沒有收到迴應,則5秒鐘後重發保活包 */
intv = 5;
if (setsockopt (fd, SOL_TCP, TCP_KEEPINTVL, &intv, sizeof intv) != 0)
{
fprintf(stderr,"Set keepalive intv error: %s.\n", strerror (errno));
return -1;
}
/* 連續3次沒收到保活包,視爲連接失效 */
cnt = 3;
if (setsockopt (fd, SOL_TCP, TCP_KEEPCNT, &cnt, sizeof cnt) != 0)
{
fprintf(stderr,"Set keepalive cnt error: %s.\n", strerror (errno));
return -1;
}
return 0;
}
int open_listenfd(int port)
{
int listenfd, optval=1;
struct sockaddr_in serveraddr;
/* Create a socket descriptor */
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
return -1;
/* Eliminates "Address already in use" error from bind. */
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR,
(const void *)&optval , sizeof(int)) < 0)
return -1;
/* Listenfd will be an endpoint for all requests to port
on any IP address for this host */
bzero((char *) &serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons((unsigned short)port);
if (bind(listenfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
return -1;
/* Make it a listening socket ready to accept connection requests */
if (listen(listenfd, 5) < 0)
return -1;
return listenfd;
}
void echo(int connfd)
{
size_t n;
char buf[100];
while((n = read(connfd, buf, 100)) != 0) {
printf("server received %d bytes\n", n);
write(connfd, buf, n);
}
}
int main()
{
int listenfd,connfd,port,clientlen;
struct sockaddr_in clientaddr;
struct hostent *hp;
char *haddrp;
listenfd=open_listenfd(50000);
while(1)
{
clientlen=sizeof(clientaddr);
connfd=accept(listenfd,(struct sockaddr*)&clientaddr,&clientlen);
socket_set_keepalive(connfd);
hp = gethostbyaddr((const char *)&clientaddr.sin_addr.s_addr,
sizeof(clientaddr.sin_addr.s_addr), AF_INET);
haddrp = inet_ntoa(clientaddr.sin_addr);
printf("server connected to %s (%s)\n", hp->h_name, haddrp);
echo(connfd);
close(connfd);
}
}
在linux下進行測試運行tcpdump捕獲數據包,運行tcpdump -e -i host 127.0.0.1
在客戶端沒有異常斷開時數據輸出,由前面的時間間隔可知爲一分鐘發送一次,和我們定義的每分鐘發一次的心跳包吻合。
參考資料:http://www.ibm.com/developerworks/cn/aix/library/0808_zhengyong_tcp/index.html
http://www.cnblogs.com/rainbowzc/archive/2009/06/02/1494779.html