tcp

作者:灵剑
链接:https://www.zhihu.com/question/51438786/answer/125920692
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。



ocket通信的双方自己决定自己要用什么端口,服务器端只能决定自己listen的是哪个端口,不能决定客户端的端口;客户端也只能决定自己的端口,当然,因为它是主动连接的,所以它知道对方的端口号,但它也不能把对方的服务移到别的端口上面去。端口号是用来区分相同主机上的不同socket的,它相当于socket在本机这一端的名字,你去邮局寄信给别人,收件人总不能改你名字吧?客户端实际上是可以为自己绑定一个特定的端口号的,只要在connect之前先进行bind就行了:from socket import socket



s = socket()
s.bind((‘’, 9999))
s.connect((‘10.1.2.3’, 80))

但是一般来说没有人实际这样做,首先一个端口只能bind一次,占用了之后同时就不能再给别人用了,这么宝贵的资源都要留给listen的一方,主动连接的一方一般去用那些操作系统随机分配的端口号。bind也可以指定端口号为0,这种情况下就是随机绑定一个没有使用过的端口号,可以用来在建立连接之前就确定本端的端口号。某些情况下还是有用的,设想某种网络环境下,我们需要在建立连接之前主动去申请防火墙通过,就可以考虑先进行bind,否则连接建立不起来(不过会有这么傻的设备吗……)作为listen的一方,所有被动建立起来的连接的本端端口都是listen的端口。只有源IP、源端口、目的IP、目的端口还有协议号都完全相同才会认为是同一个socket,所以被动建立连接的socket源端口号不同,目的端口号是可以相同的。

如果是客户端是客户端操作系统分配,与服务端无关,不存在服务端为客户端分配端口,以tcp为例,相关linux内核代码函数如下:如何申请端口:由int inet_csk_get_port(struct sock *sk, unsigned short snum)来完成何时申请端口,客户端发送报文的时候:客户端发送第一个报文,在int inet_sendmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *msg, size_t size)中申请端口 如果是服务端连接端口由服务端bind指定的,以tcp为例,相关linux内核函数如下:int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)另外补充说明一条流由四元组来区分:clientip+clientport+serverip+serverport



如何标识一个TCP连接
在确定最大连接数之前,先来看看系统如何标识一个tcp连接.系统用一个四元组来唯一标识一个TCP连接:{local ip, local port,remote ip,remote port}.
我们知道在网络通信过程中服务端监听一个固定的端口,客户端主动发起连接请求后要经过三次握手才能与服务器建立起一个tcp连接.客户端每次发起一个tcp连接时,系统会随机选取一个空闲的端口,该端口是独占的不能与其他tcp连接共享,因此理论上一台机器有多少空闲的端口,就能对外发起多少个tcp连接,根据tcp/ip协议,端口port使用16位无符号整数unsigned short来存储,因此本地端口一共有2^16=65536个,即0-65535,其中0~1023是预留端口,0有特殊含义不能使用,1024以下端口都是超级管理员用户(如root)才可以使用,因此就算使用root权限,一台机器最多能使用的端口也只有65535个(除去一些保留的和已被占用的端口,实际可能不足这个数).



客户端便抛出了大量拒绝连接的异常,而服务端显示出来的已连接数只有1000多,于是猜测可能是服务端达到了所能接收的最大tcp连接数(即文件描述符个数),因为受服务器资源、操作系统的限制,linux内核默认文件描述符最大值是1024,也就是说默认支持最大并发连接是1024个(每个tcp连接都要占用一定内存,每个socket就是一个文件描述符)
我们输入以下命令查看linux内核参数:



[root@bogon local]# ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 7271
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 7271
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
可以看到其中的open files是1024,接着我们调整内核参数:



ulimit -n 102400
注意以上命令只能临时生效,系统重启后又恢复成默认值.
要想永久生效,在/etc/security/limits.conf文件中配置如下两行:



hard nofile 102400
soft nofile 102400
soft和hard为两种限制方式,其中soft表示警告的限制,hard表示真正限制,nofile表示打开的最大文件数.
但这是针对于单个进程的修改,若想修改系统全局的限制,可以执行下面的操作:



cat /proc/sys/fs/file-max
etc/sysctl.conf
cat /proc/sys/fs/file-max查看我所有进程能够打开的最大文件数是多少,每一个tcp连接代表一个文件,局部的不能大于全局的限制,然后vi etc/sysctl.conf,在里面添加fs.file-max = 102400,file-max表示全局文件句柄数的限制.
最后看看执行结果:当server端连接数到达28232时,client开始抛出大量异常java.net.BindException: 无法指定被请求的地址.



从测试的结果可以证明,linux对外随机分配的端口是有限制,理论上单机对外端口数可达65535,但实际对外可建立的连接默认最大只有28232个,在linux下执行以下命令可知:



[root@bogon local]# cat /proc/sys/net/ipv4/ip_local_port_range
32768 60999
也就是在这个区间内的端口可以使用,所以单个IP对外最多只能发送28232个tcp请求.
通过以下命令可以临时调整这个区间的范围,但是系统重启后会还原成默认值.



echo “1024 65535”> /proc/sys/net/ipv4/ip_local_port_range
要想永远生效可以修改/etc/sysctl.conf文件,增加一行:



net.ipv4.ip_local_port_range= 1024 65535
然后再执行:



sysctl -p
这样就永远生效了.现在单ip可以发起64510个tcp连接了.
扩展
单机最多有6w多连接,若想模拟实现百万级别的客户端连接,至少需要十几台机器以上,当然也可以在单机上使用多个虚拟ip来模拟实现.
前面研究的是client能建立的最大tcp连接数,而server能接收的最大tcp连接数又是多少呢?
理论上是无上限的,server通常固定在某个本地端口上监听client的连接请求.不考虑地址重用(unix的SO_REUSEADDR选项)的情况下,即使server端有多个ip,本地监听端口也是独占的,因此server端tcp连接4元组中只有remote ip(也就是client ip)和remote port(客户端port)是可变的,因此最大tcp连接为客户端ip数×客户端port数,对于IPV4,不考虑ip地址分类等因素,最大tcp连接数约为2的32次方(ip数)×2的16次方(port数),也就是server端单个ip单个端口最大tcp连接数约为2的48次方.
但是在实际环境中,受到服务器配置等物理条件的制约,其最大并发tcp连接数远不能达到理论值,不过通过增加内存、修改最大文件描述符个数等参数,单机最大并发TCP连接数是可以达到10万,甚至上百万的.


Category linux