当然,只要你用 ,SO_REUSEPORT 这个参数。
先来看看man文档中是怎么说的:
SO_REUSEPORT(sinceLinux3.9)PermitsmultipleAF_INETorAF_INET6socketstobeboundtoanidenticalsocketaddress.Thisoptionmustbesetoneachsocket(includingthefirstsocket)priortocallingbind(2)onthesocket.Topreventporthijacking,allofthepro‐cessesbindingtothesameaddressmusthavethesameeffec‐tiveUID.ThisoptioncanbeemployedwithbothTCPandUDPsockets.ForTCPsockets,thisoptionallowsaccept(2)loaddistribu‐tioninamulti-threadedservertobeimprovedbyusingadis‐tinctlistenersocketforeachthread.Thisprovidesimprovedloaddistributionascomparedtotraditionaltechniquessuchusingasingleaccept(2)ingthreadthatdistributesconnec‐tions,orhavingmultiplethreadsthatcompetetoaccept(2)fromthesamesocket.ForUDPsockets,theuseofthisoptioncanprovidebetterdistributionofincomingdatagramstomultipleprocesses(orthreads)ascomparedtothetraditionaltechniqueofhavingmultipleprocessescompetetoreceivedatagramsonthesamesocket.从文档中可以看出,该参数允许多个socket即使绑定到同一地址,socket是处于listen状态的。
当多个listen状态的socket绑定到同一地址时,各个socket的accept操作可以接受新的tcp连接。
写段代码测试很神奇,对吧?
#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>staticinttcp_listen(char*ip,intport){intlfd,opt,err;structsockaddr_inaddr;lfd=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);assert(lfd!=-1);opt=1;err=setsockopt(lfd,SOL_SOCKET,SO_REUSEPORT,&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,(structsockaddr*)&addr,sizeof(addr));assert(!err);err=listen(lfd,8);assert(!err);returnlfd;}intmain(intargc,char*argv[]){intlfd,sfd;lfd=tcp_listen("127.0.0.1",8888);while(1){sfd=accept(lfd,NULL,NULL);close(sfd);printf("接收到tcp连接:%d\n",sfd);}return0;}编译和执行程序:
$gccserver.c&&./a.out看看目前8888端口的所有内容socket的状态:
$ss-antp|grep8888LISTEN08127.0.0.1:88880.0.0.0:*users:(("a.out",pid=32505,fd=3))和我们预期的一样,只有一个socket处于listen状态。
我们再次执行这个程序:
$gccserver.c&&./a.out再次检查888端口socket的状态:
$ss-antp|grep8888LISTEN08127.0.0.1:88880.0.0.0:*users:(("a.out",pid=32607,fd=3))LISTEN08127.0.0.1:88880.0.0.0:*users:(("a.out",pid=32505,fd=3))这时已经有两个了socket监控8888端口(注意他们ip地址也一样),这两个socket属于两个过程。
我们现在再用ncat连接8888端口的模拟客户端:
$ncatlocalhost8888重复操作并建立n个到8888端口的tcp此时,两个服务终端的输出如下。
服务端1:
$gccserver.c&&./a.out接收到tcp连接:4接收到tcp连接:4接收到tcp连接:4服务端2:
$gccserver.c&&./a.out接收到tcp连接:4接收到tcp连接:4可以看到,tcp连接基本上均匀分布在两个服务器上,神奇。
下面我们来看看相应的。linux内核代码,看看它是如何实现的。
//net/ipv4/inet_connection_sock.cintinet_csk_get_port(structsock*sk,unsignedshortsnum){...structinet_hashinfo*hinfo=sk->sk_prot->h.hashinfo;intret=1,port=snum;structinet_bind_hashbucket*head;...structinet_bind_bucket*tb=NULL;...head=&hinfo->bhash[inet_bhashfn(net,port,hinfo->bhash_size)];...inet_bind_bucket_for_each(tb,&head->chain)if(net_eq(ib_net(tb),net)&&tb->l3mdev==l3mdev&&tb->port==port)gototb_found;tb_not_found:tb=inet_bind_bucket_create(hinfo->bind_bucket_cachep,net,head,port,l3mdev);...tb_found:if(!hlist_empty(&tb->owners)){...if(...||sk_reuseport_match(tb,sk))gotosuccess;...}success:if(hlist_empty(&tb->owners)){...if(sk->sk_reuseport){tb->fastreuseport=FASTREUSEPORT_ANY;...}else{tb->fastreuseport=0;}}else{...}...}EXPORT_SYMBOL_GPL(inet_csk_get_port);当我们做bind此方法将在操作时调用,参数snum就是我们要bind的端口。
该方法,类型struct inet_bind_bucket代表端口bind具体信息,比如:哪个?socket在bind这个端口。
hinfo->bhash是用于存放struct inet_bind_bucket实例的hashmap。
该方法先从hinfo->bhash这个hashmap中搜,这个端口已经被发现了吗?bind如果没有,创造一个新的tb,比如我们第一次listen该端口在操作过程中没有使用,因此将创建新的端口tb。
新创建的tb,它的tb->owners是empty,此时,如果我们设置它,SO_REUSEPORT参数,那sk->sk_reuseport字段值大于0,也就是说,第一次listen操作之后,tb->fastreuseport设值设置为FASTREUSEPORT_ANY(大于0)。
第二次做的时候listen操作时,此时将进入此方法hinfo->bhash的map有相同的端口tb,所以会goto到tb_found部分。
因为之前的listen操作对应socket放入到tb->owners所以第二次listen操作,tb->owners不为empty。
然后,逻辑处理将进入sk_reuseport_match如果该方法返回,则该方法true,内核允许第二次listen使用本地址进行操作。
我们看下sk_reuseport_match方法:
//net/ipv4/inet_connection_sock.cstaticinlineintsk_reuseport_match(structinet_bind_bucket*tb,structsock*sk){...if(tb->fastreuseport<=0)return0;if(!sk->sk_reuseport)return0;...if(tb->fastreuseport==FASTREUSEPORT_ANY)return1;...}由于上一次listen操作,tb->fastreuseport被设置为FASTREUSEPORT_ANY,而此次listen操作的socket,又设置了SO_REUSEPORT参数,即sk->sk_reuseport值大于0,因此该方法最终返回true。
由上可见,设置SO_REUSEPORT参数结束后,第二次listen中的bind操作没用。我们来看看相应的。listen操作:
//net/core/sock_reuseport.cintreuseport_add_sock(structsock*sk,structsock*sk2,boolbind_inany){structsock_reuseport*old_reuse,*reuse;...reuse=rcu_dereference_protected(sk2->sk_reuseport_cb,lockdep_is_held(&reuseport_lock));...reuse->socks[reuse->num_socks]=sk;...reuse->num_socks ;rcu_assign_pointer(sk->sk_reuseport_cb,reuse);...}EXPORT_SYMBOL(reuseport_add_sock);listen方法最终会调用上面的方法,在该方法中,sk代表第二次listen操作的socket,sk2代表第一次listen操作的socket。
该方法的一般逻辑如下:
1. 将sk2->sk_reuseport_cb给予字段值赋值reuse。
2. 将sk放入到reuse->socks在代表字段的数组中。
3. 将sk的sk_reuseport_cb字段也指向这个数组。
也就是说,这种方法将是所有的第二次及以后listen操作的socket放入到reuse->socks代表字段的数组(第一次listen操作的socket在创建struct sock_reuseport同时,所有的例子都被放入了数组中),同时,listen的socket的sk->sk_reuseport_cb所有的字段reuse,这样,我们就可以通过listen的socket的sk_reuseport_cb字段,拿到struct sock_reuseport例子,然后你可以得到所有其他的东西listen同一端口的socket。
到目前为止,reuseport如何实现基本清晰,当有新的时候tcp当连接到来时,只要我们找到一个来监控端口listen的socket,就等于拿到了所有设置了SO_REUSEPORT参数,监控同一端口的其他参数socket,我们只需要随机选择一个socket,然后让它完成tcp这样我们就可以实现连接建立过程tcp连接均匀负载到这些listen socket上了。
看看相应的代码:
//net/core/sock_reuseport.cstructsock*reuseport_select_sock(structsock*sk,u32hash,structsk_buff*skb,inthdr_len){structsock_reuseport*reuse;...structsock*sk2=NULL;u16socks;...reuse=rcu_dereference(sk->sk_reuseport_cb);...socks=READ_ONCE(reuse->num_socks);if(likely(socks)){...if(!sk2)sk2=reuse->socks[reciprocal_scale(hash,socks)];}...returnsk2;}EXPORT_SYMBOL(reuseport_select_sock);看,这个方法终于用了reciprocal_scale方法,计算被选中的listen socket索引,最后回到这个listen socket继续处理tcp连接请求。
看下reciprocal_scale方法是如何实现的:
//include/linux/kernel.h/***reciprocal_scale-"scale"avalueintorange[0,ep_ro)*...*/staticinlineu32reciprocal_scale(u32val,u32ep_ro){return(u32)(((u64)val*ep_ro)>>32);}虽然我们看不懂算法,但通过它的注释,我们可以知道它返回值的范围是[0, ep_ro),结合以上reuseport_select_sock我们可以确定方法,所有的返回都是listen socket数组下标索引。
至此,有关SO_REUSEPORT我们完成了参数的内容。
上篇文章 socket的SO_REUSEADDR 中参数综合分析,我们分析SO_REUSEADDR参数,这个参数和SO_REUSEADDR有什么区别?
SO_REUSEPORT参数是SO_REUSEADDR这两个参数的目的是重复使用本地地址,但是SO_REUSEADDR不允许处于listen重复使用状态地址,SO_REUSEPORT允许,同时,SO_REUSEPORT参数还会把新来的tcp连接负载载均衡listen socket上,为我们tcp服务器编程提供了一种新的模式。
其实这个参数是我上次写的。socks5代理那个项目是有用的(是的,我又用了rust实现了一版socks5代理),通过使用该参数,我可以开多个进程同时处理socks5代理请求,现在使用的感觉是,真的很快,使用Google一点问题都没有。
好吧,就到这里吧。
本文转载自微信公众号「卯时卯刻」,可通过以下二维码关注。转载本文请联系毛时毛时刻微信官方账号。