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<stdlib.h>
  #include<stdio.h>
  #include<sys/types.h>
  #include<sys/socket.h>
  #include<netinet/in.h>
  #include<arpa/inet.h>
  #include<string.h>
  #include<unistd.h>
  #include<errno.h>
  #include<fcntl.h>
  #include<signal.h>
  #include<string.h>
  #include<netinet/tcp.h>
  
  #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<stdlib.h>
  #include<stdio.h>
  #include<sys/types.h>
  #include<sys/socket.h>
  #include<netinet/in.h>
  #include<arpa/inet.h>
  #include<string.h>
  #include<unistd.h>
  #include<errno.h>
  #include<fcntl.h>
  #include<signal.h>
  #include<string.h>
  #include<netinet/tcp.h>
  
  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);
  }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章