当前位置: 首页 > 科技观察

多个进程可以监听同一个端口吗?

时间:2023-03-16 16:34:08 科技观察

当然可以,只要使用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是Inthelistenstate.当多个处于listen状态的socket绑定到同一个地址时,每个socket的accept操作都可以接受新的tcp连接。厉害吧,写一段代码测试下:#include#include#include#include#include#include#include#includestaticinttcp_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));地址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("Receivedtcpconnection:%d\n",sfd);}return0;}编译执行程序:$gccserver.c&&./a.查看端口8888上所有套接字的当前状态:$ss-antp|grep8888LISTEN08127.0.0.1:88880.0.0.0:*users:(("a.out",pid=32505,fd=3))正如我们所料,只有一个socket处于监听状态我们再执行程序:$gccserver.c&&./a.out再次查看8888端口socket的状态:$ss-antp|grep8888LISTEN08127.0.0.1:88880.0.0.0:*用户:(("a.out",pid=32607,fd=3))LISTEN08127.0.0.1:88880.0.0.0:*用户:(("a.out",pid=32505,fd=3))此时有两个socket监听8888端口(注意他们的ip地址也是一样的),这两个socket分别属于两个进程,我们现在将使用ncat模拟客户端,连接到8888端口:$ncatlocalhost8888重复这个操作,建立n个到8888端口的tcp连接,此时两个服务器终端的输出如下,服务器1:$gccserver.c&&。/a.outreceivesTotcpconnection:4Receivedtcpconnection:4Receivedtcpconnection:4Server2:$gccserver.c&&./a.outReceivedtcpconnection:4Receivedtcpconnection:4可以看到tcp连接是基本上是均匀分布的d到两个服务器,这是惊人的。下面我们看看相应的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)gotob_found;tb_not_found:tb=inet_bind_bucket_create(hinfo->bind_bucket_cachep,net,head,port,l3mdev);...tb_found:if(!hlist_empty(&tb->所有者)){...if(...||sk_reuseport_match(tb,sk))gotosuccess;...}成功: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就是我们要绑定的端口。在该方法中,structinet_bind_bucket类型表示端口绑定的具体信息,例如:哪个套接字绑定了这个端口。hinfo->bhash是一个用于存储structinet_bind_bucket实例的hashmap。该方法首先从hinfo->bhash的hashmap中查找端口是否已经绑定,如果没有,则新建一个tb,比如我们第一次listen操作时,端口没有被使用,所以会新建一个结核病。新创建的tb,其tb->owners为空,此时如果我们设置SO_REUSEPORT参数,那么sk->sk_reuseport字段的值会大于0,即第一次listen操作后,tb->fastreuseport的值设置为FASTREUSEPORT_ANY(大于0)。当我们第二次做listen操作的时候,又会进入这个方法。此时在hinfo->bhashmap中有相同端口的tb,所以我们会转到tb_found部分。因为前面的listen操作会将其对应的socket放到tb->owners中,所以第二次listen操作时,tb->owners不为空。再者,逻辑处理会进入sk_reuseport_match方法。如果此方法返回true,内核将允许第二个侦听操作使用本地地址。再看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操作就没有用了。我们看对应的监听操作://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);监听方法将最后调用上面的方法,在这个方法中,sk代表第二次监听的socket,sk2代表第一次监听的socket。该方法的大致逻辑是:1.将sk2->sk_reuseport_cb字段值赋值给reuse。2.将sk放入reuse->socks字段代表的数组中。3.将sk的sk_reuseport_cb字段指向这个数组。也就是说,该方法会将第二次及后续listen操作的所有socket放入reuse->socks字段所代表的数组中(第一次listen操作的socket在创建structsock_reuseport实例时已经放入数组中)),同时将所有listensocket的sk->sk_reuseport_cb字段指向reuse,这样我们就可以通过listensocket的sk_reuseport_cb字段得到structsock_reuseport实例,进而可以得到所有其他listensocket上同一个端口。到这里,reuseport是如何实现的,基本清楚了。当有新的tcp连接时,我们只要找到一个监听该端口的listensocket,就相当于得到了所有设置了SO_REUSEPORT参数且监听同一个端口的socket。对于其他socket,我们只需要随机挑选一个socket,然后让它完成后续的tcp连接建立过程,这样就可以在这些listensocket上实现tcp连接的负载均匀。看对应代码://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方法计算出选中的listensocket的index,最后返回到这个listensocket继续处理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方法我们可以确定返回的是所有listensocket的数组下标索引。至此,我们就把SO_REUSEPORT参数的内容说完了。在上一篇SO_REUSEADDR参数综合分析中,我们分析了SO_REUSEADDR参数。这个参数和SO_REUSEADDR有什么区别?SO_REUSEPORT参数是SO_REUSEADDR参数的超集。两个参数的目的都是为了复用本地地址,但是SO_REUSEADDR不允许listen状态的地址复用,而SO_REUSEPORT允许。同时,SO_REUSEPORT参数也会均衡新的tcp连接到各个listensocket的负载,为我们的tcpserver编程提供了一种新的模式。其实这个参数在我上次写的socks5代理项目中就用到了(没错,我又用rust实现了一个版本的socks5代理)。通过这个参数,我可以开启多个进程同时处理socks5代理请求。现在用起来的感觉是真的很快,用google什么的都不是问题。好的,让我们到此为止。本文转载自微信公众号“猫食猫客”,可通过以下二维码关注。转载本文请联系猫猫猫客公众号。