SO_REUSEPORT选项是在Linux3.9内核中引入的,之前也有一个非常相似的选项SO_REUSEADDR。如果你不是很清楚两者的区别和联系,建议阅读HowdoSO_REUSEADDRandSO_REUSEPORTdiffer?。如果你不想看,下面的部分是给懒人看的。什么是SO_REUSEADDR和SO_REUSEPORT?TCP/UDP使用五元组来唯一标识一个连接。在任何时候,两个连接的五元组都不能完全相同,否则,当收到一条消息时,协议栈没有办法确定它属于哪个连接。在五元组{,,,,}五元组中,协议是在创建socket时确定的,和是在bind()期间确定的,和是在connect()期间确定的。当然,bind()和connect()在某些情况下不需要显式使用,但这超出了本文的范围。那么,如果在套接字上设置了SO_REUSEADDR和SO_REUSEPORT选项,它们什么时候起作用?答案是bind(),就是在确定和的时候。不同的操作系统内核对SO_REUSEADDR和SO_REUSEPORT的处理方式略有不同,但它们都源自BSD。因此,下面的描述将基于BSD的实现。SO_REUSEADDR假设我需要bind()将socketA绑定到A:X,socketB绑定到B:Y(不管X=0还是Y=0,因为0代表让内核自动分配端口,不会有冲突)。如果X!=Y,则无论A和B之间的关系如何,两个bind()都会成功。但是如果X==Y,那么结果会是这样:SO_REUSEADDRsocketAsocketBResult------------------------------------------------------------------开/关192.168.0.1:21192.168.0.1:21错误(EADDRINUSE)打开/关闭192.168.0.1:2110.0.0.1:21正常打开/关闭10.0.0.1:21192.168.0.1:21正常关闭0.0.0.0:21192.168.1.0:21错误(EADDRINUSE)关闭192.168。1.0:210.0.0.0:21错误(EADDRINUSE)开启0.0.0.0:21192.168.1.0:21正常开启192.168.1.0:210.0.0.0:21正常开启/关闭0.0.0.0:210.0.0.0:21错误(EADDRINUSE)第一列表示是否设置SO_REUSEADDR注解,最后一列表示post-boundsocket是否可以绑定成功。注意:这里设置的对象指的是后绑定的socket(即不关心前面有没有设置)。可以看出,在BSD的实现中,SO_REUSEADDR可以让一个使用通配符地址(0.0.0.0),一个使用指定地址。(192.168.1.0)socket同时绑定成功。SO_REUSEADDR还有一个应用场景:TCP中有一个TIME_WAIT状态,指的是主动关闭端的最后阶段。假设socketA绑定到A:X,完成TCP通信后,主动使用close()进入TIME_WAIT。这时候如果socketB也绑定了A:X,也会报EADDRINUSE错误,但是如果socketB设置了SO_REUSEADDR,那么就可以绑定成功。如果SO_REUSEPORT理解了SO_REUSEADDR,那么SO_REUSEPORT就很容易理解了。它允许两个套接字绑定相同的。SO_REUSEPORTsocketAsocketB结果------------------------------------------------------------------ON192.168.0.1:21192.168.0.1:21OK提醒一下,以上结果是BSD的结果,linux内核有有一些差异。具体来说,3.9版本支持SO_REUSEPORT。一旦将TCPSocketasServer绑定到特定端口并启用LISTEN,即使之前设置了SO_REUSEADDR,也不会生效。这点Linux比BSD严格SO_REUSEADDRsocketAsocketBResult-------------------------------------------------------------------ON/OFF192.168.0.1:210.0.0.0:21版本之前的错误(EADDRINUSE)3.9、作为Client的Socket,SO_REUSEADDR选项在BSD中具有SO_REUSEPORT的作用。Linux在这一点上比BSD宽松。SO_REUSEADDRsocketAsocketB结果------------------------------------------------------------------ON192.168.0.2:55555192.168.0.2:55555OKLinuxLinux<3.9中reuseport的演进让我们看看它是如何完成的:内核套接字使用skc_reuse字段来指示是否设置了SO_REUSEADDRstructsock_common{/*省略*/unsignedcharskc_reuse;/*省略*/}intsock_setsockopt(structsocket*sock,intlevel,intoptname,...{......caseSO_REUSEADDR:sk->sk_reuse=(valbool?SK_CAN_REUSE:SK_NO_REUSE);break;}inet_bind_bucketstructinet_bind_bucket{/*省略*/unsignedshortport;signedshortfastreuse;intnum_owners;structhlist_nodenode;structhlist_headowners;};上面结构中的fastreuse表示端口是否支持共享,所有socket共享端口挂在owner成员上,当用户使用bind()时,内核使用TCP:inet_csk_get_port(),UDP:udp_v4_get_port()绑定端口。/*inet_connection_Sock.c:inet_csk_get_port()*/tb_found:if(!hlist_empty(&tb->owners)){......if(tb->fastreuse>0&&sk->sk_reuse&&sk->sk_state!=TCP_LISTEN&&smallest_size==-1){转到成功;因此,当端口支持共享,并且socket也设置为SO_REUSEADDR且不处于LISTEN状态时,此时bind()可以成功。3.9=。这时,当Server收到Client发来的SYN报文后,会选择其中一个socket进行响应。【图】在实现上,3.9版本扩展了sock_common,拆分了原来的记录skc_reuse。structsock_common{unsignedshortskc_family;volatileunsignedcharskc_state;-unsignedcharskc_reuse;+unsignedcharskc_reuse:4;+unsignedcharskc_reuseport:4;@@intsock_setsockopt(structsocket*sock,intlevel,intoptname,caseSO_REUSEDDR=sk_resk:(valbool?SK_CAN_REUSE:SK_NO_REUSE);break;+caseSO_REUSEPORT:+sk->sk_reuseport=valbool;+break;然后对inet_bind_bucket进行相应的扩展structinet_bind_bucket{/*省略*/unsignedshortport;-signedshortfastreuse;+signedcharfastreuse;+signedcharfastreuseport;+kuid_tfastuid;并且在绑定端口的时候,加上reuseport的通过条件/*inet_connection_sock.c:inet_csk_get_port()*/tb_found:if(sk->sk_reuse==SK_FORCE_REUSE)gotosuccess;-如果(tb->fastreuse>0&&-sk->sk_reuse&&sk->sk_state!=TCP_LISTEN&&+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){gotosuccess;而当Client的SYN报文到达时,Server会先,根据本地端口(SYN报文的)计算一条hash冲突链,然后遍历链表上的所有Socket,根据四元组的匹配度打分;如果开启了reuseport,则可能有多个sockets会得到最高分,内核会随机选择一个进行后续处理/*inet_hashtables.c*/structsock*__inet_lookup_listener(struct...){structsock*sk,*result;unsignedinthash=inet_lhashfn(net,hnum);structinet_listen_hashbucket*ilb=&hashinfo->listening_hash[hash];//根据本地端口查找哈希冲突链/*代码省略*/result=NULL;他的分数=0;sk_nulls_for_each_rcu(sk,node,&ilb->head){score=compute_score(sk,net,hnum,daddr,dif);//根据匹配度打分if(score>hiscore){result=sk;hiscore=得分;reuseport=sk->sk_reuseport;if(reuseport){phash=inet_ehashfn(net,daddr,hnum,saddr,sport);匹配=1;//如果是reuseport,则累计满足多少个socket}}elseif(score==hiscore&&reuseport){matches++;if(reciprocal_scale(phash,matches)==0)result=sk;phash=next_pseudo_random32(phash);}}/**如果我们在查找结束时得到的nulls值不是预期的值,我们必须重新开始查找。*我们可能遇到了一个被转移到另一个连锁店的项目。*/returnresult;}例如假设内核有4条listeningsockets的hash冲突链,然后用户创建了4个Server:A、B、C、D。监听的地址和端口如图下面,A和B以端口为key启用了SO_REUSEPORT冲突链,所以A、B、D会挂在同一条冲突链上。如果此时从对端收到一个SYN报文<192.168.10.1,21>,内核会遍历listening_hash[0]对以上7个socket进行打分,而由于B监听到的是准确地址,所以B的打分会高于A,内核最终选择一个SocketB进行后续处理。4.5listening_hash[hash];intscore,hiscore,matches=0,reuseport=0;+boolselect_ok=true;u32phash=0;rcu_read_lock();@@-230,6+233,15@@begin:if(reuseport){phash=inet_ehashfn(net,daddr,hnum,saddr,sport);+if(select_ok){+structsock*sk2;+sk2=reuseport_select_sock(sk,phash,+skb,doff);+if(sk2){+result=sk2;+gotofound;+}+}匹配=1;}}