一台主机上只能保持最多的存储器 一台主机上只能保持最多
我是一个 Linux 服务器上的进程,名叫小进。老是有人说我最多只能创建 65535 个 TCP 连接。我不信这个邪,今天我要亲自去实践一下。我走到操作系统老大的跟前,说:"老操,我要建立一个 tcp 连接!"老操不慌不忙,拿出一个表格递给我,"小进,先填表吧"
我一看这个表,这不就是经典的socket 四元组嘛。我只有一块网卡,其 IP 地址是123.126.45.68,我想要与110.242.68.3的 80 端口建立一个 TCP 连接,我将这些信息填写在了表中。
源端口号填什么呢?我记得端口号是 16 位的,可以有 0 ~ 65535 这个范围的数字,那我随便选一个吧!正当我犹豫到底选什么数字的时候,老操一把抢过我的表格。"你墨迹个啥呢小进?源端口号不用你填,我会给你分配一个可用的数字。源IP也不用你填,我知道都有哪些网卡,并且会帮你选个合适的。真是个新手,回去等消息吧。""哦"老操带着我的表格,走了。过了很长时间,老操终于回来了,并且带着一个纸条。
"小进,你把这个收好了。"我问道,"这是啥呀?"老操不耐烦地说道,"刚刚说你是新手你还不服,这个 5 表示文件描述符,Linux 下一切皆文件,你待会和你那个目标 IP 进行 TCP 通信的时候,就对着这个文件描述符读写就好啦。""这么方便!好的,谢谢老操。"我拿着这个文件描述符,把它放到属于我的内存中裱起来了,反正我只是想看看最多能创建多少 TCP 连接,又不是去真的用它,嘻嘻。
端口号过了一分钟,我又去找老操了。"老操,我要建立一个 TCP 连接!"老操不慌不忙,拿出一个表格递给我,"小进,先填表吧"
这回我熟悉了,只把目标IP和目标端口填好。
老操办好事之后,又带着一个纸条回来,上面写着数字"6"。就这样,我每隔一分钟都去找老操建立一个新的 TCP 连接,目标 IP 都是110.242.68.3,目标端口都是 80。老操也很奇怪,不知道我在这折腾啥,他虽然权力大,但无权拒绝我的指令,每次都兢兢业业地把事情办好,并给我一张一张写着文件描述符的纸条。直到有一次,我收到的纸条有些不同。
我带着些许责怪的语气问,"老操,这是怎么回事呀?"老操也没好气地说,"这表示端口号不够用啦!早就觉得你小子不对劲了,一个劲地对着同一个 IP 和端口创建 TCP 连接,之前没办法必须执行你给的指令,现在不行了,端口号不够用了,源端口那里我没法给你填了。"我也不是那么好骗的,质疑道。"老操,你也别欺负我这个新手,我可是知道端口号是 16 位的,范围是 1~65535,一共可以创建 65535 个 TCP 连接,我现在才创建了 63977 个,怎么就不够了!"老操鄙视地看了我一眼,"你小子可真是闲的蛋疼啊,还真一个个数,来我告诉你吧,Linux 对可使用的端口范围是有具体限制的,具体可以用如下命令查看。"
[root]# cat /proc/sys/net/ipv4/ip_local_port_range
1024 65000
"看到没,当前的限制是1024~65000,所以你就只能有63977个端口号可以使用。"
我赶紧像老操道歉,"哎哟真是抱歉,还是我见识太少,那这个数可以修改么?"老操也没跟我一般见识,还是耐心地回答我,"可以的,具体可以 vim /etc/sysctl.conf 这个文件进行修改,我们在这个文件里添加一行记录"
net.ipv4.ip_local_port_range = 60000 60009
"保存好后执行 sysctl -p /etc/sysctl.conf 使其生效。这样你就只有 10 个端口号可以用了,就会更快报出端口号不够用的错误""原来如此,谢谢老操又给我上了一课。"哎不对,建立一个 TCP 连接,需要将通信两端的套接字(socket)进行绑定,如下:源 IP 地址:源端口号 <----> 目标 IP 地址:目标端口号只要这套绑定关系构成的四元组不重复即可,刚刚端口号不够用了,是因为我一直对同一个目标IP和端口建立连接,那我换一个目标端口号试试。
我又把这个表交给老操,老操一眼就看破了我的小心思,可是也没办法,马上去给我建立了一个新的TCP连接,并且成功返回给我一个新的文件描述符纸条。看来成功了,只要源端口号不用够用了,就不断变换目标 IP 和目标端口号,保证四元组不重复,我就能创建好多好多 TCP 连接啦!这也证明了有人说最多只能创建 65535 个TCP连接是多么荒唐。
文件描述符找到了突破端口号限制的办法,我不断找老操建立TCP连接,老操也拿我没有办法。直到有一次,我又收到了一张特殊的纸条,上面写的不是文件描述符。
我又没好气地问老操,"这又是咋回事?"老操幸灾乐祸地告诉我,"呵呵,你小子以为突破端口号限制就无法无天了?现在文件描述符不够用啦!""怎么啥啥都有限制啊?你们操作系统给我们的限制也太多了吧?""废话,你看看你都建了多少个TCP连接了!每建立一个TCP连接,我就得分配给你一个文件描述符,linux 对可打开的文件描述符的数量分别作了三个方面的限制。"
系统级:当前系统可打开的最大数量,通过 cat /proc/sys/fs/file-max 查看
用户级:指定用户可打开的最大数量,通过 cat /etc/security/limits.conf 查看
进程级:单个进程可打开的最大数量,通过 cat /proc/sys/fs/nr_open 查看
天呢,真是人在屋檐下呀,我赶紧看了看这些具体的限制。
[root ~]# cat /proc/sys/fs/file-max
100000
[root ~]# cat /proc/sys/fs/nr_open
100000
[root ~]# cat /etc/security/limits.conf
...
* soft nproc 100000
* hard nproc 100000
原来如此,我记得刚刚收到的最后一张纸条是。
再之后就收到文件描述符不够的错误了。我又请教老操,"老操,那这个限制可以修改么?"老操仍然耐心地告诉我,"当然可以,比如你想修改单个进程可打开的最大文件描述符限制为100,可以这样。"
echo 100 > /proc/sys/fs/nr_open
"原来如此,我这就去把各种文件描述符限制都改大一点,也不多,就在后面加个0吧""额,早知道不告诉你小子了。"老操再次用鄙视的眼睛看着我。
线程突破了文件描述符限制,我又开始肆无忌惮地创建起了TCP连接。但我发现,老操的办事效率越来越慢,建立一个TCP连接花的时间越来越久。有一次,我忍不住责问老操,"你是不是在偷懒啊?之前找你建一个TCP连接就花不到一分钟时间,你看看最近我哪次不是等一个多小时你才搞好?"老操也忍不住了,"小进啊你还好意思说我,你知不知道你每建一个TCP连接都需要消耗一个线程来为你服务?现在我和CPU老大那里都忙得不可开交了,一直在为你这好几十万个线程不停地进行上下文切换,我们精力有限啊,自然就没法像以前那么快为你服务了。"
听完老操的抱怨,我想起了之前似乎有人跟我说过C10K问题,就是当服务器连接数达到 1 万且每个连接都需要消耗一个线程资源时,操作系统就会不停地忙于线程的上下文切换,最终导致系统崩溃,这可不是闹着玩的。我赶紧像操作系统老大请教,"老操,实在不好意思,一直以为你强大无比,没想到也有忙得不可开交的时候呀,那我们现在应该怎么办呀?"老操无奈地说,"我劝你还是别再继续玩了,没什么意义,不过我想你也不会听我的,那我跟你说两句吧。"你现在这种每建一个TCP连接就创建一个线程的方式,是最传统的多线程并发模型,早期的操作系统也只支持这种方式。但现在我进化了,我还支持 IO 多路复用的方式,简单说就是一个线程可以管理多个 TCP 连接的资源,这样你就可以用少量的线程来管理大量的 TCP 连接了。
我一脸疑惑,"啥是 IO 多路复用啊?"。老操一脸鄙视,"你这... 你去看看闪客的《你管这破玩意叫 IO 多路复用》,就明白了。"这次真是大开眼界了,我赶紧把代码改成了这种 IO 多路复用的模型,将原来的 TCP 连接销毁掉,改成同一个线程管理多个 TCP 连接,很快,操作系统老大就恢复了以往的办事效率,同时我的 TCP 连接数又多了起来。
内存突破了端口号、文件描述符、线程数等重重限制的我,再次肆无忌惮地创建起了TCP连接。直到有一次,我又收到了一张红牌。
嗨,又是啥东西限制了呀,改了不就完了。我不耐烦地问老操,"这回又是啥毛病?"老操说道。"这个错误叫内存溢出,每个TCP连接本身,以及这个连接所用到的缓冲区,都是需要占用一定内存的,现在内存已经被你占满了,不够用了,所以报了这个错。"
我看这次老操特别耐心,也没多说什么,但想着被内存限制住了,有点不太开心,于是我让老操帮我最后一个忙。"老操呀,帮小进我最后一个忙吧,你权利大,你看看把那些特别占内存的进程给杀掉,给我腾出点地方,我今天要完成我的梦想,看看TCP连接数到底能创建多少个!"老操见我真的是够拼的,便答应了我,杀死了好多进程,我很是感动。
CPU有了老操为我争取的内存资源,我又开始日以继日地创建TCP连接。老操也不再说什么,同样日以继日地执行着我的指令。有一次,老操语重心长地对我说,"差不多了,我劝你就此收手吧,现在 CPU 的占用率已经快到 100% 了。"
我觉得老操这人真的可笑,经过这几次的小挫折,我明白了只要思想不滑坡,方法总比苦难多,老操这人就是太谨慎了,我岂能半途而废,不管他。我仍然继续创建着 TCP 连接。直到有一天,老操把我请到一个小饭馆,一块吃了顿饭,吃好后说道。"咱哥俩也算是配合了很久啦,今天我是来跟你道个别的。"我很不解地问,"怎么了老操,发生什么事了?。"老操
说,"由于你的 TCP 连接,CPU 占用率已经很长时间维持在 100%,我们的使用者,也就是我们的上帝,几乎什么事情都做不了了,连鼠标动一下都要等好久,所以他给我下达了一个重启的指令,我执行这个指令后,你,以及像你一样的所有进程,包括我这个操作系统本身,一切都就消失了。"我大惊失色,"啊,这么突然么?这条指令什么时候执行?"老操缓缓起身,"就现在了,刚刚这条指令还没得到 CPU 运行的机会,不过现在到了。"突然,我眼前一黑,一切都没了。
总结
资源 |
一台Linux服务器的资源 |
一个TCP连接占用的资源 |
占满了会发生什么 |
CPU |
看你花多少钱买的 |
看你用它干嘛 |
电脑卡死 |
内存 |
看你花多少钱买的 |
取决于缓冲区大小 |
OOM |
临时端口号 |
ip_local_port_range |
1 |
cannot assign requested address |
文件描述符 |
fs.file-max |
1 |
too many open files |
进程\线程数 |
ulimit -n |
看IO模型 |
系统崩溃 |
------------------------------------------ 追更 ----------------------------------------------
想不到突然获得这么多赞,其实我写的原文中还有后记部分,被我删了,因为有几个动图总是动不起来,大家感兴趣的话可以再看看原文哈。
最多能创建多少个 TCP 连接?
编辑于 2021-05-07 14:15
赞同 393388 条评论
分享
收藏喜欢
收起
更多回答
张彦飞
北京搜狗网络技术有限公司 专家开发工程师
关注
2,240 人赞同了该回答
几年前的我也产生过同样的困惑。
为了给自己解惑,我扒内核源码,做测试实验,写技术文章,从头到尾把这个问题扒了一遍。
要想把这个问题搞清楚,关键的地方在于要把TCP连接的两端里的客户端和服务端两个角色分开来讨论。 因为它两对端口号的使用方式不一样,区分开了能讨论的更清晰。
先抛出结论,无论是服务端还是客户端,单机支撑 100W 以上的连接都是没有问题的。
我在 4GB 的机器上都测试过的。如果内存更大,能支持的连接数会更多。咱们先从理论讲起来。
一、TCP 并发理论基础1 服务器理论最大并发数
TCP连接四元组是由源IP地址、源端口、目的IP地址和目的端口构成。
当四元组中任意一个元素发生了改变,那么就代表的是一条完全不同的新连接。
我们算下服务器上理论上能达成的最高并发数量。拿我们常用的 Nginx 举例,假设它的 IP 是 A,端口80。这样就只剩下源IP地址、源端口是可变的。
IP 地址是一个 32 位的整数,所以源 IP 最大有 2 的 32 次方这么多个。 端口是一个 16 位的整数,所以端口的数量就是 2 的 16 次方。
2 的 32 次方(ip数)× 2的 16 次方(port数)大约等于两百多万亿。
所以理论上,我们每个 server 可以接收的连接上限就是两百多万亿。(不过每条 TCP 连接都会消耗服务器内存,实践中绝不可能达到这个理论数字,稍后我们就能看到。)
2 客户端理论最大并发数
注意:这里的客户端是一个角色,并不具体指的是哪台机器。当你的 Java/c/go 程序响应用户请求的时候,它是服务端。当它访问 redis/mysql 的时候,你这台机器就变成客户端角色了。这里假设我们一台机器只用来当客户端角色。
我们再算一下客户端的最大并发数的上限。
很多同学认为一台 Linux 客户端最多只能发起 64 k 条 TCP 连接。因为TCP 协议规定的端口数量有 65535 个,但是一般的系统里 1024 以下的端口都是保留的,所以没法用。可用的大约就是 64 k 个。
但实际上客户端可以发出的连接远远不止这个数。咱们看看以下两种情况
情况1: 这个 64 k 的端口号实际上说的是一个 IP 下的可用端口号数量。而一台 Linux 机器上是可以配置多个 IP 的。假如配置了 20 个 IP,那这样一台客户端机就可以发起 120 万多个 TCP 连接了。
情况2: 再退一步讲,假定一台 Linux 上确实只有一个 IP,那它就只能发起 64 k 条连接了吗? 其实也不是的。
根据四元组的理论,只要服务器的 IP 或者端口不一样,即使客户端的 IP 和端口是一样的。这个四元组也是属于一条完全不同的新连接。
比如下面的两条连接里,虽然客户端的 IP 和端口完全一样,但由于服务器侧的端口不同,所以仍然是两条不同的连接。
- 连接1:客户端IP 10000 服务器IP 10000
- 连接2:客户端IP 10000 服务器IP 20000
所以一台客户端机器理论并发最大数是一个比服务器的两百万亿更大的一个天文数字(因为四元组里每一个元素都能变)。这里就不展开计算了,因为已经没有意义了。
3 Linux 最大文件描述符限制
linux 下一切皆文件,包括 socket。所以每当进程打开一个 socket 时候,内核实际上都会创建包括 file 在内的几个内核对象。该进程如果打开了两个 socket,那么它的内核对象结构如下图。
进程打开文件时消耗内核对象,换一句直白的话就是打开文件对象吃内存。所以linux系统出于安全角度的考虑,在多个位置都限制了可打开的文件描述符的数量,包括系统级、进程级、用户进程级。
- fs.file-max: 当前系统可打开的最大数量
- fs.nr_open: 当前系统单个进程可打开的最大数量
- nofile: 每个用户的进程可打开的最大数量
本文的实验要涉及对以上参数的修改。
4 TCP 连接的内存开销
介绍内存开销之前,需要先理解内核的内存使用方式。只有理解了这个,才能深刻理解 TCP 连接的内存开销。
Linux 内核和应用程序使用的是完全不同的两套机制。 Linux 给它的内核对象分配使用 SLAB 的方式。
一个 slab 一般由一个或者多个 Page 组成(每个 Page 一般为 4 KB)。在一个 slab 内只分配特定大小、甚至是特定的对象。这样当一个对象释放内存后,另一个同类对象可以直接使用这块内存。通过这种办法极大地降低了碎片发生的几率。
Linux 提供了 slabtop 命令来按照占用内存从大往小进行排列,这对我们查看内核对象的内存开销非常方便。
在 Linux 3.10.0 版本中,创建一个socket 需要消耗 densty、flip、sock_inode_cache、TCP 四个内核对象。这些对象加起来总共需要消耗大约 3 KB 多一点的内存。
如果连接上有数据收发的话,还需要消耗发送、接收缓存区。这两个缓存区占用内存影响因素比较多,既受收发数据的大小,也受 tcp_rmem、tcp_wmem 等内核参数,还取决于服务器进程能否及时接收(及时接收的话缓存区就能回收)。总之影响因素比较多,不同业务之间实际情况差别太大,比较复杂。所以不在本文讨论范围之内。
二、百万连接达成实验了解了理论基础后,其实你的疑惑就得到基本的解释了。剩下的事情你可以把我的这本电子书下载回去慢慢看。
下载地址在这里:
飞哥的《理解了实现再谈网络性能》电子书发布啦!mp.weixin.qq/s/xlRxat5F-G6eZqY9M2JDog
咱们来接着继续来看实验过程。
本实验需要准备两台机器。一台作为客户端,另一台作为服务器。如果你选用的是 c 或者 php 源码,这两台机器内存只要大于 4GB 就可以。 如果使用的是 Java 源码,内存要大于 6 GB。对 cpu 配置无要求,哪怕只有 1 个核都够用。
本方案中采用的方法是在一台客户端机器上配置多个 ip 的方式来发起所有的 tcp 连接请求。所以需要为你的客户端准备 20 个 IP,而且要确保这些 IP 在内网环境中没有被其它机器使用。如果实在选不出这些 IP,那么可以采用百看不如一练,动手测试单机百万连接的保姆级教程! 一文中的方案二。
除了用 20 个 IP 以外,也可以使用 20 台客户端。每个客户端发起 5 万个连接同时来连接这一个server。但是这个方法实际操作起来太困难了。
客户端机和服务器分别下载源码:
github/yanfeizhang/coder-kung-fu/tree/main/tests/network/test02
下面我们来详细看每一个实验步骤。
1 调整客户端可用端口范围
默认情况下,Linux 只开启了 3 万多个可用端口。但我们今天的实验里,客户端一个进程要达到 5 万的并发。所以,端口范围的内核参数需要修改。
#vi /etc/sysctl.conf
net.ipv4.ip_local_port_range = 5000 65000
执行 sysctl -p 使之生效。
2 调整客户端最大可打开文件数
我们要测试百万并发,所以客户端的系统级参数 fs.file-max 需要加大到 100 万。另外 Linux 上还会存在一些其它的进程要使用文件,所以我们需要多打一些余量出来,直接设置到 110 万。
对于进程级参数 fs.nr_open 来说,因为我们开启 20 个进程来测,所以它设置到 60000 就够了。这些都在 /etc/sysctl.conf 中修改。
#vi /etc/sysctl.conf
fs.file-max=1100000
fs.nr_open=60000
sysctl -p 使得设置生效。并使用 sysctl -a 查看是否真正 work。
#sysctl -p
#sysctl -a
fs.file-max = 1100000
fs.nr_open = 60000
接着再加大用户进程的最大可打开文件数量限制(nofile)。这两个是用户进程级的,可以按不同的用户来区分配置。 这里为了简单,就直接配置成所有用户 * 了。每个进程最大开到 5 万个文件数就够了。同样预留一点余地,所以设置成 55000。 这些是在 /etc/security/limits.conf 文件中修改。
注意 hard nofile 一定要比 fs.nr_open 要小,否则可能导致用户无法登陆。
# vi /etc/security/limits.conf
* soft nofile 55000
* hard nofile 55000
配置完后,开个新控制台即可生效。 使用 ulimit 命令校验是否生效成功。
#ulimit -n
55000
3 服务器最大可打开文件句柄调整
服务器系统级参数 fs.file-max 也直接设置成 110 万。 另外由于这个方案中服务器是用单进程来接收客户端所有的连接的,所以进程级参数 fs.nr_open, 也一起改成 110 万。
#vi /etc/sysctl.conf
fs.file-max=1100000
fs.nr_open=1100000
sysctl -p 使得设置生效。并使用 sysctl -a 验证是否真正生效。
接着再加大用户进程的最大可打开文件数量限制(nofile),也需要设置到 100 万以上。
# vi /etc/security/limits.conf
* soft nofile 1010000
* hard nofile 1010000
配置完后,开个新控制台即可生效。 使用 ulimit 命令校验是否成功生效。
4 为客户端配置额外 20 个 IP
假设可用的 ip 分别是 CIP1,CIP2,......,CIP20,你也知道你的子网掩码。
注意:这 20 个 ip 必须不能和局域网的其它机器冲突,否则会影响这些机器的正常网络包的收发。
在客户端机器上下载的源码目录 test02 中,找到你喜欢用的语言,进入到目录中找到 tool.sh。修改该 shell 文件,把 IPS 和 NETMASK 都改成你真正要用的。
为了确保局域网内没有这些 ip,最好先执行代码中提供的一个小工具来验证一下
make ping
当所有的 ip 的 ping 结果均为 false 时,进行下一步真正配置 ip 并启动网卡。
make ifup
使用 ifconfig 命令查看 ip 是否配置成功。
#ifconfig
eth0
eth0:0
eth0:1
...
eth:19
5 开始实验
ip 配置完成后,可以开始实验了。
在服务端中的 tool.sh 中可以设置服务器监听的端口,默认是 8090。启动 server
make run-srv
使用 netstat 命令确保 server 监听成功。
netstat -nlt | grep 8090
tcp 0 0.0.0.0:8090 0.0.0.0:* LISTEN
在客户端的 tool.sh 中设置好服务器的 ip 和端口。然后开始连接
make run-cli
同时,另启一个控制台。使用 watch 命令来实时观测 ESTABLISH 状态连接的数量。
实验过程中不会一帆风顺,可能会有各种意外情况发生。 这个实验我前前后后至少花了有一周时间,所以你也不要第一次不成功就气馁。 遇到问题根据错误提示看下是哪里不对。然后调整一下,重新做就是了。 重做的时候需要重启客户端和服务器。
对于客户端,杀掉所有的客户端进程的方式是
make stop-cli
对于服务器来说由于是单进程的,所以直接 ctrl c 就可以终止服务器进程了。 如果重启发现端口被占用,那是因为操作系统还没有回收,等一会儿再启动 server 就行。
当你发现连接数量超过 100 万的时候,你的实验就成功了。
watch "ss -ant | grep ESTABLISH"
1000013
这个时候别忘了查看一下你的服务端、客户端的内存开销。
先用 cat proc/meminfo 查看,重点看 slab 内存开销。
$ cat /proc/meminfo
MemTotal: 3922956 kB
MemFree: 96652 kB
MemAvailable: 6448 kB
Buffers: 44396 kB
......
Slab: 3241244KB kB
再用 slabtop 查看一下内核都是分配了哪些内核对象,它们每个的大小各自是多少。
如果发现你的内核对象和上图不同,也不用惊慌。因为不同版本的 Linux 内核使用的内核对象名称和数量可能会有些许差异。
6 结束实验
实验结束的时候,服务器进程直接 ctrl c 取消运行就可以。客户端由于是多进程的,可能需要手工关闭一下。
make stop-cli
最后取消为实验临时配置的新 ip
make ifdown
最后上面实验只用了一种方法,还有另外一种方法,参见百看不如一练,动手测试单机百万连接的保姆级教程! 一文中的方案二。
总结下,一台主机上的 TCP 连接数并不会受端口号 65535 的限制,我们有很多的办法绕开。最终限制最大 TCP 连接数的资源是机器上的内存。
我们上述的实验只涉及了连接本身的内存开销,如果连接上有数据收发你们还需要消耗接收缓存区、发送缓存区内存开销。这个开销受实际收发速度、内核参数配置大小的影响,情况会比较复杂。
不过我用收发 “Hello world” 之类的短消息也简单测试了一下。
下面这些文章是我之前发表的几篇相关的文章。
- 漫画 | 一台Linux服务器最多能支撑多少个TCP连接
- 漫画 | 理解了TCP连接的实现以后,客户端的并发也爆发了!
- 漫画 | 花了七天时间测试,我彻底搞明白了 TCP 的这些内存开销!
- 百看不如一练,动手测试单机百万连接的保姆级教程!
- 说出来你可能不信,内核这家伙在内存的使用上给自己开了个小灶!
我把我在网络方面的研究整理成了本电子书叫《理解了实现再谈网络性能》。深度挖了一下网络在 Linux 上是怎么实现的,进而帮助大家深刻理解网络再 CPU、内存等方面的开销,相信对于你一定有帮助。
飞哥的《理解了实现再谈网络性能》电子书发布啦!mp.weixin.qq/s/xlRxat5F-G6eZqY9M2JDog
2021年04月23日更新:
我前面的回答中粗略提到了客户端理论最大并发数情况2,部分同学表示不理解,我今天就专门针对这个点进行一次更新。
情况 2 中的客户端在只有一个 IP 的情况下,也能发出很多连接,比如 100 W。最主要的原理是客户端的一个端口,可以用于连接多个服务器。
比如下面的两条连接是完全可以同时存在的:
- 连接1:客户端IP1 10000 服务器IP 8090
- 连接2:客户端IP1 10000 服务器IP 8091
在客户端机上,即使只有一个 IP,用一个端口也可以连接两个不同的 Server(服务器 Server 只要 IP 和端口有一个不同就算不同的server)。
这部分同学的疑惑在于,客户端上如果两条连接使用一个端口,那数据包来了不会错乱吗? 答案是不会错乱,我今天好好给大家讲讲!
我们现在假设服务器给客户端机上发过来数据了,我给大家看一下 TCP 数据包接收的源码。
// file: net/ipv4/tcp_ipv4.c
int tcp_v4_rcv(struct sk_buff *skb)
{
......
th = tcp_hdr(skb); //获取tcp header
iph = ip_hdr(skb); //获取ip header
sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
......
}
__inet_lookup_skb 函数是根据收到的网络包来寻找机器上对应的连接的。该函数又会进入到 __inet_lookup_established, 来继续看内核源码。
//file: net/ipv4/inet_hashtables.c
struct sock *__inet_lookup_established(struct net *net,
struct inet_hashinfo *hashinfo,
const __be32 saddr, const __be16 sport,
const __be32 daddr, const u16 hnum,
const int dif)
{
......
begin:
//遍历 sk
sk_nulls_for_each_rcu(sk, node, &head->twchain) {
if (likely(INET_MATCH(sk, net, acookie,
saddr, daddr, ports, dif))) {
...
goto out;
}
}
out:
return sk;
}
根据文件名也能看出来,内核是使用 hashtable 来管理所有的 TCP 连接的(socket)的。该函数中遍历某个 hash 下面的 socket 链表,最后通过 INET_MATCH 进行精确的比对。
#define INET_MATCH(__sk, __net, __cookie, __saddr, __daddr, __ports, __dif) \
((inet_sk(__sk)->inet_portpair == (__ports)) && \
(inet_sk(__sk)->inet_daddr == (__saddr)) && \
(inet_sk(__sk)->inet_rcv_saddr == (__daddr)) && \
(!(__sk)->sk_bound_dev_if || \
((__sk)->sk_bound_dev_if == (__dif))) && \
net_eq(sock_net(__sk), (__net)))
保证客户端使用了相同端口的两条连接不串线的原因就在 INET_MATCH 这里。在这个函数里不仅仅是比较了客户端的端口号,包括四元组在内的标识都比较了一遍。 (注意下,每个 socket 上都保存了连接四元组等信息,所以才能比较)
所以:对于下面这两条连接,因为服务器端口不同,所以当服务器发送数据过来的时候 INET_MATCH 可以精确定位到连接,而不会串线。
- 连接1:客户端IP1 10000 服务器IP 8090
- 连接2:客户端IP1 10000 服务器IP 8091
而且除了内核源码之外,我也还进行了实际测试,而且文中我还提供出来了测试源码,你想玩也可以玩玩。参见:
- 百看不如一练,动手测试单机百万连接的保姆级教程!
- 漫画 | 理解了TCP连接的实现以后,客户端的并发也爆发了!
我再给大伙儿贴一下我实验时候在客户机上实验时的实际截图,来实际看一下一个端口号确实是被用在了多条连接上了。
截图中左边的 192 是客户端,右边的 119 是服务器的 ip。可以看到客户端的 10000 这个端口号是用在了多条连接上了的。
如果你在客户端非得使用 bind 还想让端口重用,那可以开启 so_reuseport 试试,不过我觉得有点多此一举。
最后,我再来说一下我的个人电子书《理解了实现再谈网络性能》的创作思路。
我觉得业界里计算机网络的知识太多都聚集于协议层面了,太过于偏向理论。而对于网络在机器是如何被实现的,开销如何,这部分知识在业界里太少了。 比如网络是如何使用 cpu 的,如何使用内存的,这些只是感觉在整个业界说很匮乏也不为过。
我的整本电子书是对网络性能进行拆解,把性能拆分为三个角度:CPU 开销、内存开销等。
具体到某个角度比如 CPU,那我需要给自己解释清楚网络包是怎么从网卡到内核中的,内核又是通过哪些方式通知进程的。只有理解清楚了这些才能真正把握网络对 CPU 的消耗。
对于内存角度也是一样,只有理解了内核是如何使用内存,甚至需要哪些内核对象都搞清楚,也才能真正理解一条 TCP 连接的内存开销。
除此之外我还增加了一些性能优化建议和前沿技术展望等,最终汇聚出了这本《理解了实现再谈网络性能》。在此无私分享给大家。
下载链接传送门:《理解了实现再谈网络性能》
Github: github/yanfeizhang/coder-kung-fu
编辑于 2021-04-23 09:53
真诚赞赏,手留余香
赞赏
还没有人赞赏,快来当第一个赞赏的人吧!
赞同 224098 条评论
分享
收藏喜欢
收起
文礼
游戏开发等 2 个话题下的优秀答主
关注
417 人赞同了该回答
正确的说法是,服务端的一个特定的TCP端口,只能最多和一个特定的客户端同时保持最多64k个链接。并且,这还只是服务端和客户端各自只有一个IP地址的情况。
就如别的回答提到的,TCP连接依靠服务端IP、服务端端口号、客户端IP、客户端端口号这4个元素来标识。当服务端IP、客户端IP固定时,因为端口号是16bit整数,所以任何一个服务端端口可以和64k个客户端端口形成连接,反之亦然。也就是特定的客户端和特定的服务端之间总共可能的同时TCP连接为64k x 64k=4G个。
但是当客户端IP不同时,它们可以用同一个客户端端口连接到同一个服务端端口。上面的4个因素当中,只要有一个不同,就是不同的连接。
所以理论上,一个服务端端口可以服务最多大约2^32(IPv4)x64k个客户端连接。当然,有一些IP地址为特殊地址,比如广播地址、组播地址、以及诸如10.x.x.x/192.168.x.x这样的本地地址等,对于互联网服务需要排除。
此外,由于操作系统需要为每个连接分配内存以及文件描述子,所以实际能支持的数量远小于理论值。linux系统的通常缺省设置,同时支持的文件描述子在30万上下,内存则每连接独享的至少在1k上下,那么30万连接就在300MB附近。虽然300MB对于当今的服务器物理内存容量可能不是一个很大的数值,但是注意这是操作系统内核空间的内存,非用户态空间。当然,这些限制都是可以通过配置内核参数进行修改的,至少linux是肯定可以的。
实战当中,一般单台服务器的同时TCP连接上限按8万上下估计。这不是一个确切的理论值,只是经验值。而且这只是仅仅考虑TCP连接,不考虑后面的实际工作负载。
,免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com