一个 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