關於“端口重用引發的慘案”的思考

讀了“端口重用引發的慘案”(http://csrd.aliapp.com/?p=1195),決定對SO_REUSEADDR做一次深入的學習。


1、基礎

SO_REUSEADDR可以用在以下四種情況下。(摘自《Unix網絡編程》卷一,即UNPv1)
1、當有一個有相同本地地址和端口的socket1處於TIME_WAIT狀態時,而你啓動的程序的socket2要佔用該地址和端口,你的程序就要用到該選項。
2、SO_REUSEADDR允許同一port上啓動同一服務器的多個實例(多個進程)。但每個實例綁定的IP地址是不能相同的。在有多塊網卡或用IP Alias技術的機器可以測試這種情況。
3、SO_REUSEADDR允許單個進程綁定相同的端口到多個socket上,但每個socket綁定的ip地址不同。這和2很相似,區別請看UNPv1。
4、SO_REUSEADDR允許完全相同的地址和端口的重複綁定。但這隻用於UDP的多播,不用於TCP。


2、相關代碼

inet_csk_bind_conflict()這一函數在bind()中被調用,用於檢測是否與已存在的sock衝突。

int inet_csk_bind_conflict(const struct sock *sk,
                           const struct inet_bind_bucket *tb)
{
        const __be32 sk_rcv_saddr = inet_rcv_saddr(sk);
        struct sock *sk2;
        struct hlist_node *node;
        int reuse = sk->sk_reuse;

        /*
         * Unlike other sk lookup places we do not check
         * for sk_net here, since _all_ the socks listed
         * in tb->owners list belong to the same net - the
         * one this bucket belongs to.
         */

        sk_for_each_bound(sk2, node, &tb->owners) {
                if (sk != sk2 &&
                    !inet_v6_ipv6only(sk2) &&
                    (!sk->sk_bound_dev_if ||
                     !sk2->sk_bound_dev_if ||
                     sk->sk_bound_dev_if == sk2->sk_bound_dev_if)) {
                        if (!reuse || !sk2->sk_reuse ||
                            sk2->sk_state == TCP_LISTEN) {
                                const __be32 sk2_rcv_saddr = inet_rcv_saddr(sk2);
                                if (!sk2_rcv_saddr || !sk_rcv_saddr ||
                                    sk2_rcv_saddr == sk_rcv_saddr)
                                        break;
                        }
                }
        }
        return node != NULL;
}
從代碼中可以看出,必須判斷reuse、sk2->sk_reuse、sk2->sk_state這三個量(如果一個sock上設置了SO_REUSEADDR標誌,則reuse=1),所以如果reuse=1、sk2->sk_reuse=1、sk2->sk_state != TCP_LISTEN(表示在同樣的IP和端口上,沒有正在監聽),我們就可以重用完全相同的IP+端口號。看下面兩種情況。


3、情況一

如果服務器1的A端口(監聽端口,設置SO_REUSEADDR)與服務器2的B端口(非監聽端口)建立連接。關閉A端口(並保持兩者的連接不斷開),重新打開A端口(同樣設置SO_REUSEADDR)進行監聽,則可成功。(滿足A端口新的sock reuse=1;原連接的A端口的sock reuse=1,且狀態爲連接、不爲TCP_LISTEN,所以不衝突)


4、情況二

如果服務器1的A端口(非監聽端口)與服務器2的B端口(爲監聽端口)建立連接,這時如果想在服務器1上啓動一個進程,並以A端口作爲listen端口,就會出錯了,即使使用SO_REUSEADDR也不行。(滿足原A端口處於連接,不爲TCP_LISTEN;但即使滿足準備監聽的A端口設置reuse=1,但是原A端口連接對應的sock其reuse!=1。)因爲一般情況下,服務器1去與服務器2的B端口進行連接時,服務器1上的端口是自由分配的,該自由分配端口即爲A端口(在這個sock上,可沒有設置SO_REUSEADDR)。


5、附錄:關於情況一的測試程序

這段代碼是在別人的代碼上加以修改的,代碼較亂,見諒。

//本例用於測試SO_REUSEADDR的作用,主要是兩方面的作用:
//(1)允許重啓的監聽服務器bind其衆所周知端口,即使以前建立的將該端口用作它們本地端口的連接
//仍存在。本文件中的子進程保持連接工程中,父進程關閉listensocket,並重新建立socket,
//bind,listen。模擬父進程崩潰並重啓,子進程仍在執行的情況。
//(2)允許進程綁定一個處於TIME_WAIT的端口。本程序運行一次後,連接進入time_wait,
//繼續執行本程序,可以正常執行。

//客戶端程序可以使用命令:telnet localhost 10013
//連接狀態查詢,可使用命令:netstat -na | head -n 10
#include <netinet/in.h> 
#include <sys/socket.h> 
#include <time.h> 
#include <stdio.h> 
#include <stdlib.h>
#include <string.h> 
 #include<unistd.h>

#define MAXLINE 100 

int main(int argc, char** argv) 
{ 
   int listenfd,connfd; 
   struct sockaddr_in servaddr; 
   char buff[MAXLINE+1]; 
   time_t ticks; 
   unsigned short port; 
   int len=sizeof(int); 

   port=10013; 
   if( (listenfd=socket(AF_INET,SOCK_STREAM,0)) == -1) 
   { 
     perror("socket"); 
     exit(1); 
   } else
	printf("listenfd = %d\n",listenfd);
   bzero(&servaddr,sizeof(servaddr)); 
   servaddr.sin_family=AF_INET; 
   servaddr.sin_addr.s_addr=htonl(INADDR_ANY); 
   servaddr.sin_port=htons(port); 
	int flag = 1;
   if( setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &flag, len) == -1) 
   { 
      perror("setsockopt"); 
      exit(1); 
   } 
   if( bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) == -1) 
   { 
      perror("bind"); 
      exit(1); 
   } 
   else 
      printf("bind call OK!\n"); 
   if( listen(listenfd,5) == -1) 
   { 
      perror("listen"); 
      exit(1); 
   } 
//   for(;;) 
//   { 
      if( (connfd=accept(listenfd,(struct sockaddr*)NULL,NULL)) == -1)

      { 
          perror("accept"); 
          exit(1); 
      } 
      if( fork() == 0)/*child process*/ 
      { 
        close(listenfd);/*這句不能少,原因請大家想想就知道了。*/ 
        ticks=time(NULL); 
        snprintf(buff,100,"%.24s\n",ctime(&ticks)); 
         
        
        sleep(20); write(connfd,buff,strlen(buff));close(connfd);
//        execlp("f1-9d",NULL); 
//        perror("execlp"); 
        exit(1); 
     } 
     close(connfd); 
   
     sleep(5);
     close(listenfd);
sleep(5);
if( (listenfd=socket(AF_INET,SOCK_STREAM,0)) == -1) 
   { 
     perror("socket"); 
     exit(1); 
   } 
   bzero(&servaddr,sizeof(servaddr)); 
   servaddr.sin_family=AF_INET; 
   servaddr.sin_addr.s_addr=htonl(INADDR_ANY); 
   servaddr.sin_port=htons(port); 
   if( setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &flag, len) == -
1) 
   { 
      perror("setsockopt"); 
      exit(1); 
   } 
   if( bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) == 
-1) 
   { 
      perror("bind"); 
      exit(1); 
   } 
   else 
      printf("bind call OK!\n"); 
   if( listen(listenfd,5) == -1) 
   { 
      perror("listen"); 
      exit(1); 
   } else{
printf("rebind and relisten call OK! listenfd = %d\n", listenfd); 
}

sleep(30);
     exit(0);/* end parent*/ 
//  }
}

這段程序中,進程先是listen,該sock設置SO_REUSEADDR;當有連接到來時,子進程關閉listenfd,但仍保留這個連接。父進程又一次新建listen sock,同樣設置了SO_REUSEADDR,滿足條件,綁定成功。


6、附錄:關於情況2的測試程序

其實將情況1的測試程序稍加改造即可。主要是將第一次對setsockopt()的調用刪掉。不過這個測試跟情況2所述稍有不同,但是效果是一樣的。

這樣之後,進程先是listen,該sock未設置SO_REUSEADDR;當有連接到來時,子進程關閉listenfd,但仍保留這個連接。父進程又一次新建listen sock,但是即使設置了SO_REUSEADDR,綁定也會失敗(bind: Address already in use)。


7、總結

1、在listenfd上設置了SO_REUSEADDR之後,該listen套接字具有reuse屬性;之後所有的連接產生的套接字(sock)也具有reuse屬性。

2、“端口重用引發的慘案”這篇文章說“設置SO_RUSEADDR爲1是無效的”;但是實際上,不是無效,而是你做不到設置SO_RUSEADDR。客戶端向服務器80端口發起連接,客戶端上臨時分配了一個端口A,對應建立起來的sock是沒有reuse屬性的(因爲是臨時端口,我們也沒有爲他設置SO_REUSEADDR)。之後我們想在客戶端上啓動一個進程,該進程以端口A爲監聽端口,這當然是不滿足前述條件的。所以說這篇文章中的說法是有點問題的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章