最近看了一篇socket编程:SO_REUSEADDR例解 的文章,写的比较详细,但对其中Richard Stevens的《Unix网络编程指南》中关于SO_REUSEADDR的讲解不甚清楚,自己做了个决定,花了两天的时间研究了一下,不算太深也不算太浅。
那篇文章的出处已找不到,还是非常感谢原作者,本文会借用一下那篇文章的一些内容,比较好的东西在于分享和应用吧~~~~
首先:
引用的内容:
首先声明一个问题:当两个socket的address和port相冲突,而你又想重用地
址和端口,则旧的socket和新的socket都要已经被设置了SO_REUSEADDR特性,只
有两者之一有这个特性还是有问题的。
SO_REUSEADDR可以用在以下四种情况下。
(摘自《Unix网络编程》卷一,即UNPv1)
1、当有一个有相同本地地址和端口的socket1处于TIME_WAIT状态时,而你启
动的程序的socket2要占用该地址和端口,你的程序就要用到该选项。 (这个是正确的)
2、SO_REUSEADDR允许同一port上启动同一服务器的多个实例(多个进程)。但
每个实例绑定的IP地址是不能相同的。在有多块网卡或用IP Alias技术的机器可
以测试这种情况。 (测试是不正确的)
3、SO_REUSEADDR允许单个进程绑定相同的端口到多个socket上,但每个soc
ket绑定的ip地址不同。这和2很相似,区别请看UNPv1。 (觉得不对,测试在此情况下不需要使用SO_REUSEADDR也都正常)
4、SO_REUSEADDR允许完全相同的地址和端口的重复绑定。但这只用于UDP的
多播,不用于TCP。引出本文的测试的基本点:测试SO_REUSEADDR和bind的关系,分为两部分:单进程---单个sock 和多个sock 和两个进程的情况。
主要内容:
测试过程中遇到的问题终结出得结论:
1)bind时必须是自己的本地地址,不然提示:#define EADDRNOTAVAIL 99 /* Cannot assign requested address */
2)先bind,后listen,最后connect到任意目的地址时,都会提示:#define EISCONN 106 /* Transport endpoint is already connected */
一)单进程
1)单个socket,主要引用的内容,稍有改动。代码如下:
#include <netinet/in.h> #include <sys/socket.h> #include <time.h> #include <stdio.h> #include <string.h> #include <stdlib.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 flag=1,len=sizeof(int); port=10013; 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); inet_pton(AF_INET, "192.168.6.28", &servaddr.sin_addr); servaddr.sin_port=htons(port); printf("%08x\n", INADDR_ANY); 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);/**//*这句不能少,原因请大家想想就知道了。*/ /* 因为端口已经被绑定和监听了,需要先释放,注意是在监听的情况下 才需要释放,如果光绑定不监听,两个socket都可以绑定同一个端口。*/ ticks=time(NULL); snprintf(buff,100,"%.24saa\r\n",ctime(&ticks)); write(connfd,buff,strlen(buff)); close(connfd); sleep(1); execlp("/home/ansx/ctest/apue/socket_option/reuseaddr_timewait",NULL); perror("execlp"); exit(1); } close(connfd); exit(0);/**//* end parent*/ } }
第一个终端:
[root@localhost socket_option]# ./reuseaddr_timewait
00000000
bind call OK!
[root@localhost socket_option]# 00000000
bind call OK!
第二个终端:[root@localhost ~]# telnet 192.168.6.28 10013
Trying 192.168.6.28...
Connected to bogon (192.168.6.28).
Escape character is '^]'.
Fri Sep 16 02:33:18 2011aa
Connection closed by foreign host.2)两个socket的情况,代码如下:编译:gcc -o reuseaddr_1proc2port reuseaddr_1proc2port.c -lpthread
#include <netinet/in.h> #include <sys/socket.h> #include <time.h> #include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <pthread.h> #define BUFSIZE 1024 #define MAXLINE 100 //#define TEST_LISTEN #define IPSTR1 "127.0.0.1" //#define IPSTR2 "127.0.0.1" #define IPSTR2 "192.168.6.28" //#define IPSTR1 "0.0.0.0" void process_fd(int, int); int main(int argc, char** argv) { int fd1,fd2; struct sockaddr_in servaddr1,servaddr2; char buff[MAXLINE+1]; time_t ticks; unsigned short port; int flag=1,len=sizeof(int); port=10013; if( (fd1=socket(AF_INET,SOCK_STREAM,0)) == -1) { perror("socket"); exit(1); } if( (fd2=socket(AF_INET,SOCK_STREAM,0)) == -1) { perror("socket"); exit(1); } bzero(&servaddr1,sizeof(servaddr1)); bzero(&servaddr2,sizeof(servaddr2)); servaddr1.sin_family=AF_INET; servaddr2.sin_family=AF_INET; if( inet_pton(AF_INET, IPSTR1, &servaddr1.sin_addr) <= 0) { printf("inet_pton() call error:127.0.0.1\n"); exit(1); } if( inet_pton(AF_INET, IPSTR2, &servaddr2.sin_addr) <= 0) { printf("inet_pton() call error:128.160.1.230\n"); exit(1); } servaddr1.sin_port=htons(port); servaddr2.sin_port=htons(port); #if 1 if( setsockopt(fd1, SOL_SOCKET, SO_REUSEADDR, &flag, len) == -1) { perror("setsockopt"); exit(1); } if( setsockopt(fd2, SOL_SOCKET, SO_REUSEADDR, &flag, len) == -1) { perror("setsockopt"); exit(1); } #endif if( bind(fd1,(struct sockaddr*)&servaddr1,sizeof(servaddr1)) == -1) { perror("bind fd1"); exit(1); } if( bind(fd2,(struct sockaddr*)&servaddr2,sizeof(servaddr2)) == -1) { perror("bind fd2"); exit(1); } printf("bind fd1 and fd2 OK!\n"); if( listen(fd1,5) == -1) { perror("listen fd1"); exit(1); } #ifdef TEST_LISTEN (aa) if( listen(fd2,5) == -1) { perror("listen fd2"); exit(1); } #endif /**//*put other process here*/ process_fd(fd1, fd2); getchar(); exit(0);/**//* end */ } void pthread_process(void *arg) { int listen_fd = (int)arg; struct sockaddr_in cli_addr; int conn_fd; char buff[100]; int len; int thread_id = pthread_self(); time_t ticks; while(1) { len = sizeof(cli_addr); conn_fd = accept(listen_fd, (struct sockaddr *)&cli_addr, (socklen_t *)&len); if (conn_fd == -1) { if (errno == EINVAL) { continue; } close(listen_fd); pthread_exit(NULL); } snprintf(buff, 100, "threadid = %08x,listen_fd = %d, cli_addr = %s, cli_port = %d\n", thread_id, listen_fd, inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port)); printf("%s", buff); ticks=time(NULL); snprintf(buff,100,"%.24s\r\n",ctime(&ticks)); write(conn_fd, buff, strlen(buff)); close(conn_fd); } pthread_exit(NULL); } void pthread_connect(void *arg) { int cli_fd = (int)arg; struct sockaddr_in serv_addr; int ret; char buf[BUFSIZE]; printf("pthread_connect fd = %d, serverip = %s\n", cli_fd, IPSTR1); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(10013); ret = inet_pton(AF_INET, IPSTR1, &serv_addr.sin_addr); if (ret == -1) { perror("inet_pton"); exit(1); } while (1) { printf("cli connect start\n"); ret = connect(cli_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); if (ret == -1) { if (errno == EINVAL) { continue; } printf("errno = %d\n", errno);//#define EISCONN 106 /* Transport endpoint is already connected */ perror("cli connect"); close(cli_fd); return ; } break; } printf("cli connect end\n"); while (1) { printf("cli recv start\n"); ret = recv(cli_fd, buf, BUFSIZE - 1, 0); if (ret < 0) { if (errno == EINTR) { continue; } perror("cli recv"); return ; } buf[ret] = '\0'; printf(buf); break; } printf("cli recv end\n"); return ; } void process_fd(int fd1, int fd2) { pthread_t td1,td2; int sock_fd1 = fd1; int sock_fd2 = fd2; pthread_create(&td1, NULL, (void *(*)(void*))pthread_process, (void *)fd1); #ifdef TEST_LISTEN pthread_create(&td2, NULL, (void *(*)(void*))pthread_process, (void *)fd2); #else pthread_create(&td2, NULL, (void *(*)(void*))pthread_connect, (void *)fd2); #endif printf("threadid = %08x, fd1 = %d, address = %s\n", td1, fd1, IPSTR1); printf("threadid = %08x, fd2 = %d, address = %s\n", td2, fd2, IPSTR2); }
分为两种情况:
a)两个sock bind的ip和port都相同,在程序文件头部修改:#define IPSTR1 "127.0.0.1" 和 #define IPSTR2 "127.0.0.1",下面的修改都是在此基础上。
i)
则两个sock都必须使用SO_REUSEADDR(只有一个使用不行),不然有#define EADDRINUSE 98 /* Address already in use */此错误。
如其中的#if 1改为#if 0,编译运行,结果如下:
[root@localhost socket_option]# ./reuseaddr_1proc2port bind fd2: Address already in use
ii)另个sock不能同时listen,如果把代码中的(aa)的去掉,则编译运行,结果如下:
[root@localhost socket_option]# ./reuseaddr_1proc2port bind fd1 and fd2 OK! listen fd2: Address already in use
iii)直接编译运行,结果如下:
第一个端口:
[root@localhost socket_option]# ./reuseaddr_1proc2port bind fd1 and fd2 OK! threadid = b7f41b90, fd1 = 3, address = 127.0.0.1 threadid = b7540b90, fd2 = 4, address = 127.0.0.1 pthread_connect fd = 4, serverip = 127.0.0.1 cli connect start cli connect end cli recv start(从这可以看到没有recv 结果,比较奇怪,先不管那么多了) -----自己listen,自己connect自己,就会这样了,从状态上看是ESTABLISHED.但没有数据交互。 threadid = b7f41b90,listen_fd = 3, cli_addr = 127.0.0.1, cli_port = 44370
第二个端口:
[root@localhost socket_option]# telnet 127.0.0.1 10013 Trying 127.0.0.1... Connected to localhost.localdomain (127.0.0.1). Escape character is '^]'. Fri Sep 16 02:58:55 2011(这是收到的结果) Connection closed by foreign host.
第三个端口:
[root@localhost ~]# netstat -anp | grep :10013 tcp 0 0 127.0.0.1:10013 0.0.0.0:* LISTEN 20157/reuseaddr_1pr tcp 0 0 127.0.0.1:10013 127.0.0.1:10013 ESTABLISHED 20157/reuseaddr_1pr (这个也比较奇怪)----显示连接,但没有数据交互。注意只有一行ESTABLISHED,
b)两个sock bind的ip地址不同,port相同,此时经测试无须使用SO_REUSEADDR都正常。这两个sock既可以都作为客户端,也可以都作为服务器,另外两个也可以一个sock作为服务器,另一个 sock 作为客户端。都正常。
i)
一个sock作为服务器,一个sock作为客户端,同时这个客户端去向这个服务器端请求。编译测试结果如下:
第一个终端:
[root@localhost socket_option]# ./reuseaddr_1proc2port bind fd1 and fd2 OK! threadid = b7fe2b90, fd1 = 3, address = 127.0.0.1 threadid = b75e1b90, fd2 = 4, address = 192.168.6.28 pthread_connect fd = 4, serverip = 127.0.0.1 cli connect start cli connect end cli recv start threadid = b7fe2b90,listen_fd = 3, cli_addr = 192.168.6.28, cli_port = 10013 Fri Sep 16 03:16:27 2011 cli recv end threadid = b7fe2b90,listen_fd = 3, cli_addr = 127.0.0.1, cli_port = 35187
第二个终端:
[root@localhost socket_option]# telnet 127.0.0.1 10013 Trying 127.0.0.1... Connected to localhost.localdomain (127.0.0.1). Escape character is '^]'. Fri Sep 16 03:16:42 2011 Connection closed by foreign host.
第三个终端:
[root@localhost ~]# netstat -anp | grep :10013 tcp 0 0 127.0.0.1:10013 0.0.0.0:* LISTEN 20737/reuseaddr_1pr tcp 0 0 192.168.6.28:10013 127.0.0.1:10013 CLOSE_WAIT 20737/reuseaddr_1pr ---这是192的c一直没关闭,而127s已经关闭。等待本地的socket关闭。 tcp 0 0 127.0.0.1:10013 192.168.6.28:10013 FIN_WAIT2 - (这是什么情况,难得一见啊)----这是127的s先关闭socket,而192的c一直没关闭。 tcp 0 0 127.0.0.1:10013 127.0.0.1:10013 TIME_WAIT -
ii) 两个sock都作为服务器端,这是需要添加 #define TEST_LISTEN ,编译运行:
第一个终端:
[root@localhost socket_option]# ./reuseaddr_1proc2port bind fd1 and fd2 OK! threadid = b7f44b90, fd1 = 3, address = 127.0.0.1 threadid = b7543b90, fd2 = 4, address = 192.168.6.28 threadid = b7f44b90,listen_fd = 3, cli_addr = 127.0.0.1, cli_port = 53069 threadid = b7543b90,listen_fd = 4, cli_addr = 192.168.6.28, cli_port = 53553
第二个终端:Connected to localhost.localdomain (127.0.0.1). Escape character is '^]'. Fri Sep 16 03:22:59 2011 Connection closed by foreign host. [root@localhost socket_option]# telnet 192.168.6.28 10013 Trying 192.168.6.28... Connected to bogon (192.168.6.28). Escape character is '^]'. Fri Sep 16 03:23:13 2011 Connection closed by foreign host.
第三个终端:[root@localhost ~]# netstat -anp | grep :10013 tcp 0 0 192.168.6.28:10013 0.0.0.0:* LISTEN 20941/reuseaddr_1pr tcp 0 0 127.0.0.1:10013 0.0.0.0:* LISTEN 20941/reuseaddr_1pr tcp 0 0 192.168.6.28:10013 192.168.6.28:53553 TIME_WAIT - tcp 0 0 127.0.0.1:10013 127.0.0.1:53069 TIME_WAIT -
二) 测试两个进程的情况,比较简单,测试结果及过程:
在终端执行命令:ip addr add 192.168.1.234/24 dev eth0(相当于ip alias) 然后把reuseaddr_timewait.c的程序的服务器地址修改成192.168.6.28 和 192.168.1.232 (并且去掉SO_REUSEDADDR)分别编译为a1a.c和a2a.c并运行,显示结果正常。
另外在有两个网卡的情况下也测试不用SO_REUSEDADDR也正常。
ok了,寥寥数语,一切在代码中见分晓。下班回家了。