reuseaddr和reuseport

reuseaddr和reuseport对于 reuseaddr 和 reuseport 的演进 可以参考这篇文章 本文主要基于 kernel 3 18 79 版本分析下这两个参数如何生效 SO REUSEADDR 解决 server 重启的问题 server 端调用 close client 还没有调用 close 则 server 端 socket 处于 FIN WAIT2 状态

大家好,我是讯享网,很高兴认识大家。

对于reuseaddr和reuseport的演进,可以参考这篇文章,本文主要基于kernel 3.18.79 版本分析下这两个参数如何生效。

SO_REUSEADDR

  1. 解决server重启的问题
    server端调用close,client还没有调用close,则server端socket处于FIN_WAIT2状态,持续时间60s,此时server重启会失败,在bind时会报错 Address already in use。
    或者server端先调用close,client后调用close,则server会处于TIME_WAIT状态,持续时间也是60s,此时server重启会失败,在bind时也会报错 Address already in use。
  1. 解决ip为零的通配符问题
    例如: socketA绑定了0.0.0.0:2222,socketB绑定10.164.129.22:2222时,或者 socketA绑定了10.164.129.22:2222,socketB绑定0.0.0.0:2222时,都会报错 Address already in use。因为0.0.0.0相当于通配符,可以匹配到10.164.129.22,在没有设置地址复用或者端口复用前就会有此问题。

解决方法:
方案1: 如果socketA调用bind后,又调用了listen,则fastreuse 会恢复为0(即使socketA在bind前设置了SO_REUSEADDR),此时即使socketB在bind前设置了SO_REUSEADDR也不管用。
socketA和socketB在bind前设置SO_REUSEADDR, 并且socketB必须在socketA调用listen前调用bind。
方案2: socketA和socketB在bind前均设置了SO_REUSEPORT。
方案3: socketB在调用bind前设置 setsockopt(fd, SOL_TCP, TCP_REPAIR, &status, sizeof(int)) 进行强制bind,而不用管socketA是什么状态。

SO_REUSEPORT
SO_REUSEPORT支持多个进程或者线程绑定到同一端口,提高服务器程序的性能,解决的问题:
a. 允许多个套接字 bind()/listen() 同一个TCP/UDP端口
每一个线程拥有自己的服务器套接字
在服务器套接字上没有了锁的竞争
b. 内核层面实现负载均衡
c. 安全层面,监听同一个端口的套接字只能位于同一个用户下面

如何设置
可以在调用bind绑定端口号之前,通过如下调用设置socket的reuseaddr或者reuseport

setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &status, sizeof(int)) setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &status, sizeof(int)) 

讯享网

kernel中对应代码如下

讯享网sock_setsockopt(struct socket *sock, int level, int optname, char __user *optval, unsigned int optlen) case SO_REUSEADDR: sk->sk_reuse = (valbool ? SK_CAN_REUSE : SK_NO_REUSE); break; case SO_REUSEPORT: sk->sk_reuseport = valbool; 

出问题场景下(报错 Address already in use),代码分析
a. socketA 调用bind时

//在内核中会调用inet_bind,接着调用inet_csk_get_port,在函数 //inet_csk_get_port中,先在bind hash表查找是否有其他socket绑 //定此port了,第一次bind肯定查找失败,跳转到tb_not_found have_snum: //在bind hash表中查找,hash key为net和local port head = &hashinfo->bhash[inet_bhashfn(net, snum, hashinfo->bhash_size)]; spin_lock(&head->lock); inet_bind_bucket_for_each(tb, &head->chain) if (net_eq(ib_net(tb), net) && tb->port == snum) goto tb_found; tb = NULL; goto tb_not_found; tb_not_found: ret = 1; //创建hash表项,并将表项添加到bind hash表中 if (!tb && (tb = inet_bind_bucket_create(hashinfo->bind_bucket_cachep, net, head, snum)) == NULL) goto fail_unlock; //第一次创建,并且是第一次调用,tb->owners为空 if (hlist_empty(&tb->owners)) { //sk如果设置了SO_REUSEADDR,并且socketA不为TCP_LISTEN状态, //则设置tb->fastreuse为1 if (sk->sk_reuse && sk->sk_state != TCP_LISTEN) tb->fastreuse = 1; else tb->fastreuse = 0; //sk如果设置了SO_REUSEPORT,则设置fastreuseport 为1,设置fastuid为uid if (sk->sk_reuseport) { tb->fastreuseport = 1; tb->fastuid = uid; } else tb->fastreuseport = 0; } else { //调用listen时,会再次调用inet_csk_get_port,此情况下才会走到此else分支。 if (tb->fastreuse && (!sk->sk_reuse || sk->sk_state == TCP_LISTEN)) tb->fastreuse = 0; if (tb->fastreuseport && (!sk->sk_reuseport || !uid_eq(tb->fastuid, uid))) tb->fastreuseport = 0; } success: //将tb赋给icsk_bind_hash if (!inet_csk(sk)->icsk_bind_hash) inet_bind_hash(sk, tb, snum); WARN_ON(inet_csk(sk)->icsk_bind_hash != tb); ret = 0; } 

b. socketA 调用listen时

讯享网sk->sk_state = TCP_LISTEN; if (!sk->sk_prot->get_port(sk, inet->inet_num)) { inet->inet_sport = htons(inet->inet_num); sk_dst_reset(sk); sk->sk_prot->hash(sk); return 0; } //在函数inet_csk_get_port中,先在bind hash表查找是否有其他 //socket绑定此port了,这次会查找成功,跳转到tb_found have_snum: //在bind hash表中查找,hash key为net和local port head = &hashinfo->bhash[inet_bhashfn(net, snum, hashinfo->bhash_size)]; spin_lock(&head->lock); inet_bind_bucket_for_each(tb, &head->chain) if (net_eq(ib_net(tb), net) && tb->port == snum) goto tb_found; tb = NULL; goto tb_not_found; tb_found: //不为空 if (!hlist_empty(&tb->owners)) { //没有设置此标志 if (sk->sk_reuse == SK_FORCE_REUSE) goto success; //fastreuse 和fastureseport都为0,走else分支 if (((tb->fastreuse > 0 && sk->sk_reuse && sk->sk_state != TCP_LISTEN) || (tb->fastreuseport > 0 && sk->sk_reuseport && uid_eq(tb->fastuid, uid))) && smallest_size == -1) { goto success; } else { ret = 1; //执行函数 inet_csk_bind_conflict,在调用listen情况下,会返回false,说明没有冲突的sk if (inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb, true)) { .... } } } tb_not_found: ret = 1; if (!tb && (tb = inet_bind_bucket_create(hashinfo->bind_bucket_cachep, net, head, snum)) == NULL) goto fail_unlock; //tb->owners不为空,执行else分支 if (hlist_empty(&tb->owners)) { //sk如果设置SO_REUSEADDR,并且socketA不为 //TCP_LISTEN状态,则设置tb->fastreuse为1 if (sk->sk_reuse && sk->sk_state != TCP_LISTEN) tb->fastreuse = 1; else tb->fastreuse = 0; //sk如果设置SO_REUSEPORT,则设置fastreuseport 为1,设置fastuid if (sk->sk_reuseport) { tb->fastreuseport = 1; tb->fastuid = uid; } else tb->fastreuseport = 0; } else { //调用listen时,会再次调用inet_csk_get_port,此情况下才会走到此else分支。 //如果socketA调用bind后,又调用了listen,则fastreuse 会恢复为0(即使socketA在bind前设 //置了SO_REUSEADDR)。此时即使socketB在bind前设置了SO_REUSEADDR也不管用。 //但是fastreuseport 不会被恢复为0, if (tb->fastreuse && (!sk->sk_reuse || sk->sk_state == TCP_LISTEN)) tb->fastreuse = 0; if (tb->fastreuseport && (!sk->sk_reuseport || !uid_eq(tb->fastuid, uid))) tb->fastreuseport = 0; } success: //icsk_bind_hash已经有值 if (!inet_csk(sk)->icsk_bind_hash) inet_bind_hash(sk, tb, snum); WARN_ON(inet_csk(sk)->icsk_bind_hash != tb); ret = 0; } int inet_csk_bind_conflict(const struct sock *sk, const struct inet_bind_bucket *tb, bool relax) { struct sock *sk2; int reuse = sk->sk_reuse; int reuseport = sk->sk_reuseport; kuid_t uid = sock_i_uid((struct sock *)sk); /* * 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, &tb->owners) { //因为是同一个sk,所以sk等于sk2,循环完后,sk2为NULL 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) && (!reuseport || !sk2->sk_reuseport || (sk2->sk_state != TCP_TIME_WAIT && !uid_eq(uid, sock_i_uid(sk2))))) { if (!sk2->sk_rcv_saddr || !sk->sk_rcv_saddr || sk2->sk_rcv_saddr == sk->sk_rcv_saddr) break; } if (!relax && reuse && sk2->sk_reuse && sk2->sk_state != TCP_LISTEN) { if (!sk2->sk_rcv_saddr || !sk->sk_rcv_saddr || sk2->sk_rcv_saddr == sk->sk_rcv_saddr) break; } } } return sk2 != NULL; } 

c. socketB 调用bind函数时,


讯享网

//在函数inet_csk_get_port中,先在bind hash表查找是否有其他 //socket绑定此port了,这次会查找成功,跳转到tb_found have_snum: //在bind hash表中查找,hash key为net和local port head = &hashinfo->bhash[inet_bhashfn(net, snum, hashinfo->bhash_size)]; spin_lock(&head->lock); inet_bind_bucket_for_each(tb, &head->chain) if (net_eq(ib_net(tb), net) && tb->port == snum) goto tb_found; tb = NULL; goto tb_not_found; tb_found: //tb->owners不为空 if (!hlist_empty(&tb->owners)) { //没有设置此标志 //如果socketB设置了TCP_REPAIR,则强制bind成功 if (sk->sk_reuse == SK_FORCE_REUSE) goto success; //如果socketA没有设置reuseaddr和reuseport,则fastreuse 和fastureseport都为0,走else分支。 //如果tb的fastreuse不为0,即socketA设置了reuseaddr,并且还没有调用listen,如果 //socketB也设置了reuseaddr,则socketB可以bind成功。 //或者tb的fastreuseport不为0,即socketA设置了reuseport,如果socketB也设置了reuseport,则socketB可以bind成功。 if (((tb->fastreuse > 0 && sk->sk_reuse && sk->sk_state != TCP_LISTEN) || (tb->fastreuseport > 0 && sk->sk_reuseport && uid_eq(tb->fastuid, uid))) && smallest_size == -1) { goto success; } else { ret = 1; //执行函数inet_csk_bind_conflict,在此例会返回true,说明找到了冲突的sk if (inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb, true)) { //不满足if条件,跳转到fail_unlock,返回值为1. //在inet_bind中,判断如果返回值为1,则设置err为EADDRINUSE if (((sk->sk_reuse && sk->sk_state != TCP_LISTEN) || (tb->fastreuseport > 0 && sk->sk_reuseport && uid_eq(tb->fastuid, uid))) && smallest_size != -1 && --attempts >= 0) { spin_unlock(&head->lock); goto again; } goto fail_unlock; } } } fail_unlock: spin_unlock(&head->lock); fail: local_bh_enable(); return ret; } int inet_csk_bind_conflict(const struct sock *sk, const struct inet_bind_bucket *tb, bool relax) { struct sock *sk2; int reuse = sk->sk_reuse; int reuseport = sk->sk_reuseport; kuid_t uid = sock_i_uid((struct sock *)sk); /* * 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, &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) && (!reuseport || !sk2->sk_reuseport || (sk2->sk_state != TCP_TIME_WAIT && !uid_eq(uid, sock_i_uid(sk2))))) { //只要sk和sk2两个套接字绑定的ip地址,任意一个为全零(相当于通配符)或者 //两个ip完全相等,就认为有地址冲突。 if (!sk2->sk_rcv_saddr || !sk->sk_rcv_saddr || sk2->sk_rcv_saddr == sk->sk_rcv_saddr) break; } if (!relax && reuse && sk2->sk_reuse && sk2->sk_state != TCP_LISTEN) { if (!sk2->sk_rcv_saddr || !sk->sk_rcv_saddr || sk2->sk_rcv_saddr == sk->sk_rcv_saddr) break; } } } return sk2 != NULL; } 

接收client连接
如果有两个完全重复的套接字在监听,如下,哪个套接字接收客户端的请求呢?

讯享网root@ubuntu:/home/jk/socket# netstat -nap | grep 2222 tcp 0 0 192.168.122.1:2222 0.0.0.0:* LISTEN 46735/server1 tcp 0 0 192.168.122.1:2222 0.0.0.0:* LISTEN 46728/server 

因为有多个相同的监听套接字,需要找个使用哪个套接字来处理。

客户请求首先在tcp_v4_rcv中调用__inet_lookup查找,先调用__inet_lookup_established使用四元组在已建立连接表中查找,如果是第一次请求显然找不到,接着调用__inet_lookup_listener在监听表中查找,具体代码如下:

struct sock *__inet_lookup_listener(struct net *net, struct inet_hashinfo *hashinfo, const __be32 saddr, __be16 sport, const __be32 daddr, const unsigned short hnum, const int dif) struct sock *sk, *result; struct hlist_nulls_node *node; unsigned int hash = inet_lhashfn(net, hnum); struct inet_listen_hashbucket *ilb = &hashinfo->listening_hash[hash]; int score, hiscore, matches = 0, reuseport = 0; u32 phash = 0; result = NULL; hiscore = 0; sk_nulls_for_each_rcu(sk, node, &ilb->head) { //根据目的ip和目的端口号进行匹配。如果匹配成功则返回值大于0 //返回值就是这次匹配得到的分数,最终分数高的胜出。匹配度越高分数越高。 score = compute_score(sk, net, hnum, daddr, dif); if (score > hiscore) { result = sk; hiscore = score; //如果设置了reuseport选项 reuseport = sk->sk_reuseport; if (reuseport) { phash = inet_ehashfn(net, daddr, hnum, saddr, sport); matches = 1; } } else if (score == hiscore && reuseport) { matches++; if (reciprocal_scale(phash, matches) == 0) result = sk; phash = next_pseudo_random32(phash); } } 

计算分数。
三个匹配条件:sk_family ,socket绑定的本地ip rcv_saddr,socket绑定的本地接口sk_bound_dev_if。
如果目的ip和socket绑定的ip地址不同或者报文入接口和socket绑定的接口不同,则得分为-1,
说明匹配失败
如果三个都匹配,则分数为10,为最高分数。

讯享网static inline int compute_score(struct sock *sk, struct net *net, const unsigned short hnum, const __be32 daddr, const int dif) { int score = -1; struct inet_sock *inet = inet_sk(sk); //网络空间net必须相同,目的端口号必须和绑定的端口号相同,否则直接返回 -1 if (net_eq(sock_net(sk), net) && inet->inet_num == hnum && !ipv6_only_sock(sk)) { __be32 rcv_saddr = inet->inet_rcv_saddr; //sk->sk_family为PF_INET 得分为2,否则为1 score = sk->sk_family == PF_INET ? 2 : 1; if (rcv_saddr) { //目的ip和绑定ip不同,直接返回 -1 if (rcv_saddr != daddr) return -1; //目的ip和绑定ip相同,得分加4 score += 4; } //调用 SO_BINDTODEVICE 绑定了接口 if (sk->sk_bound_dev_if) { //报文收接口和绑定接口不同,直接返回 -1 if (sk->sk_bound_dev_if != dif) return -1; //报文收接口和绑定接口相同,得分加4 score += 4; } } return score; } 

举例1:server上有三个监听套接字(前两个属于ip冲突的情况,需要设置reuseaddr才能bind成功),并且都没有绑定本地接口。
0.0.0.0:80 --对应sock1
192.168.1.2:80 --对应sock2
10.24.35.142:80 --对应sock3

如果此时client访问server: 192.168.1.2:80,则得分情况如下:
匹配到0.0.0.0时,rcv_saddr为0,所以只能 score=2
匹配到192.168.1.2时,rcv_saddr不为0,并且和请求目的ip相同,所以 socre=2+4=6
匹配到10.24.35.142时,rcv_saddr不为0,但是和请求目的ip不同,所以score=-1
所以最终返回的socket为最匹配的sock2

举例2:server上有三个监听套接字(需要设置reuseport才能bind成功),并且都没有绑定本地接口。
server端有三个监听socket
192.168.1.2:80
192.168.1.2:80
192.168.1.2:80

当有client连接到来时,用四元组计算hash,将结果对reuseport套接字数量取模,得到一个索引,该索引指示的数组位置对应的套接字便是工作套接字。

同一条tcp流的前两个建立连接的请求syn和响应ack报文需要走上面流程,连接建立后,后续报文到来后,可直接在已建立连接表查找到。

参考
https://segmentfault.com/a/24323
https://blog.csdn.net/dog250/article/details/

也可参考:https://www.jianshu.com/p/9cc2b5b9ad4d 

小讯
上一篇 2025-03-20 17:48
下一篇 2025-04-04 11:05

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/116273.html