关于“端口重用引发的惨案”的思考

读了“端口重用引发的惨案”(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为监听端口,这当然是不满足前述条件的。所以说这篇文章中的说法是有点问题的。

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