【tcp解惑】多进程可以监听同一端口吗
nanshan 2024-12-16 15:03 10 浏览 0 评论
当然可以,只要你使用 SO_REUSEPORT 这个参数。
还是先来看下man文档中是怎么说的:
SO_REUSEPORT (since Linux 3.9)
Permits multiple AF_INET or AF_INET6 sockets to be bound to an
identical socket address. This option must be set on each
socket (including the first socket) prior to calling bind(2)
on the socket. To prevent port hijacking, all of the pro‐
cesses binding to the same address must have the same effec‐
tive UID. This option can be employed with both TCP and UDP
sockets.
For TCP sockets, this option allows accept(2) load distribu‐
tion in a multi-threaded server to be improved by using a dis‐
tinct listener socket for each thread. This provides improved
load distribution as compared to traditional techniques such
using a single accept(2)ing thread that distributes connec‐
tions, or having multiple threads that compete to accept(2)
from the same socket.
For UDP sockets, the use of this option can provide better
distribution of incoming datagrams to multiple processes (or
threads) as compared to the traditional technique of having
multiple processes compete to receive datagrams on the same
socket.
从文档中可以看到,该参数允许多个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>
static int tcp_listen(char *ip, int port) {
int lfd, opt, err;
struct sockaddr_in addr;
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, (struct sockaddr *)&addr, sizeof(addr));
assert(!err);
err = listen(lfd, 8);
assert(!err);
return lfd;
}
int main(int argc, char *argv[]) {
int lfd, sfd;
lfd = tcp_listen("127.0.0.1", 8888);
while (1) {
sfd = accept(lfd, NULL, NULL);
close(sfd);
printf("接收到tcp连接:%d\n", sfd);
}
return 0;
}
编译并执行该程序:
$ gcc server.c && ./a.out
看下当前8888端口的所有socket的状态:
$ ss -antp | grep 8888
LISTEN 0 8 127.0.0.1:8888 0.0.0.0:* users:(("a.out",pid=32505,fd=3))
和我们预想的一样,只有一个socket处于listen状态。
我们再执行一次该程序:
$ gcc server.c && ./a.out
再次查看8888端口socket的状态:
$ ss -antp | grep 8888
LISTEN 0 8 127.0.0.1:8888 0.0.0.0:* users:(("a.out",pid=32607,fd=3))
LISTEN 0 8 127.0.0.1:8888 0.0.0.0:* users:(("a.out",pid=32505,fd=3))
此时已经出现两个socket在监听8888端口(注意它们的ip地址也是一样的),而这两个socket分别属于两个进程。
我们现在再用ncat模拟客户端,连接8888端口:
$ ncat localhost 8888
重复该操作,建立n个到8888端口的tcp连接,此时两个服务端终端的输出如下。
服务端1:
$ gcc server.c && ./a.out
接收到tcp连接:4
接收到tcp连接:4
接收到tcp连接:4
服务端2:
$ gcc server.c && ./a.out
接收到tcp连接:4
接收到tcp连接:4
可以看到,tcp连接基本上算是均匀分布到两个服务器上,神奇。
相关视频推荐
从netmap到dpdk,从硬件到协议栈,4个维度让网络体系构建起来
需要C/C++ Linux服务器架构师学习资料加qun812855908获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享
下面我们来看到对应的linux内核代码,看看它是如何实现的。
// net/ipv4/inet_connection_sock.c
int inet_csk_get_port(struct sock *sk, unsigned short snum)
{
...
struct inet_hashinfo *hinfo = sk->sk_prot->h.hashinfo;
int ret = 1, port = snum;
struct inet_bind_hashbucket *head;
...
struct inet_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)
goto tb_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))
goto success;
...
}
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.c
static inline int sk_reuseport_match(struct inet_bind_bucket *tb,
struct sock *sk)
{
...
if (tb->fastreuseport <= 0)
return 0;
if (!sk->sk_reuseport)
return 0;
...
if (tb->fastreuseport == FASTREUSEPORT_ANY)
return 1;
...
}
由于上一次listen操作,tb->fastreuseport被设置为FASTREUSEPORT_ANY,而此次listen操作的socket,又设置了SO_REUSEPORT参数,即sk->sk_reuseport值大于0,所以,该方法最终返回true。
由上可见,设置了SO_REUSEPORT参数之后,第二次listen中的bind操作是没用问题的,我们再看下对应的listen操作:
// net/core/sock_reuseport.c
int reuseport_add_sock(struct sock *sk, struct sock *sk2, bool bind_inany)
{
struct sock_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.c
struct sock *reuseport_select_sock(struct sock *sk,
u32 hash,
struct sk_buff *skb,
int hdr_len)
{
struct sock_reuseport *reuse;
...
struct sock *sk2 = NULL;
u16 socks;
...
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)];
}
...
return sk2;
}
EXPORT_SYMBOL(reuseport_select_sock);
看到了吧,该方法中,最后使用了reciprocal_scale方法,计算被选中的listen socket的索引,最后返回这个listen socket继续处理tcp连接请求。
看下reciprocal_scale方法是如何实现的:
// include/linux/kernel.h
/**
* reciprocal_scale - "scale" a value into range [0, ep_ro)
* ...
*/
static inline u32 reciprocal_scale(u32 val, u32 ep_ro)
{
return (u32)(((u64) val * ep_ro) >> 32);
}
算法虽然我们看不懂,但通过其注释我们可以知道,它返回的值的区间是[0, ep_ro),再结合上面的reuseport_select_sock方法我们可以确定,返回的就是所有listen socket的数组下标索引。
至此,有关SO_REUSEPORT参数的内容我们就讲完了。
相关推荐
- 人人视频崩了怎么回事 人人视频下架了吗为什么刷不出来了
-
[海峡网]人人视频挂了吗下架了吗人人视频崩了怎么回事刷不出来了?人人视频发生了什么?怎么都看不成了,暂时还是永久?本来还以为是手机的问题,原来是客户端崩了难怪一直显示服务器异常!追着的美剧突然都下架...
- 502 bad gateway怎么解决?(502 bad gatewaynginxundefined)
-
相信许多小伙伴都遇到打开的网页提示502badgateway,502badgateway是提示用户该网址的网关错误,Web服务器作为网关或代理服务器时收到无效的响应,不管怎么刷新怎么重新输入地...
- 2岁男童眼睛被滴入强酸药水,医生亲身“试”药吓坏了……
-
平日里,小孩子爱玩爱闹是常事儿,但是大人的注意力如果时不时的掉线可就麻烦了。这一天,杭师大附院眼科陈舒主任医师像往常一样在接诊病人,然而一个孩子的哭闹声引起了她的注意,一群人神情焦灼,簇拥着一个孩子急...
- 3岁男童误将502胶当滴眼液,幸好妈妈及时处理,医生也为她点赞
-
小孩子由于心智还不够成熟,因此往往会做出一些危险的事情,甚至对自己造成伤害,这就需要家长的监督和保护。巧也不巧,日常生活中能够对孩子造成威胁的东西实在是太多了,堪称数不胜数,水笔、筷子、桌角,甚至刚拖...
- 5岁娃把502胶当眼药水滴眼中,爸爸的做法很机智,医生都称赞
-
文|哑铃妈妈家里有小孩子的一定要注意,在我们的家里存在很多的安全隐患,有的时候连家长都想不到的东西,竟然对孩子带来了伤害。5岁娃把502胶当眼药水滴眼中,爸爸的做法很机智,医生都点赞女孩乐乐长得可爱,...
- 宝宝误食502胶水,连忙送医救治,医生却夸宝妈做得好
-
有了孩子之后,妈妈都会变得神经敏感,生怕自己没有把孩子照顾好,但是毕竟一个人的经历是有限的,再加上孩子要是会走路,会说话之后对宝妈来说更是一种挑战,危及可能无时无刻不存在,这不,因为宝妈一转身的功夫,...
- 记一次Netty「直接内存溢出」导致线上网关项目宕机排查过程
-
作为一名Java开发者,我们都知道Java进程是运行在Java虚拟机上的,而Java进程要想正常运行则需要向计算机申请内存,其中主要为Java对象实例所占用的堆(heap)内存(当然还有其他的也会占用...
- 刚刚,突然崩了!网易云音乐紧急回应
-
今天下午#网易云音乐崩了#登上微博热搜第一在社交平台上,不少网友反馈,网易云音乐疑似崩溃。网友晒出网页端出现“502BadGateway”的服务器错误,同时网易云音乐的移动应用程序也无法正常使用。...
- 常见状态码(常见的状态码)
-
一二三四五原则:(即一:消息系列;二:成功系列;三:重定向系列;四:请求错误系列;五:服务器端错误系列。301状态码是永久移动302是临时移动304如果请求头中带有If-None-Match...
- 8岁男孩眼睛溅入502胶水,妈妈一番操作结果粘得更紧了
-
家有小孩的爸妈们肯定会多留个心眼照看虽然生活中已经时刻留意可能造成伤害的物品但有时一不留神幼小的孩子就会做出让人担心的事↓↓↓家住深圳的辰辰(化名)今年8岁了3月31日他在家里做手工时想要用未开封的5...
- 3岁娃滴502胶水在眼睛疼的尖叫,宝妈急中生智,保住孩子眼睛
-
但还好宝妈急中生智,连忙将孩子带到水龙头处,用水给孩子冲洗了一下眼睛,还用大量的生理盐水来给孩子清洗眼球,之后又立马将孩子送往医院,最后孩子的眼睛也没有什么大碍,拿了点药就能顺利出院了。而502胶水这...
- 网易云音乐回应App崩了:故障已陆续修复,补偿7天会员
-
2024年8月19日下午,多名网友反馈称,网易云音乐服务器疑似出现故障,登录网易云音乐APP后发现,个性化推荐和搜索功能均无法使用,并收到“获取数据失败”的提示。此外,网易云音乐的网页端也显示502错...
- 又崩了!不少人直接傻眼:太离谱!(台湾人到大陆后傻眼)
-
造车新势力哪吒汽车再被推向舆论风口。5月4日,话题#曝哪吒汽车APP断网#冲上微博热搜App断网无法使用从5月2日开始,陆续有多位网友反映哪吒汽车App断网,App控车无法使用。哪吒汽车App目前出现...
- 男子误把502胶水当眼药水!千万别犯这种低级错误!
-
你敢相信吗?有人竟然误把五零二胶水当成了眼药水滴进眼睛里。这可不是什么玩笑话,而是近日发生在武汉的一起真实事件。一名男子因此导致眼角膜严重受损,不得不紧急就医。据武汉大学附属爱尔眼科医院报道,这名男子...
- 502入眼危机!这份急救指南请牢记(502进入眼中怎么办)
-
502入眼,真实案例触目惊心生活中,502胶水是常用的黏合剂,以其强力黏合性备受青睐。但它一旦进入眼睛,后果不堪设想,下面这些真实案例,足以让我们警醒。曾有这样一则新闻,一位4岁女童在家玩耍时...
你 发表评论:
欢迎- 一周热门
-
-
UOS服务器操作系统防火墙设置(uos20关闭防火墙)
-
极空间如何无损移机,新Z4 Pro又有哪些升级?极空间Z4 Pro深度体验
-
如何修复用户配置文件服务在 WINDOWS 上登录失败的问题
-
手机如何设置与显示准确时间的详细指南
-
如何在安装前及安装后修改黑群晖的Mac地址和Sn系列号
-
日本海上自卫队的军衔制度(日本海上自卫队的军衔制度是什么)
-
10个免费文件中转服务站,分享文件简单方便,你知道几个?
-
爱折腾的特斯拉车主必看!手把手教你TESLAMATE的备份和恢复
-
NAS:DS video/DS file/DS photo等群晖移动端APP远程访问的教程
-
FANUC 0i-TF数据备份方法(fanuc系统备份教程)
-
- 最近发表
- 标签列表
-
- linux 查询端口号 (58)
- docker映射容器目录到宿主机 (66)
- 杀端口 (60)
- yum更换阿里源 (62)
- internet explorer 增强的安全配置已启用 (65)
- linux自动挂载 (56)
- 禁用selinux (55)
- sysv-rc-conf (69)
- ubuntu防火墙状态查看 (64)
- windows server 2022激活密钥 (56)
- 无法与服务器建立安全连接是什么意思 (74)
- 443/80端口被占用怎么解决 (56)
- ping无法访问目标主机怎么解决 (58)
- fdatasync (59)
- 405 not allowed (56)
- 免备案虚拟主机zxhost (55)
- linux根据pid查看进程 (60)
- dhcp工具 (62)
- mysql 1045 (57)
- 宝塔远程工具 (56)
- ssh服务器拒绝了密码 请再试一次 (56)
- ubuntu卸载docker (56)
- linux查看nginx状态 (63)
- tomcat 乱码 (76)
- 2008r2激活序列号 (65)