讀了“端口重用引發的慘案”(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爲監聽端口,這當然是不滿足前述條件的。所以說這篇文章中的說法是有點問題的。