一、场景引入如果有人问能不能够在一个端口上监听多个程序估计很多人会回答不可能。有过网络开发经验的小伙伴可能也遇到这样的问题调试程序时如果快速的重启程序结果报地址无法绑定的错误。那么这是否意味着一个端口只能绑定一个程序进程呢为什么有的时候网上一些工具可以进行多个进程同时监听一个端口呢这些问题会让不少网络开发者感到有点莫名其妙。其实这不是多么复杂的问题。而是开发者没有理解网络开发的过程以及相关技术的迭代升级。二、问题分析还是要把问题拉回到网络开发的基础知识上来。所谓的网络通信其实最主要的是在复杂的网络中找到各自通信的具体的目标。而这个目标的表述就是一个核心的问题。可以理解成为身份的ID但这个ID由于涉及的范围较多所以会出现这个描述的具体情况分为三元组四元组五元组。这个几元组其实就是由几个方面来组成网络通信会话Session描述的内容。以五元组为例它包括源IP地址源端口目的IP地址目的端口传输层协议。而四元组则则去除了传输协议层内容三元组则根据情况可以忽略源相关内容或忽略所有端口项。可以这样理解只要在指定场景中能够互相明确的发现目标就可以省略相关的细节描述组成。举一个简单的例子一个学校有三个年级每个年级都有叫张三的同学而每个年级又有三个班每个班又都有一个叫张三的同学。如果学校的广播室广播就需要把年级、班号和张三的姓名组合起来广播才能找到具体的张三同学。但是如果班主任到班里点名就直接叫张三也可以清楚的表明叫的是哪个张三。最典型的就是四元组创建TCP或UDP网络通信时接口已经默认了传输协议所以就不用再费心的在程序中专门的指出协议的具体类型了。再回到前面的问题对于大多数的网络开发都会有一个bind的过程它的目的就在于将进程绑定到一个目标上IP端口。系统会将相关的所有的绑定都保存到一个表中用来保证绑定连接的安全性。即如果一个进程已经绑定到了一个目标IPPORT后再有进程想绑定到这个目标时就会提示错误。这就是本文最初提到的一个进程只能绑定到一个端口上。即默认情况下确实是一个程序只能监听一个端口。但是凡事都有但是是不是可以在一个端口上进行多个进程的监听呢答案是肯定的。三、解决要想实现多个进程绑定到一个端口只需要打开端口重用的设置即可。这在Linux内核的3.9就已经支持了。需要说明的是端口重用分为两种情况地址重用在前面提到的服务端快速断开并重新启动的情况下由于TCP的四次挥手的原因导致TIME_WAIT主动关闭的出现它一般是2MSL在Linux中MSL一般默认是60秒即2分钟。这在某些场景下就导致了无法再次使用同一个地址端口的问题。解决它的方法就是使用SO_REUSEADDR。它的配置方法有很多种可以在配置文件中操作也可以直接操作setsockopt函数进行参数设置注意不同平台的细节差异但到底层都是一致的。端口重用另外一种情况就是真的需要多个进程监听在同一个端口上在这种情况下网络通信就必须能够真正的判定到底是哪个进程在和连接目标通信。这就用到了四元组。虽然本地的IP和端口几个进程是一样的但连接目标的IP和地址一般是不同的。这时使用一种方法如哈希就可以得到一个具体的标识ID指定给监听的特定的进程形成一个映射关系。这样连接成功后这个套接字五元组通过网络得到的所有数据系统会根据映射关系查表准确投递到特定进程中。好那这里有一个问题如果服务端和客户端都使用了端口重用还能不能够安全可靠的进行网络通信呢不能。因为标识双方的五元组是相同的已经无法再正确的进行识别了。结果就是一定有一方无法正常工作。需要提醒的是端口重用解决的是数据的投递而非业务的处理。一般情况下同一端口监听多个进程处理的应该是相同类型数据。而业务数据则应该从抽象层如架构设计、协议处理或域名判断等等来区别。四、内核源码处理内核中早期处理端口重用使用的是轮询方式。可能觉得这种情况应用场景相对较少另外也可能当时觉得轮询就能满足情况。但随着应用的越来越广泛数据量也愈发增加。在Linux4.5后提供了哈希的方法后来又增加了BPF相关的内核扩展支持有兴趣的可以参看前面的eBPF相关的文章。哈希的优势是非常明显的除了可以保证高效率的负载均衡更重要的是它可以提高缓存的命中率并避免轮询可能引起的惊群。下面看一下内核中的处理源码// include/net/sock_reuseport.h//net/core/sock_reuseport.cstructsock_reuseport{structrcu_headrcu;u16 max_socks;/* length of socks */u16 num_socks;/* elements in socks */u16 num_closed_socks;/* closed elements in socks *//* The last synq overflow event timestamp of this * reuse-socks[] group. */unsignedintsynq_overflow_ts;/* ID stays the same even after the size of socks[] grows. */unsignedintreuseport_id;unsignedintbind_inany:1;unsignedinthas_conns:1;structbpf_prog__rcu*prog;/* optional BPF sock selector */structsock*socks[];/* array of sock pointers */};staticstructsock*reuseport_select_sock_by_hash(structsock_reuseport*reuse,u32 hash,u16 num_socks){inti,j;ijreciprocal_scale(hash,num_socks);while(reuse-socks[i]-sk_stateTCP_ESTABLISHED){i;if(inum_socks)i0;if(ij)returnNULL;}returnreuse-socks[i];}structsock*reuseport_select_sock(structsock*sk,u32 hash,structsk_buff*skb,inthdr_len){structsock_reuseport*reuse;structbpf_prog*prog;structsock*sk2NULL;u16 socks;rcu_read_lock();reusercu_dereference(sk-sk_reuseport_cb);/* if memory allocation failed or add call is not yet complete */if(!reuse)gotoout;progrcu_dereference(reuse-prog);socksREAD_ONCE(reuse-num_socks);if(likely(socks)){/* paired with smp_wmb() in __reuseport_add_sock() */smp_rmb();if(!prog||!skb)gotoselect_by_hash;if(prog-typeBPF_PROG_TYPE_SK_REUSEPORT)sk2bpf_run_sk_reuseport(reuse,sk,prog,skb,NULL,hash);elsesk2run_bpf_filter(reuse,socks,prog,skb,hdr_len);select_by_hash:/* no bpf or invalid bpf result: fall back to hash usage */if(!sk2)sk2reuseport_select_sock_by_hash(reuse,hash,socks);}out:rcu_read_unlock();returnsk2;}EXPORT_SYMBOL(reuseport_select_sock);//net/ipv4/inet_hashtables.c 协议查找跳转staticinlinestructsock*lookup_reuseport(structnet*net,structsock*sk,structsk_buff*skb,intdoff,__be32 saddr,__be16 sport,__be32 daddr,unsignedshorthnum){structsock*reuse_skNULL;u32 phash;if(sk-sk_reuseport){phashinet_ehashfn(net,daddr,hnum,saddr,sport);reuse_skreuseport_select_sock(sk,phash,skb,doff);}returnreuse_sk;}staticinlinestructsock*inet_lookup_run_bpf(structnet*net,structinet_hashinfo*hashinfo,structsk_buff*skb,intdoff,__be32 saddr,__be16 sport,__be32 daddr,u16 hnum,constintdif){structsock*sk,*reuse_sk;bool no_reuseport;if(hashinfo!net-ipv4.tcp_death_row.hashinfo)returnNULL;/* only TCP is supported */no_reuseportbpf_sk_lookup_run_v4(net,IPPROTO_TCP,saddr,sport,daddr,hnum,dif,sk);if(no_reuseport||IS_ERR_OR_NULL(sk))returnsk;reuse_sklookup_reuseport(net,sk,skb,doff,saddr,sport,daddr,hnum);if(reuse_sk)skreuse_sk;returnsk;}structsock*__inet_lookup_listener(structnet*net,structinet_hashinfo*hashinfo,structsk_buff*skb,intdoff,const__be32 saddr,__be16 sport,const__be32 daddr,constunsignedshorthnum,constintdif,constintsdif){structinet_listen_hashbucket*ilb2;structsock*resultNULL;unsignedinthash2;/* Lookup redirect from BPF */if(static_branch_unlikely(bpf_sk_lookup_enabled)){resultinet_lookup_run_bpf(net,hashinfo,skb,doff,saddr,sport,daddr,hnum,dif);if(result)gotodone;}hash2ipv4_portaddr_hash(net,daddr,hnum);ilb2inet_lhash2_bucket(hashinfo,hash2);resultinet_lhash2_lookup(net,ilb2,skb,doff,saddr,sport,daddr,hnum,dif,sdif);if(result)gotodone;/* Lookup lhash2 with INADDR_ANY */hash2ipv4_portaddr_hash(net,htonl(INADDR_ANY),hnum);ilb2inet_lhash2_bucket(hashinfo,hash2);resultinet_lhash2_lookup(net,ilb2,skb,doff,saddr,sport,htonl(INADDR_ANY),hnum,dif,sdif);done:if(IS_ERR(result))returnNULL;returnresult;}EXPORT_SYMBOL_GPL(__inet_lookup_listener);在inet6_hashtables.c中会对相关的端口重用哈希机制进行处理包括创建、监听、查找和销毁等等。而且可以看到对BPF的支持。但真正的端口重用的具体细节处理是在sock_reuseport.c中可以看到数据结构体中的相关的定义包括对BFP的支持。需要说明的是在这个版本中BPF程序是优先级处理最高的如果没有则会使用内核哈希表的算法从而映射到具体的数组中的Socket。五、总结抓大放小要控制好关键点。在应用到具体的技术细节时要明白如何从源头下手将原理和实现对照分析从而能够更好的理解和掌握技术的整体的演进路线。本文分析的这个端口重用就是一个非常好的例子可以将一些开发者经常忽略的如五元组等基础知识与实践应用紧密的结合起来加深印象的同时明白网络通信机制的原理。在当前云时代容器大为流行这种端口的复用更是一种常态所以明白其根本就更显得重要了。
c++应用网络编程之十六—端口重用
一、场景引入如果有人问能不能够在一个端口上监听多个程序估计很多人会回答不可能。有过网络开发经验的小伙伴可能也遇到这样的问题调试程序时如果快速的重启程序结果报地址无法绑定的错误。那么这是否意味着一个端口只能绑定一个程序进程呢为什么有的时候网上一些工具可以进行多个进程同时监听一个端口呢这些问题会让不少网络开发者感到有点莫名其妙。其实这不是多么复杂的问题。而是开发者没有理解网络开发的过程以及相关技术的迭代升级。二、问题分析还是要把问题拉回到网络开发的基础知识上来。所谓的网络通信其实最主要的是在复杂的网络中找到各自通信的具体的目标。而这个目标的表述就是一个核心的问题。可以理解成为身份的ID但这个ID由于涉及的范围较多所以会出现这个描述的具体情况分为三元组四元组五元组。这个几元组其实就是由几个方面来组成网络通信会话Session描述的内容。以五元组为例它包括源IP地址源端口目的IP地址目的端口传输层协议。而四元组则则去除了传输协议层内容三元组则根据情况可以忽略源相关内容或忽略所有端口项。可以这样理解只要在指定场景中能够互相明确的发现目标就可以省略相关的细节描述组成。举一个简单的例子一个学校有三个年级每个年级都有叫张三的同学而每个年级又有三个班每个班又都有一个叫张三的同学。如果学校的广播室广播就需要把年级、班号和张三的姓名组合起来广播才能找到具体的张三同学。但是如果班主任到班里点名就直接叫张三也可以清楚的表明叫的是哪个张三。最典型的就是四元组创建TCP或UDP网络通信时接口已经默认了传输协议所以就不用再费心的在程序中专门的指出协议的具体类型了。再回到前面的问题对于大多数的网络开发都会有一个bind的过程它的目的就在于将进程绑定到一个目标上IP端口。系统会将相关的所有的绑定都保存到一个表中用来保证绑定连接的安全性。即如果一个进程已经绑定到了一个目标IPPORT后再有进程想绑定到这个目标时就会提示错误。这就是本文最初提到的一个进程只能绑定到一个端口上。即默认情况下确实是一个程序只能监听一个端口。但是凡事都有但是是不是可以在一个端口上进行多个进程的监听呢答案是肯定的。三、解决要想实现多个进程绑定到一个端口只需要打开端口重用的设置即可。这在Linux内核的3.9就已经支持了。需要说明的是端口重用分为两种情况地址重用在前面提到的服务端快速断开并重新启动的情况下由于TCP的四次挥手的原因导致TIME_WAIT主动关闭的出现它一般是2MSL在Linux中MSL一般默认是60秒即2分钟。这在某些场景下就导致了无法再次使用同一个地址端口的问题。解决它的方法就是使用SO_REUSEADDR。它的配置方法有很多种可以在配置文件中操作也可以直接操作setsockopt函数进行参数设置注意不同平台的细节差异但到底层都是一致的。端口重用另外一种情况就是真的需要多个进程监听在同一个端口上在这种情况下网络通信就必须能够真正的判定到底是哪个进程在和连接目标通信。这就用到了四元组。虽然本地的IP和端口几个进程是一样的但连接目标的IP和地址一般是不同的。这时使用一种方法如哈希就可以得到一个具体的标识ID指定给监听的特定的进程形成一个映射关系。这样连接成功后这个套接字五元组通过网络得到的所有数据系统会根据映射关系查表准确投递到特定进程中。好那这里有一个问题如果服务端和客户端都使用了端口重用还能不能够安全可靠的进行网络通信呢不能。因为标识双方的五元组是相同的已经无法再正确的进行识别了。结果就是一定有一方无法正常工作。需要提醒的是端口重用解决的是数据的投递而非业务的处理。一般情况下同一端口监听多个进程处理的应该是相同类型数据。而业务数据则应该从抽象层如架构设计、协议处理或域名判断等等来区别。四、内核源码处理内核中早期处理端口重用使用的是轮询方式。可能觉得这种情况应用场景相对较少另外也可能当时觉得轮询就能满足情况。但随着应用的越来越广泛数据量也愈发增加。在Linux4.5后提供了哈希的方法后来又增加了BPF相关的内核扩展支持有兴趣的可以参看前面的eBPF相关的文章。哈希的优势是非常明显的除了可以保证高效率的负载均衡更重要的是它可以提高缓存的命中率并避免轮询可能引起的惊群。下面看一下内核中的处理源码// include/net/sock_reuseport.h//net/core/sock_reuseport.cstructsock_reuseport{structrcu_headrcu;u16 max_socks;/* length of socks */u16 num_socks;/* elements in socks */u16 num_closed_socks;/* closed elements in socks *//* The last synq overflow event timestamp of this * reuse-socks[] group. */unsignedintsynq_overflow_ts;/* ID stays the same even after the size of socks[] grows. */unsignedintreuseport_id;unsignedintbind_inany:1;unsignedinthas_conns:1;structbpf_prog__rcu*prog;/* optional BPF sock selector */structsock*socks[];/* array of sock pointers */};staticstructsock*reuseport_select_sock_by_hash(structsock_reuseport*reuse,u32 hash,u16 num_socks){inti,j;ijreciprocal_scale(hash,num_socks);while(reuse-socks[i]-sk_stateTCP_ESTABLISHED){i;if(inum_socks)i0;if(ij)returnNULL;}returnreuse-socks[i];}structsock*reuseport_select_sock(structsock*sk,u32 hash,structsk_buff*skb,inthdr_len){structsock_reuseport*reuse;structbpf_prog*prog;structsock*sk2NULL;u16 socks;rcu_read_lock();reusercu_dereference(sk-sk_reuseport_cb);/* if memory allocation failed or add call is not yet complete */if(!reuse)gotoout;progrcu_dereference(reuse-prog);socksREAD_ONCE(reuse-num_socks);if(likely(socks)){/* paired with smp_wmb() in __reuseport_add_sock() */smp_rmb();if(!prog||!skb)gotoselect_by_hash;if(prog-typeBPF_PROG_TYPE_SK_REUSEPORT)sk2bpf_run_sk_reuseport(reuse,sk,prog,skb,NULL,hash);elsesk2run_bpf_filter(reuse,socks,prog,skb,hdr_len);select_by_hash:/* no bpf or invalid bpf result: fall back to hash usage */if(!sk2)sk2reuseport_select_sock_by_hash(reuse,hash,socks);}out:rcu_read_unlock();returnsk2;}EXPORT_SYMBOL(reuseport_select_sock);//net/ipv4/inet_hashtables.c 协议查找跳转staticinlinestructsock*lookup_reuseport(structnet*net,structsock*sk,structsk_buff*skb,intdoff,__be32 saddr,__be16 sport,__be32 daddr,unsignedshorthnum){structsock*reuse_skNULL;u32 phash;if(sk-sk_reuseport){phashinet_ehashfn(net,daddr,hnum,saddr,sport);reuse_skreuseport_select_sock(sk,phash,skb,doff);}returnreuse_sk;}staticinlinestructsock*inet_lookup_run_bpf(structnet*net,structinet_hashinfo*hashinfo,structsk_buff*skb,intdoff,__be32 saddr,__be16 sport,__be32 daddr,u16 hnum,constintdif){structsock*sk,*reuse_sk;bool no_reuseport;if(hashinfo!net-ipv4.tcp_death_row.hashinfo)returnNULL;/* only TCP is supported */no_reuseportbpf_sk_lookup_run_v4(net,IPPROTO_TCP,saddr,sport,daddr,hnum,dif,sk);if(no_reuseport||IS_ERR_OR_NULL(sk))returnsk;reuse_sklookup_reuseport(net,sk,skb,doff,saddr,sport,daddr,hnum);if(reuse_sk)skreuse_sk;returnsk;}structsock*__inet_lookup_listener(structnet*net,structinet_hashinfo*hashinfo,structsk_buff*skb,intdoff,const__be32 saddr,__be16 sport,const__be32 daddr,constunsignedshorthnum,constintdif,constintsdif){structinet_listen_hashbucket*ilb2;structsock*resultNULL;unsignedinthash2;/* Lookup redirect from BPF */if(static_branch_unlikely(bpf_sk_lookup_enabled)){resultinet_lookup_run_bpf(net,hashinfo,skb,doff,saddr,sport,daddr,hnum,dif);if(result)gotodone;}hash2ipv4_portaddr_hash(net,daddr,hnum);ilb2inet_lhash2_bucket(hashinfo,hash2);resultinet_lhash2_lookup(net,ilb2,skb,doff,saddr,sport,daddr,hnum,dif,sdif);if(result)gotodone;/* Lookup lhash2 with INADDR_ANY */hash2ipv4_portaddr_hash(net,htonl(INADDR_ANY),hnum);ilb2inet_lhash2_bucket(hashinfo,hash2);resultinet_lhash2_lookup(net,ilb2,skb,doff,saddr,sport,htonl(INADDR_ANY),hnum,dif,sdif);done:if(IS_ERR(result))returnNULL;returnresult;}EXPORT_SYMBOL_GPL(__inet_lookup_listener);在inet6_hashtables.c中会对相关的端口重用哈希机制进行处理包括创建、监听、查找和销毁等等。而且可以看到对BPF的支持。但真正的端口重用的具体细节处理是在sock_reuseport.c中可以看到数据结构体中的相关的定义包括对BFP的支持。需要说明的是在这个版本中BPF程序是优先级处理最高的如果没有则会使用内核哈希表的算法从而映射到具体的数组中的Socket。五、总结抓大放小要控制好关键点。在应用到具体的技术细节时要明白如何从源头下手将原理和实现对照分析从而能够更好的理解和掌握技术的整体的演进路线。本文分析的这个端口重用就是一个非常好的例子可以将一些开发者经常忽略的如五元组等基础知识与实践应用紧密的结合起来加深印象的同时明白网络通信机制的原理。在当前云时代容器大为流行这种端口的复用更是一种常态所以明白其根本就更显得重要了。