🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
在具体分析之前,我们先看下socket(7)的man文档对这个参数是怎么介绍的: ~~~ SO_REUSEADDR Indicates that the rules used in validating addresses supplied in a bind(2) call should allow reuse of local addresses.For AF_INET sockets this means that a socket may bind, except when there is an active listening socket bound to the address. When the listening socket is bound to INADDR_ANY with a spe‐ cific port then it is not possible to bind to this port for any local address.Argument is an integer boolean flag. ~~~ ### 从这段文档中我们可以知道三个事: 1. 使用这个参数后,bind操作是可以重复使用local address的,注意,这里说的是local address,即ip加端口组成的本地地址,也就是说,两个本地地址,如果有任意ip或端口部分不一样,它们本身就是可以共存的,不需要使用这个参数。 2. 当local address被一个处于listen状态的socket使用时,加上该参数也不能重用这个地址。 3. 当处于listen状态的socket监听的本地地址的ip部分是INADDR\_ANY,即表示监听本地的所有ip,即使使用这个参数,也不能再bind包含这个端口的任意本地地址,这个和 2 中描述的其实是一样的。 好,接下来我们看几个例子。 上文 1 中说,只要本地地址不一样(ip或端口不一样),即使没有这个参数,两个地址也是可以同时使用的,我们来看下是不是这样。 下面是客户端的测试代码: ~~~ #include <arpa/inet.h> #include <assert.h> #include <netinet/in.h> #include <stdio.h> #include <strings.h> #include <sys/socket.h> #include <sys/types.h> #include <unistd.h> static int tcp_connect(char *ip, int port) { int sfd, err; struct sockaddr_in addr; sfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); assert(sfd != -1); // 先bind本地地址 bzero(&addr, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_addr.s_addr = inet_addr(ip); addr.sin_port = htons(port); err = bind(sfd, (struct sockaddr *)&addr, sizeof(addr)); assert(!err); // 再连接目标服务器 bzero(&addr, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_addr.s_addr = inet_addr("127.0.0.1"); addr.sin_port = htons(7777); err = connect(sfd, (struct sockaddr *)&addr, sizeof(addr)); assert(!err); return sfd; } int main(int argc, char *argv[]) { // bind本地地址:127.0.0.1:8888 tcp_connect("127.0.0.1", 8888); // bind本地地址:192.168.3.187:8888 tcp_connect("192.168.3.187", 8888); printf("两个连接同时建立成功\n"); sleep(100); return 0; } ~~~ 该段代码中,会先绑定本地地址,再连接目标服务器,由上可见,两次连接bind的本地地址中,ip部分是不同的,所以这两个bind操作应该是成功的。 我们用以下ncat命令模拟服务端: ~~~ $ ncat -lk4 7777 ~~~ 用ss命令查看有关7777端口的所有socket状态: ~~~ $ ss -antp | grep 7777 LISTEN 0 10 0.0.0.0:7777 0.0.0.0:\* users:(("ncat",pid=19208,fd=3)) ~~~ 由上可见,此时只有ncat服务端在监听7777端口,没有任何其他连接。 我们执行上面的程序,然后再次查看7777端口所有socket状态: ~~~ $ ss -antp | grep 7777 LISTEN 0 10 0.0.0.0:7777 0.0.0.0 : *users : (("ncat", pid = 19208, fd = 3)) ESTAB 0 0 127.0.0.1 : 7777 192.168.3.187 : 8888 users : (("ncat", pid = 19208, fd = 5)) ESTAB 0 0 127.0.0.1 : 7777 127.0.0.1 : 8888 users : (("ncat", pid = 19208, fd = 4)) ESTAB 0 0 192.168.3.187 : 8888 127.0.0.1 : 7777 users : (("a.out", pid = 19340, fd = 4)) ESTAB 0 0 127.0.0.1 : 8888 127.0.0.1 : 7777 users : (("a.out", pid = 19340, fd = 3)) ~~~ 由上可以看到,这两个连接的确是建立成功了。 上面命令输出中,有4个ESTAB状态的连接,这是正常的,因为这分别是从服务端角度和客户端的角度得到的输出。 前三行是从服务器角度来看的,后两行是从客户端角度来看的,这个从后面的进程名也可以看出。 对客户端来说,在connect之前可以bind不同本地地址,然后连同一目标,对服务端来说也是可以的,在listen之前,完全可以bind不同的本地地址,不需要SO\_REUSEADDR参数也可以成功,由于程序代码差不多,这里我们就不演示了。 我们下面再来看下connect之前,bind相同地址的情况,下面是测试代码: ~~~ #include <arpa/inet.h> #include <assert.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <strings.h> #include <sys/socket.h> #include <sys/types.h> #include <unistd.h> static int tcp_connect(char *ip, int port) { int sfd, err; char buf[1024]; struct sockaddr_in addr; sfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); assert(sfd != -1); // 先bind本地地址 bzero(&addr, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_addr.s_addr = inet_addr("127.0.0.1"); addr.sin_port = htons(8888); err = bind(sfd, (struct sockaddr *)&addr, sizeof(addr)); if (err) { sprintf(buf, "bind(%s:%d)", ip, port); perror(buf); exit(-1); } // 再连接目标服务器 bzero(&addr, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_addr.s_addr = inet_addr(ip); addr.sin_port = htons(port); err = connect(sfd, (struct sockaddr *)&addr, sizeof(addr)); assert(!err); return sfd; } int main(int argc, char *argv[]) { // 连接的目标地址:127.0.0.1:7777 tcp_connect("127.0.0.1", 7777); // 连接的目标地址:127.0.0.1:7778 tcp_connect("127.0.0.1", 7778); printf("两个连接同时建立成功\n"); sleep(100); return 0; } ~~~ 该程序会在connect之前,bind本地地址到127.0.0.1:8888,然后再连接目标地址,两次目标地址分别是127.0.0.1:7777和127.0.0.1:7778。 还是用ncat模拟服务端,只是这次要开两个。 服务端7777: ~~~ $ ncat -lk4 7777 ~~~ 服务端7778: ~~~ $ ncat -lk4 7778 ~~~ 运行客户端代码: ~~~ $ gcc client.c && ./a.out bind(127.0.0.1:7778): Address already in use ~~~ 由上可见,第二次连接是失败了的,因为127.0.0.1:8888本地地址已经被第一次connect使用过了。 此时,加上SO\_REUSEADDR参数应该是可以解决这个问题的。 ~~~ #include <arpa/inet.h> #include <assert.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <strings.h> #include <sys/socket.h> #include <sys/types.h> #include <unistd.h> static int tcp_connect(char *ip, int port) { int sfd, opt, err; char buf[1024]; struct sockaddr_in addr; sfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); assert(sfd != -1); // 先设置SO_REUSEADDR opt = 1; err = setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); assert(!err); // 再bind本地地址 bzero(&addr, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_addr.s_addr = inet_addr("127.0.0.1"); addr.sin_port = htons(8888); err = bind(sfd, (struct sockaddr *)&addr, sizeof(addr)); if (err) { sprintf(buf, "bind(%s:%d)", ip, port); perror(buf); exit(-1); } // 然后连接目标服务器 bzero(&addr, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_addr.s_addr = inet_addr(ip); addr.sin_port = htons(port); err = connect(sfd, (struct sockaddr *)&addr, sizeof(addr)); assert(!err); return sfd; } int main(int argc, char *argv[]) { // 连接的目标地址:127.0.0.1:7777 tcp_connect("127.0.0.1", 7777); // 连接的目标地址:127.0.0.1:7778 tcp_connect("127.0.0.1", 7778); printf("两个连接同时建立成功\n"); sleep(100); return 0; } ~~~ 再次编译后执行: ~~~ $ gcc client.c && ./a.out 两个连接同时建立成功 ~~~ 由上可以看到,这两次连接都成功了,SO\_REUSEADDR允许我们重复bind相同的本地地址。 **细心的同学可能会发现,为什么两次连接的目标地址是不同的呢?** 我们来把它改成相同的试下: ~~~ #include <arpa/inet.h> #include <assert.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <strings.h> #include <sys/socket.h> #include <sys/types.h> #include <unistd.h> static int tcp_connect(char *ip, int port) { int sfd, opt, err; char buf[1024]; struct sockaddr_in addr; sfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); assert(sfd != -1); // 先设置SO_REUSEADDR opt = 1; err = setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); assert(!err); // 再bind本地地址 bzero(&addr, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_addr.s_addr = inet_addr("127.0.0.1"); addr.sin_port = htons(8888); err = bind(sfd, (struct sockaddr *)&addr, sizeof(addr)); if (err) { sprintf(buf, "bind(%s:%d)", ip, port); perror(buf); exit(-1); } // 然后连接目标服务器 bzero(&addr, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_addr.s_addr = inet_addr(ip); addr.sin_port = htons(port); err = connect(sfd, (struct sockaddr *)&addr, sizeof(addr)); if (err) { sprintf(buf, "connect(%s:%d)", ip, port); perror(buf); exit(-1); } return sfd; } int main(int argc, char *argv[]) { // 连接的目标地址:127.0.0.1:7777 tcp_connect("127.0.0.1", 7777); // 连接的目标地址:127.0.0.1:7777 tcp_connect("127.0.0.1", 7777); printf("两个连接同时建立成功\n"); sleep(100); return 0; } ~~~ 此时,执行该程序,命令行会有如下输出: ~~~ $ gcc client.c && ./a.out connect(127.0.0.1:7777): Cannot assign requested address ~~~ 为什么呢?因为这两次连接都是从127.0.0.1:8888 到 127.0.0.1:7777的,**这个在tcp层面是不允许的,即使加了SO\_REUSEADDR参数也不行。** 本地地址和目标地址组成的元组唯一确定一个tcp连接,上面程中的两次连接本地地址和目标地址都一样,已经违背了唯一的原则。 对应内核相应检查代码如下: ~~~ // net/ipv4/inet_hashtables.c static int __inet_check_established(struct inet_timewait_death_row *death_row, struct sock *sk, __u16 lport, struct inet_timewait_sock **twp) { struct inet_hashinfo *hinfo = death_row->hashinfo; struct inet_sock *inet = inet_sk(sk); __be32 daddr = inet->inet_rcv_saddr; __be32 saddr = inet->inet_daddr; ... const __portpair ports = INET_COMBINED_PORTS(inet->inet_dport, lport); unsigned int hash = inet_ehashfn(net, daddr, lport, saddr, inet->inet_dport); struct inet_ehash_bucket *head = inet_ehash_bucket(hinfo, hash); ... struct sock *sk2; ... sk_nulls_for_each(sk2, node, &head->chain) { ... if (likely(INET_MATCH(sk2, net, acookie, saddr, daddr, ports, dif, sdif))) { ... goto not_unique; } } ... not_unique: ... return -EADDRNOTAVAIL; } ~~~ 如果本地地址和目标地址组成的元组之前已经存在了,则返回错误码EADDRNOTAVAIL,这个错误码对应的解释为: ~~~ // include/uapi/asm-generic/errno.h #define EADDRNOTAVAIL 99 /\* Cannot assign requested address \*/ ~~~ 正好和上面执行程序输出的错误信息一样。 我们再回到对SO\_REUSEADDR参数的讨论。 上面代码中,两个connect使用相同的本地地址,只要加上SO\_REUSEADDR参数是可以的,那两个listen行吗? **看代码: ** ~~~ #include <arpa/inet.h> #include <assert.h> #include <stdio.h> #include <stdlib.h> #include <strings.h> #include <sys/socket.h> #include <sys/types.h> #include <unistd.h> static int tcp_listen(char *ip, int port) { int lfd, opt, err; char buf[1024]; struct sockaddr_in addr; lfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); assert(lfd != -1); opt = 1; err = setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); assert(!err); bzero(&addr, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_addr.s_addr = inet_addr(ip); addr.sin_port = htons(port); err = bind(lfd, (struct sockaddr *)&addr, sizeof(addr)); if (err) { sprintf(buf, "bind(%s:%d)", ip, port); perror(buf); exit(-1); } err = listen(lfd, 8); assert(!err); return lfd; } int main(int argc, char *argv[]) { tcp_listen("127.0.0.1", 7777); tcp_listen("127.0.0.1", 7777); return 0; } ~~~ 该代码执行之后输出如下: ~~~ $ gcc server.c && ./a.out bind(127.0.0.1:7777): Address already in use ~~~ 由上可见,即使加上SO\_REUSEADDR参数,两个listen也是不行的。 其实,这个在最开始的man文档中已经说过了,只要有listen占了一个本地地址,其他任何操作都不能再使用这个地址了。 我们对应看下内核源码: ~~~ // net/ipv4/inet_connection_sock.c static int inet_csk_bind_conflict(const struct sock *sk, const struct inet_bind_bucket *tb, bool relax, bool reuseport_ok) { struct sock *sk2; bool reuse = sk->sk_reuse; ... sk_for_each_bound(sk2, &tb->owners) { if (sk != sk2 && ...) { if ((!reuse || !sk2->sk_reuse || sk2->sk_state == TCP_LISTEN) && ...) { if (inet_rcv_saddr_equal(sk, sk2, true)) break; } ... } } return sk2 != NULL; } ~~~ 该方法就是用来判断本地地址是否可以重复使用的代码。 如果该方法最终sk2不为null,则最终会返回错误码EADDRINUSE给用户,即我们上面程序执行之后的错误输出。 我们来看下sk2什么时候不为null。 在我们的新socket和sk2本地地址相同时,如果新socket没有设置SO\_REUSEADDR参数,或者sk2没设置SO\_REUSEADDR参数,或者sk2为listen状态,sk2最终都会不为null,也就是说,新socket的本地地址在这些情况下都不可重复使用。 和man文档中说的基本是一样的。 那我们在平时写服务器时,为什么要加上这个参数呢?我们都是先关闭服务器,再开的啊,以前那个listen的socket,以及所有当时正在连接的socket,应该都已经关闭了啊?应该不会存在相同的本地地址了啊? ### 为什么呢? 这要再说起tcp的TIME\_WAIT状态。 我们知道,在tcp连接中,主动发起关闭请求的那一端会最终进入TIME\_WAIT状态,被动关闭连接的那一端会直接进入CLOSE状态,即socket和它占用的资源会直接销毁。 ***** 假设,在我们关闭服务器之前,先把客户端都关闭掉,再关闭服务器,此时服务器的所有socket都直接进入CLOSE状态了,它们占用的本地地址等也都立即可用,此时如果我们马上开服务器,是不会出现 Address already in use 这个错误的。 但当我们在有客户端连接的情况下,直接关闭服务器,也就是说,对所有现有的tcp连接,服务端都主动发起了关闭请求,此时,这些连接就会进入TIME\_WAIT状态,一直占用服务器使用的本地地址,不让后续操作使用。 这种情况下,你再开服务器,就会出现上面那个 Address already in use 错误,这也是我们写服务器时经常会遇到的错误。 ***** **解决这个问题的方法就是设置SO\_REUSEADDR参数。** 由上面的inet\_csk\_bind\_conflict方法可以看到,如果设置了SO\_REUSEADDR参数,新socket和旧socket的reuse值都会为true,而旧socket此时处于TIME\_WAIT状态,所以后续不会调用inet\_rcv\_saddr\_equal方法,判断两个地址是否相同。 这样最终sk2也会为null,也就是说,内核允许新socket使用这个地址。 用代码验证下: ~~~ #include <arpa/inet.h> #include <assert.h> #include <stdio.h> #include <stdlib.h> #include <strings.h> #include <sys/socket.h> #include <sys/types.h> #include <unistd.h> static int tcp_listen(char *ip, int port) { int lfd, opt, err; char buf[1024]; struct sockaddr_in addr; lfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); assert(lfd != -1); opt = 1; err = setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); assert(!err); bzero(&addr, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_addr.s_addr = inet_addr(ip); addr.sin_port = htons(port); err = bind(lfd, (struct sockaddr *)&addr, sizeof(addr)); if (err) { sprintf(buf, "bind(%s:%d)", ip, port); perror(buf); exit(-1); } err = listen(lfd, 8); assert(!err); return lfd; } int main(int argc, char *argv[]) { int lfd, cfd; lfd = tcp_listen("127.0.0.1", 7777); printf("5秒钟之后将关闭第一次listen的socket,请于此期间发起一次tcp连接\n"); sleep(5); cfd = accept(lfd, NULL, NULL); assert(cfd != -1); close(cfd); close(lfd); tcp_listen("127.0.0.1", 7777); printf("第二次listen操作成功\n"); return 0; } ~~~ 按照程序提示,对服务端发起tcp连接,最终服务端输出如下: ~~~ $ gcc server.c && ./a.out 5秒钟之后将关闭第一次listen的socket,请于此期间发起一次tcp连接 ~~~ 可见,有了SO\_REUSEADDR参数,即使我们先关闭的tcp连接,也是可以再次listen的。 有兴趣的朋友可以把设置SO\_REUSEADDR参数的代码去掉,然后再执行看下,理论上来说是会报错的。 到此为止,所有有关SO\_REUSEADDR参数内容都讲完了,希望对大家有所帮助。 完。