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