在看TCP/IP 卷1和UNP的时候,对于这个TIME_WAIT状态了解的不透彻,这里记录一下个人对这个状态的理解,方便日后查阅。
一.简介
TIME_WAIT状态是TCP连接断开过程中的一种状态,TCP连接的主动关闭的一方会有这样的状态。其目的在后文介绍,先贴上一张TCP连接的状态转移图。
从上面的图中可以拿到,TCP连接的主动关闭一方,会在对最后一个FIN进行ACK后进入到这个状态。TCP连接主动关闭的一方处在这个状态时,TCP连接并没有关闭完成。通常,这个状态的时间为2MSL,MSL是IP数据报能在网络中生存的最长时间。
二. TIME_WAIT状态的目的
从TCP/IP协议详解卷一和UNP第一卷的介绍,该状态主要有两个目的:
1.确保主动关闭这一方最后发出的ACK能够顺利被被动关闭的一方收到。
2.在TIME_WAIT状态中,这个连接额插口不能再被使用/允许老的重复的分节在网络中消逝。
关于第一个目的,比较好理解,主动关闭的这一方,在发送完最后一个ACK后,如果被动关闭的一方没有收到,那么它会再次给主动关闭的一方发送一个对上一个分节的确认,告知没后收到ACK,这个时候处于TIME_WAIT的一方会再次发送ACK。对于第二个目的,理解起来比较不那么直观,本文将主要对这第二个目的进分析学习。
第二个目的:处于TIME_WAIT状态socket,不能再次被使用,也就说不能bind一个处于socket状态的端口到一个新socket,这样做是为了保证处于TIME_WAIT状态的socket以前发送在网络中的包能够正确的消逝。如果允许bind一个处于TIME_WAIT状态的socket再次启用一个新的连接,如果一个包到了这个新的连接,如何知道这个包不是以前处于TIME_WAIT那个socket应该收到的包呢?
三. TIME_WAIT的影响
本文写几个TCP连接的例子,来测试TIME_WAIT状态影响。
1.对client端的影响。
对于server端来说,确实是需要这个TIME_WAIT的,而且没什么影响,因为client端的端口号一般都是系统随机分配的,我们不会给client端的socket去bind一个端口号。内核一般也不会选择一个处于TIME_WAIT状态的端口号。TIME_WAIT的目的也正在这种情况下发挥了作用。
写一个简单的例子,来测试一下。为了测试,在client端bind一个端口。
client 端:
-
#include <stdio.h>
-
#include <string.h>
-
#include <sys/socket.h>
-
#include <unistd.h>
-
#include <netdb.h>
-
#include <netinet/in.h>
-
#include <errno.h>
-
-
char * host_name = "192.168.204.134";
-
int port = 9888;
-
int localport = 51335;
-
int
-
main()
-
{
-
int sockfd;
-
int ret = 0;
-
struct sockaddr_in servaddr;
-
struct sockaddr_in cliaddr;
-
sockfd = socket(AF_INET, SOCK_STREAM, 0);
-
memset(&servaddr, 0, sizeof(servaddr));
-
servaddr.sin_family = AF_INET;
-
servaddr.sin_port = htons(port);
-
memset(&cliaddr, 0, sizeof(cliaddr));
-
cliaddr.sin_family = AF_INET;
-
cliaddr.sin_port = htons(localport);
-
ret = bind(sockfd, (struct sockaddr*)&cliaddr, sizeof(cliaddr));//给client绑定一个端口,测试TIME_WAIT
-
if(ret < 0)
-
{
-
printf(" bind error %s\n", strerror(errno));
-
return 0;
-
}
-
ret = inet_pton(AF_INET, host_name, &servaddr.sin_addr);
-
if(ret != 1)
-
{
-
printf(" inet_pton error:%s\n", strerror(errno));
-
return -1;
-
}
-
ret = connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
-
if(ret < 0)
-
{
-
printf(" connect to server error %s\n", strerror(errno));
-
close(sockfd);
-
return -1;
-
}
-
printf(" connect successfully\n");
-
getchar();
-
close(sockfd);
-
return 0;
-
}
server端:server在accept后fork一个子进程处理这个连接,后面测试TMME_WAIT用得到。
-
#include <stdio.h>
-
#include <string.h>
-
#include <stdlib.h>
-
#include <unistd.h>
-
#include <sys/socket.h>
-
#include <netinet/in.h>
-
#include <netdb.h>
-
#include <sys/types.h>
-
#include <errno.h>
-
#include <sys/wait.h>
-
#define SERV_PORT 9888
-
#define LISTENQ 1024
-
void client_server(int sockfd)
-
{
-
char buf[100];
-
int ret = 0;
-
while(1)
-
{
-
ret = read(sockfd, buf, 100);
-
if(ret == 0)
-
{
-
printf(" connection shutdown by others \n");
-
ret = close(sockfd);
-
if(ret)
-
{
-
printf(" close error %s\n", strerror(errno));
-
}
-
exit(0) ;
-
}
-
}
-
return;
-
}
-
int main()
-
{
-
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
-
if(listenfd < 0)
-
{
-
printf(" socket error %s\n", strerror(errno));
-
return 0;
-
}
-
int sockfd = 0;
-
pid_t child;
-
if(listenfd == -1)
-
{
-
printf(" socket error\n");
-
return 0;
-
}
-
struct sockaddr_in serveraddr, clientaddr;
-
memset(&serveraddr, 0, sizeof(serveraddr));
-
memset(&clientaddr, 0, sizeof(clientaddr));
-
serveraddr.sin_family = AF_INET;
-
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
-
serveraddr.sin_port = htons(SERV_PORT);
-
int ret = 0;
-
ret = bind(listenfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
-
if(ret < 0)
-
{
-
printf(" bind error %s\n", strerror(errno));
-
return -1;
-
}
-
ret =listen(listenfd, LISTENQ);
-
if(ret < 0)
-
{
-
printf(" listen error %s\n", strerror(errno));
-
return -1;
-
}
-
printf(" create server successfully\n");
-
while(1)
-
{
-
sockfd = accept(listenfd, NULL, NULL);
-
if(sockfd < 0)
-
{
-
printf(" accept error %s\n", strerror(sockfd));
-
return -1;
-
}
-
child = fork();
-
if(child == 0)
-
{
-
close(listenfd);
-
client_server(sockfd);
-
exit(0);
-
}
-
else if(child > 0)
-
{
-
int status;
-
ret = close(sockfd);
-
printf(" ret = %d\n", ret);
-
ret = wait(&status);
-
printf("ret = %d\n", ret);
-
continue;
-
}
-
-
}
-
}
server 启动之后,client端连接,然后关闭,用netstat查看client的端的处于TIME_WAIT状态。然后再次连接,bind如预期显示失败。
四.对server端的影响
TIME_WAIT对server端的影响和client端的影响是一样的,但是不同之处是作为server端,我们并不需要有这样的影响。同样用上面的代码,这次测试关闭监听socket,然后再次开启的例子。TCP连接的server的监听套接口关闭后,server也就关闭了,不会再次接收新的连接了,所以这种情况下,我们要重启server。但是这个以后也是不能重启来的。
五.SO_REUSEADDR选项
对于client端的TIME_WAIT状态,是必要的,但有时对于server端,我们不需要这种状态。SO_REUSEADDR选项可以(1)对于一个处于TIME_WAIT状态的socket再次成功建立连接,(2)在一个已有连接的socket上再次创建一个TCP监听socket。
(1). 在一个已有TCP连接的socket上重新bind成功,但此时并不一定能成功建立TCP连接。在下面的实验中,给client端设置一个SO_REUSEADDR选项,并且先把client端变成TIME_WAIT状态,然后再在这个端口上重新建立一个连接,从实验结果上看,我们连接是成功的。
设置client端socket的SO_REUSEADDR选项的代码如下:
-
int val = 1;
-
int len = sizeof(val);
-
ret = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &val, len);
-
if(ret < 0)
-
{
-
printf(" setsockopt error: %s\n", strerror(errno));
-
return -1;
-
}
下图是测试在一个处于TIME_WAIT状态的socket上新建一个连接的情况。
对于处于TIME_WAIT状态的socket,通过设置SO_REUSEADDR可以在这个socket上新建一个连接。然而,对于一个已经成功建立连接的socket,即便是SO_REUSEADDR也不一定能新建一个连接。在下面的实验中,我们动一个client,然后将他切换到后台运行,然后再次尝试启动这个client
从上面的结果可以看到,我门不能在一个已经连接的socket上再次尝试连接,即使是设置了SO_REUSEADDR。但是从上面结果上,可以看到出错的原因并不是bind错误,而是调用connect时出的错误。而且从errno的信息可以看到是没办法分配IP地址的错误,在client端bind协议地址的时候没有选定IP地址,由内核选定IP地址,现在内核不能给分配IP地址。
如果我们在给client端bind协议地址的时候指定不同的IP地址,不让内核去选择,则是可以建立一个新的连接的,即使不设置SO_REUSEADDR选项也是可以的,因为IP不同,意味着不同的连接。
(2). 在一个已有连接的socket上再次创建一个TCP监听socket
如果server端accept了一个连接,创建了一个子线程服务这个连接,如果这个时候父进程因为意外退出,这时候是需要重启这个监听socket的。但是这个是在端口上已经有新的连接了,所以这个监听进程是启动不起来的。下面的测试,先启动server,再accept一个连接,fork一个子进程。然后查看父进程额PID,并且KILL掉,尝试再次启动这个server。
再次启动这个server可以看到bind调用失败了。
修改server端的代码,加上SO_REUSEADDR选项。server先添加代码如下:
-
int ret = 0;
-
int val = 1;
-
int len = sizeof(val);
-
ret = setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &val, len);
-
if(ret < 0)
-
{
-
printf(" setsockopt error %s\n", strerror(errno));
-
return -1;
-
}
再次重现上面的测试,可以看到这次重启server成功了。
阅读(1144) | 评论(0) | 转发(0) |