关于TIME_WAIT状态

1130阅读 0评论2017-09-02 B_C_1024
分类:LINUX


 在看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 端:

点击(此处)折叠或打开

  1. #include <stdio.h>
  2. #include <string.h>
  3. #include <sys/socket.h>
  4. #include <unistd.h>
  5. #include <netdb.h>
  6. #include <netinet/in.h>
  7. #include <errno.h>

  8. char * host_name = "192.168.204.134";
  9. int port = 9888;
  10. int localport = 51335;
  11. int
  12. main()
  13. {
  14.     int sockfd;
  15.     int ret = 0;
  16.     struct sockaddr_in servaddr;
  17.     struct sockaddr_in cliaddr;
  18.     sockfd = socket(AF_INET, SOCK_STREAM, 0);
  19.     memset(&servaddr, 0, sizeof(servaddr));
  20.     servaddr.sin_family = AF_INET;
  21.     servaddr.sin_port = htons(port);
  22.     memset(&cliaddr, 0, sizeof(cliaddr));
  23.     cliaddr.sin_family = AF_INET;
  24.     cliaddr.sin_port = htons(localport);
  25.     ret = bind(sockfd, (struct sockaddr*)&cliaddr, sizeof(cliaddr));//给client绑定一个端口,测试TIME_WAIT
  26.     if(ret < 0)
  27.     {
  28.         printf(" bind error %s\n", strerror(errno));
  29.         return 0;
  30.     }
  31.     ret = inet_pton(AF_INET, host_name, &servaddr.sin_addr);
  32.     if(ret != 1)
  33.     {
  34.         printf(" inet_pton error:%s\n", strerror(errno));
  35.         return -1;
  36.     }
  37.     ret = connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
  38.     if(ret < 0)
  39.     {
  40.         printf(" connect to server error %s\n", strerror(errno));
  41.         close(sockfd);
  42.         return -1;
  43.     }
  44.     printf(" connect successfully\n");
  45.     getchar();
  46.     close(sockfd);
  47.     return 0;
  48. }

      server端:server在accept后fork一个子进程处理这个连接,后面测试TMME_WAIT用得到。

点击(此处)折叠或打开

  1. #include <stdio.h>
  2. #include <string.h>
  3. #include <stdlib.h>
  4. #include <unistd.h>
  5. #include <sys/socket.h>
  6. #include <netinet/in.h>
  7. #include <netdb.h>
  8. #include <sys/types.h>
  9. #include <errno.h>
  10. #include <sys/wait.h>
  11. #define SERV_PORT 9888
  12. #define LISTENQ 1024
  13. void client_server(int sockfd)
  14. {
  15.     char buf[100];
  16.     int ret = 0;
  17.     while(1)
  18.     {
  19.         ret = read(sockfd, buf, 100);
  20.         if(ret == 0)
  21.         {
  22.             printf(" connection shutdown by others \n");
  23.             ret = close(sockfd);
  24.             if(ret)
  25.             {
  26.                 printf(" close error %s\n", strerror(errno));
  27.             }
  28.             exit(0) ;
  29.         }
  30.     }
  31.     return;
  32. }
  33. int main()
  34. {
  35.     int listenfd = socket(AF_INET, SOCK_STREAM, 0);
  36.     if(listenfd < 0)
  37.     {
  38.         printf(" socket error %s\n", strerror(errno));
  39.         return 0;
  40.     }
  41.     int sockfd = 0;
  42.     pid_t child;
  43.     if(listenfd == -1)
  44.     {
  45.         printf(" socket error\n");
  46.         return 0;
  47.     }
  48.     struct sockaddr_in serveraddr, clientaddr;
  49.     memset(&serveraddr, 0, sizeof(serveraddr));
  50.     memset(&clientaddr, 0, sizeof(clientaddr));
  51.     serveraddr.sin_family = AF_INET;
  52.     serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
  53.     serveraddr.sin_port = htons(SERV_PORT);
  54.     int ret = 0;
  55.     ret = bind(listenfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
  56.     if(ret < 0)
  57.     {
  58.         printf(" bind error %s\n", strerror(errno));
  59.         return -1;
  60.     }
  61.     ret =listen(listenfd, LISTENQ);
  62.     if(ret < 0)
  63.     {
  64.         printf(" listen error %s\n", strerror(errno));
  65.         return -1;
  66.     }
  67.     printf(" create server successfully\n");
  68.     while(1)
  69.     {
  70.         sockfd = accept(listenfd, NULL, NULL);
  71.         if(sockfd < 0)
  72.         {
  73.              printf(" accept error %s\n", strerror(sockfd));
  74.              return -1;
  75.         }
  76.         child = fork();
  77.         if(child == 0)
  78.         {
  79.             close(listenfd);
  80.             client_server(sockfd);
  81.             exit(0);
  82.         }
  83.         else if(child > 0)
  84.         {
  85.             int status;
  86.             ret = close(sockfd);
  87.             printf(" ret = %d\n", ret);
  88.             ret = wait(&status);
  89.             printf("ret = %d\n", ret);
  90.             continue;
  91.         }
  92.         
  93.     }
  94. }
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选项的代码如下:

点击(此处)折叠或打开

  1. int val = 1;
  2.     int len = sizeof(val);
  3.     ret = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &val, len);
  4.     if(ret < 0)
  5.     {
  6.         printf(" setsockopt error: %s\n", strerror(errno));
  7.         return -1;
  8.     }
下图是测试在一个处于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先添加代码如下:

点击(此处)折叠或打开

  1. int ret = 0;
  2.     int val = 1;
  3.     int len = sizeof(val);
  4.     ret = setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &val, len);
  5.     if(ret < 0)
  6.     {
  7.         printf(" setsockopt error %s\n", strerror(errno));
  8.         return -1;
  9.     }
再次重现上面的测试,可以看到这次重启server成功了。 

上一篇:linux内核同步机制completion机制
下一篇:没有了