SO_REUSEADDR和SO_REUSEPORT选项

    最近看redis源码,看到redis的网络模型,借机对socket编程和TCP/IP协议做了进一步的巩固和熟悉。其中对socket选项SO_REUSEADDR和SO_REUSEPORT写了一些demo,文章根据测试结果对SO_REUSEADDR选项和SO_REUSEPORT选项做一个总结,同时对博客的总结做一个纠正。

    先来了解一下socket默认的行为:

    ·每个TCP连接都是由唯一的五元组<协议、源IP、源PORT、目的IP、目的PORT>进行标识,任何两条有效连接不可能具有完全相同的五元组。

    ·socket编程存在通配绑定(IPV4的0.0.0.0:PORT和IPV6的:::PORT)和特定的绑定(比如127.0.0.1:3602),无论我们先进行特殊的绑定然后进行通配绑定,还是先进行通配绑定然后进行特殊绑定,只要二者的IP+PORT存在冲突,就不可能绑定成功,通常出现Address already in use的错误。

    ·通常情况下,当连接主动关闭的一方进入到TIME_WAIT的状态时,该连接占用的IP+PORT被认为是有效的且不能立即被再次绑定并占用,需要等到2MSL时间之后才能够被复用。

    当然,SO_RESUEADDR和SO_REUSEPORT的目的是改变IP+PORT的绑定和占用的默认行为,但是在不同的系统上却有不同的表现。文章对BSD(选取Mac)和Linux系统下的测试结果进行总结。以两个IP地址为例,分别是通配地址0.0.0.0、回旋地址127.0.0.1,使用3602的端口。

  SO_REUSEADDR选项

    SO_REUSEADDR选项目的是在以下两个方面改变socket默认的行为:

    ·绑定的IP+PORT存在冲突时(部分重合,但不是完全相同),允许进行绑定。

    ·主动关闭连接的一方处于TIME_WAIT的时候,允许新的连接重新绑定与TIME_WAIT状态的连接有冲突的IP+PORT,并立即接收数据。

    首先,对下面表格中使用的标识进行说明,“First”表示先启动进程,“After”表示后启动;“是”表示设置了对应的标识,“否”表示没有设置;“TIME_WAIT”表示连接处于TIME_WAIT状态;“S”表示绑定成功,“F”表示绑定失败。

   SO_REUSEADDR对绑定的影响

    下面,我们以表格的形式给出不同系统下SO_REUSEADDR对绑定有冲突的IP+PORT的影响。

    BSD系统

    首先来看BSD系统下SO_REUSEADDR的对于socket绑定行为的影响.

                                                                      表1 BSD系统使用SO_REUSEADDR选项

  127.0.0.1(After,否) 0.0.0.0(After,否) 127.0.0.1(After,是) 0.0.0.0(After,是)
127.0.0.1(First,否) F F F S
0.0.0.0(First,否) F F S F
127.0.0.1(First,是) F F F S
0.0.0.0(First,是) F F S F

    通过对比,我们得出在BSD系统中使用SO_REUSEADDR的以下几点结论:

    1、SO_REUSEADDR只是解决了通配绑定和具体的IP+PORT绑定的冲突问题;无论是否使用SO_REUSEADDR,当一个IP+PORT组合已经被使用之后,另一个连接无法绑定完全相同(两个都是统配绑定或者两个都是具体绑定)的IP+PORT。

    2、解决通配绑定和具体的IP+PORT绑定之间并没有顺序的限制;不使用SO_REUSEADDR的时候,无论先进行通配绑定还是先进行具体绑定,另一种绑定都不可能成功;第一个启动的程序可以不用设置SO_REUSEADDR选项,但是只要后面启动的程序(无论是使用通配绑定还是使用具体的IP+PORT绑定)设置了SO_REUSEADDR选项,如果存在IP+PORT的冲突(不是完全相同),那么绑定也能成功。

    这里需要说明一下,如果我们启动两个进程分别绑定127.0.0.1、0.0.0.0和相同的端口,如果一个启动client进程连接到127.0.0.1的话,会连接到绑定127.0.0.1地址的进程;如果只有绑定0.0.0.0的进程存在,那么client连接到该进程。也就是说,client连接server的时候,首先使用具体的具体IP+PORT对应的进程,然后使用通配的IP+PORT对应的进程。

    Linux系统

    Linux系统下SO_REUSEADDR对socket绑定行为的影响,使用的Linux系统版本为Linux 2.6.32。

                                                       表2 Linux系统使用SO_REUSEADDR对绑定行为的影响

  127.0.0.1(After,否) 0.0.0.0(After,否) 127.0.0.1(After,是) 0.0.0.0(After,是)
127.0.0.1(First,否) F F F F
0.0.0.0(First,否) F F F F
127.0.0.1(First,是) F F F F
0.0.0.0(First,是) F F F F

     在Linux下,SO_REUSEADDR对于绑定的地址冲突并没有什么影响,无论是否使用SO_REUSEADDR,只要IP+PORT之间存在冲突,后面的绑定就会失败,而博客中却说先进行特定绑定再进行通配绑定才能成功,否则不能成功(X)。

   TIME_WAIT下SO_REUSEPORT对有冲突的IP+PORT的影响

    每一个TCP连接的socket描述符都有各自的发送缓冲区和接收缓冲区,且都可以分别使用setsockopt进行设置。连接建立之后,我们使用send、write等函数发送数据时,都是将数据写入到socket缓冲区中就立即返回,此时数据并没有立即被发送到网络上,至于什么时候将数据发送到网络上是由操作系统的调度、网络情况和接收端的通告窗口等各种因素决定。因此,从写入数据进socket的缓冲区直到数据实际被发送出去可能存在一定时间的延迟。TCP是一种可靠的连接,当主动关闭连接的一方close一个socket描述符的时候,该方将进入TIME_WAIT状态,以等待缓冲区的数据发送到对端、已经发送出的数据离开网络或者重传数据(重传断开连接时四次握手过程中的最后一个ACK),因此TIME_WAIT状态的连接使用的IP+PORT仍然被认为是一个有效的IP+PORT组合,相同机器上不能够在该IP+PORT组合上进行绑定。这个状态的持续时间可以通过改变内核参数进行设置,Linux下通过sudo sysctl -w net.ipv4.tcp.fin.timeout=value 进行设置。表3和表4总结了SO_REUSEADDR对于处于TIME_WAIT状态的连接占用的IP+PORT的影响。

    在下面的例子中,我们先在server端关闭连接,然后再在client端关闭连接,那么server端将进入到TIME_WAIT状态,然后在server端启动测试的进程,观察IP+PORT有冲突时SO_REUSEADDR对启动新进程的影响。

    BSD系统

                                                          表3 TIME_WAIT状态下SO_REUSEADDR对重新绑定的影响

  127.0.0.1(After,否) 0.0.0.0(After,否) 127.0.0.1(After,是) 0.0.0.0(After,是)
127.0.0.1(First,否,TIME_WAIT) F F S S
0.0.0.0(First,否,TIME_WAIT) F F S S
127.0.0.1(First,是,TIME_WAIT) F F S S
0.0.0.0(First,是,TIME_WAIT) F F S S

        BSD系统下,无论原来的进程是否使用SO_REUSEADDR选项,如果当前启动进程绑定的IP+PORT与处于TIME_WAIT状态的连接占用的IP+PORT存在冲突,但是新启动的进程使用了SO_REUSEADDR选项,那么该进程就可以成功启动,并且能够立即接收数据;否则,新启动的进程无法绑定与处于TIME_WAIT状态的连接占用的IP+PORT有冲突的IP+PORT。此时通过netstat -an -p tcp | grep 3602查看,可以看到一个处于TIME_WAIT状态的连接和一个处于ESTABLISHED状态的连接,如图1所示。

                                图1 BSD系统SO_REUSEADDR对处于TIME_WAIT状态的连接占用的IP+PORT的复用影响

    Linux系统

                                                     表4 TIME_WAIT状态下SO_REUSEADDR对重新绑定的影响

  127.0.0.1(After,否) 0.0.0.0(After,否) 127.0.0.1(After,是) 0.0.0.0(After,是)
127.0.0.1(First,否,TIME_WAIT) F F F F
0.0.0.0(First,否,TIME_WAIT) F F F F
127.0.0.1(First,是,TIME_WAIT) F F S S
0.0.0.0(First,是,TIME_WAIT) F F S S    

      Linux系统下,只有当处于TIME_WAIT状态的连接对应的进程在创建时设置了SO_REUSEADDR,并且当前要启动的绑定了有冲突的IP+PORT的进程也使用了SO_REUSEADDR选项,那么进程才能够绑定并启动成功。启动成功之后使用netstat -anpt | grep 3602查看可以看到如图2的连接状态。

                                       图2 Linux系统中SO_REUSEADDR对处于TIME_WAIT状态的IP+PORT的复用

  SO_REUSEPORT选项

   SO_REUSEADDR解决了通配IP+PORT和具体的IP+PORT绑定之间的冲突,但是完全相同的IP+PORT绑定(无论是具体的IP还是通配)仍然出现Address already in use的错误,使用SO_REUSEPORT选项可以避免此错误。

   SO_REUSEPORT对绑定的影响

    BSD系统

                                                             表5 BSD系统SO_REUSEPORT对绑定行为的影响

  127.0.0.1(After,否) 0.0.0.0(After,否) 127.0.0.1(After,是) 0.0.0.0(After,是)
127.0.0.1(First,否) F F F S
0.0.0.0(First,否) F F S F
127.0.0.1(Fitst,是) F F S S
0.0.0.0(First,是) F F S S   

      BSD系统下SO_REUSEPORT与SO_REUSEADDR对绑定行为的不同影响在于,如果先启动的进程和后启动的进程都设置了SO_REUSEPORT选项,那么即使是绑定完全相同的IP+PORT也能够启动成功;如果绑定了有冲突的IP+PORT的前面的实例没有设置SO_REUSEPORT选项,但是只要后面的实例设置了SO_REUSEPORT选项,只要IP+PORT不是完全相同,后面的进程能够成功绑定并启动。比如,如果我们启动了一个绑定127.0.0.1+3602的进程且该实例设置了SO_REUSEPORT选项,那么该进程可以再次被重复启动。下面,我们在所有的运行实例中都设置了SO_REUSEPORT选项,那么启动两次绑定127.0.0.1+3602的进程和一次绑定0.0.0.0+3602的进程之后,通过ps aux命令查看到的输出结果如图3所示,通过netstat查看连接的输出结果如图4所示,存在两个127.0.0.1+3602端口的侦听进程。

                                                    图3 SO_REUSEPORT对绑定有冲突的IP+PORT的进程的影响

                                                     图4 SO_REUSEPORT对绑定的有冲突的IP+PORT的连接的影响

    如果所有的实例都设置了SO_REUSEPORT选项,绑定完全相同的IP+PORT的进程也能够重复启动。通过ps命令能够看到所有启动的进程,而且通过netstat命令能够看到所有启动的连接。

    Linux系统    

                                                               表6 Linux系统SO_REUSEPORT对绑定行为的影响

  127.0.0.1((After,否) 0.0.0.0((After,否) 127.0.0.1((After,是) 0.0.0.0((After,是)
127.0.0.1(First,否) F F F F
0.0.0.0(First,否) F F F F
127.0.0.1(Fitst,是) F F S S
0.0.0.0(First,是) F F S S

    Linux系统下,如果两个实例绑定的IP+PORT存在冲突,那么只有当前、后启动的实例都设置了SO_REUSEPORT选项,进程才能够启动成功;存在冲突的实例中如果只有一个(无论前、后)设置了SO_REUSEPORT选项,那么后面启动的进程无法进行绑定,出现Address already in use的错误。所有实例都设置了SO_REUSEPORT选项的情况下,重复启动绑定127.0.0.1+3602的实例两次,绑定0.0.0.0+3602的实例一次,通过ps查看到的进程如图5所示;通过netstat查看到的连接如图5所示。

                                                           图5  Linux系统SO_REUSEPORT对启动进程的影响

                                                         图6 Linux系统SO_REUSEPORT对建立连接的影响

    对比图5和图6,发现虽然,成功启动了三个进程,但是netstat却只看到了两个连接,且是后面启动的进程的连接覆盖了前面的连接。关于这一点还是希望明白其中原理的大神解答一下。

    TIME_WAIT下SO_REUSEPORT对有冲突的IP+PORT的影响

      BSD系统

                                               表7 BSD系统下TIME_WAIT状态下SO_REUSEPORT选项对启动进程的影响    

  127.0.0.1(Second,否) 0.0.0.0(Second,否) 127.0.0.1(Second,是) 0.0.0.0(Second,是)
127.0.0.1(First,否,TIME_WAIT) F F S S
0.0.0.0(First,否,TIME_WAIT) F F S S
127.0.0.1(First,是,TIME_WAIT) F F S S
0.0.0.0(First,是,TIME_WAIT) F F S S

                                           表8 Linux系统下TIME_WAIT状态下SO_REUSEPORT选项对启动进程的影响

  127.0.0.1(Second,否) 0.0.0.0(Second,否) 127.0.0.1(Second,是) 0.0.0.0(Second,是)
127.0.0.1(First,否,TIME_WAIT) F F F F
0.0.0.0(First,否,TIME_WAIT) F F F F
127.0.0.1(First,是,TIME_WAIT) F F S S
0.0.0.0(First,是,TIME_WAIT) F F S S

    对比表7和表8,可以发现:

    ·BSD系统下无论使连接处于TIME_WAIT状态的原来的进程是否设置了SO_REUSEPORT,如果以后启动的进程绑定的IP+PORT与处于TIME_WAIT状态连接的IP+PORT存在冲突(完全相同或者是存在部分重合),那么只要以后启动的进程设置了SO_REUSEPORT,那么可以成功启动。

   ·Linux系统下,只有使处于TIME_WAIT连接状态的原来进程设置了SO_REUSEPORT选项,并且以后启动的进程绑定了有冲突的IP+PORT时也设置了SO_REUSEPORT选项,才能够成功绑定并启动以后的进程。

 

    以上是个人关于SO_REUSEADDR和SO_REUSEPORT选项的理解,欢迎吐槽。

 

    附测试使用代码:

    server端:

  #include
  #include
  #include
  #include
  #include
  #include
  #include
  #include
  #include
  #include
  #include
  #include
  #include
  
  #define WAIT_COUNT 5
  #define SERV_PORT 3602
  
  int main(int argc, char** argv)
  {
      int listen_fd, real_fd;
      struct sockaddr_in listen_addr, client_addr;
      socklen_t len = sizeof(struct sockaddr_in);
      listen_fd = socket(AF_INET, SOCK_STREAM, 0);
      if(listen_fd == -1)
      {   
          fprintf(stderr, "errno=%d,errmsg=%s\n", errno, strerror(errno));
          return -1;
      }
  
      int result = 1;
      socklen_t socklen = sizeof(result);
      /* 根据需要对是否设置该选项、以及替换成SO_REUSEPORT选项进行设置 */
      setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &result, socklen);
  
      char *ip = "0.0.0.0";
      bzero(&listen_addr,sizeof(listen_addr));
      inet_pton(AF_INET, ip, &listen_addr.sin_addr);
      listen_addr.sin_family = AF_INET;
      listen_addr.sin_port = htons(SERV_PORT);
      if(bind(listen_fd,(struct sockaddr *)&listen_addr, len) < 0)
      {
          fprintf(stderr, "errno=%d,errmsg=%s\n", errno, strerror(errno));
          fprintf(stderr, "bind error!\n");
          exit(1);
      }
      listen(listen_fd, WAIT_COUNT);
      real_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &len);
  
      if(real_fd == -1)
      {
          fprintf(stderr, "errno=%d,errmsg=%s\n", errno, strerror(errno));
          return -1;
      }
  
      while(1)
      {
          char pcContent[50];
          read(real_fd,pcContent,50);
  
          fprintf(stdout, "Read finish!\n");
      }
  
      close(real_fd);
      close(listen_fd);
      return 0;
  }

    client端:

  #include
  #include
  #include
  #include
  #include
  #include
  #include
  #include
  #include
  #include
  #include
  #include
  #include
  
  int setSendBuf(int socketFd, int size)
  {
      int valSize = sizeof(size);
      if(setsockopt(socketFd, SOL_SOCKET, SO_SNDBUF, &size, (socklen_t)valSize) < 0)
      {   
          fprintf(stderr, "Set send buf error!\n");
          return -1; 
      }   
  
      return 0;
  
  }
  
  int getSendBuf(int fd) 
  {
      int bufSize = 0;
      int size = sizeof(bufSize);
      if(getsockopt(fd, SOL_SOCKET, SO_SNDBUF, &bufSize, (socklen_t*)&size) < 0)
      {   
          fprintf(stderr, "Get send buf error!\n");
  
          return -1; 
      }   
      
      fprintf(stdout, "Send buffer size = %d\n", bufSize);
  
      return 0;
  }
  
  
  
  int main(int argc, char** argv)
  {
      char *serverIp = "127.0.0.1";
      int serverPort = 3602;
  
      int send_sk;
      struct sockaddr_in s_addr;
      socklen_t len = sizeof(s_addr);
      send_sk = socket(AF_INET, SOCK_STREAM, 0);
      if(send_sk == -1)
      {
          fprintf(stderr, "errno=%d,errmsg=%s\n", errno, strerror(errno));
          return -1;
      }
  
      int bufSize = 8192;
      if(setSendBuf(send_sk, bufSize) == -1 || getSendBuf(send_sk) == -1)
      {
          exit(1);
      }
  
      bzero(&s_addr, sizeof(s_addr));
      s_addr.sin_family = AF_INET;
      inet_pton(AF_INET,serverIp, &s_addr.sin_addr);
      s_addr.sin_port = htons(serverPort);
  
      if(connect(send_sk,(struct sockaddr*)&s_addr,len) == -1)
      {
          fprintf(stderr, "errno=%d,errmsg=%s\n", errno, strerror(errno));
          return -1;
      }
  
      int bufSize = 8192;
      if(setSendBuf(send_sk, bufSize) == -1 || getSendBuf(send_sk) == -1)
      {
          exit(1);
      }
  
      bzero(&s_addr, sizeof(s_addr));
      s_addr.sin_family = AF_INET;
      inet_pton(AF_INET,serverIp, &s_addr.sin_addr);
      s_addr.sin_port = htons(serverPort);
  
      if(connect(send_sk,(struct sockaddr*)&s_addr,len) == -1)
      {
          fprintf(stderr, "errno=%d,errmsg=%s\n", errno, strerror(errno));
          return -1;
      }
  
      char pcContent[1024]={0};
      for(int cnt = 0; cnt < 6; ++cnt)
      {
          write(send_sk,pcContent,1024);
      }
  
      fprintf(stdout ,"send finish!\n");
      sleep(5);
      close(send_sk);
  }

你可能感兴趣的:(计算机网络)