在握手阶段存在两个队列:syns queue(半连接队列);accept queue(全连接队列)。
流程简述如下:
1. 客户端发送SYN付服务端进行第一次报文握手,此时服务端将此请求信息放在半连接队列中并回复SYN+ACK给客户端。
此处就是SYN Flood(后续关注,此篇记录)攻击的点,攻击方要做的就是不停的建立连接,但是确不给出ACK确认,导致半连接队列满了,其他请求无法进入。
2. 客户端收到SYN+ACK,随机发出ACK确认给服务端;
全队列未满:从半连接队列拿出此消息放入全队列中。
全队列已满:处理方式和tcp_abort_on_overflow(cat /proc/sys/net/ipv4/tcp_abort_on_overflow)有关:
tcp_abort_on_overflow=0;表示丢弃该ACK;
tcp_abort_on_overflow=1;表示发送一个RST给客户端,直接废弃掉这个握手过程。
3. 服务端accept处理此请求,从全队列中将此请求信息拿出。
backlog:
backlog表明它是已连接但是还未处理的队列的大小。这个队列如果满的话,会发送一个ECONNREFUSED错误信息给客户端。就是常见的“Connection Refused”。
简单测试:使用Java内的socket测试:
客户端:
privatestaticSocket[] clients =newSocket[30];
publicstaticvoidmain(String[] args)throwsException{
for(inti =0; i <10; i++) {
clients[i] =newSocket("127.0.0.1",8888);
System.out.println("Client:"+ i);
}
}
服务端1:
publicstaticvoidmain(String[] args)throwsException{
ServerSocket server =newServerSocket(8888,5);
while(true) {
// server.accept();
}
}
输出:
Client:0
Client:1
Client:2
Client:3
Client:4
Exception in thread"main"java.net.ConnectException: Connection refused: connect
at java.net.DualStackPlainSocketImpl.connect0(Native Method)
at java.net.DualStackPlainSocketImpl.socketConnect(DualStackPlainSocketImpl.java:79)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:172)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
at java.net.Socket.connect(Socket.java:589)
at java.net.Socket.connect(Socket.java:538)
at java.net.Socket.(Socket.java:434)
at java.net.Socket.(Socket.java:211)
at Test1.main(Test1.java:15)
源码简析:从下面的源码内可以看见backlog提供了一个类似容量的限制,小于1或者不传默认值是50。因此在只提供了5个backlog大小的测试中,因为前5个占据了全连接队列没有被处理,第六个进来时,没有位置,所以拒绝了(从此处可以看见参数tcp_abort_on_overflow=1,0的话会直接丢弃)
publicServerSocket(intport,intbacklog, InetAddress bindAddr)throwsIOException{
setImpl();
if(port <0|| port >0xFFFF)
thrownewIllegalArgumentException(
"Port value out of range: "+ port);
if(backlog <1)
backlog =50;
try{
bind(newInetSocketAddress(bindAddr, port), backlog);
}catch(SecurityException e) {
close();
throwe;
}catch(IOException e) {
close();
throwe;
}
}
服务端2:(注释打开)
publicstaticvoidmain(String[] args)throwsException{
ServerSocket server =newServerSocket(8888,5);
while(true) {
server.accept();
}
}
输出:
Client:0
Client:1
Client:2
Client:3
Client:4
Client:5
Client:6
Client:7
Client:8
Client:9
此时由于accept处理的存在,使得全连接队列有位置,所以能够处理完所有的请求。
全连接队列的大小:
Java的Socket的默认backlog大小是50;(看上面源码分析)
Tomcat的默认backlog大小是100;(看8080端口)
可以自由配置
connectionTimeout="20000"
redirectPort="8443"
maxThreads="800"acceptCount="1000"/>
AliTomcat默认backlog值为200;(参考别的连接)
Nginx的默认backlog值是511;
全队列的大小和backlog有关,但是未必是backlog的值,是由backlog和somaxconn(系统参数)两个值中的较小值决定。
cat /proc/sys/net/core/somaxconn
此处我贴一下linux下的大小是128,如果我们传进来的值是50,那么全队列大小就是50。
半连接队列的大小:
半连接的大小取决于64和tcp_max_syn_backlog的较大值。
cat /proc/sys/net/ipv4/tcp_max_syn_backlog
本机的值是256。
通过ss查看Socket的状态:
基于上面的测试例子中的Java代码,这次backlo的值设置为10,作为明显区分。
Send-Q表示的就是当前端口的全连接队列大小,Recv-Q表示的是全连接队列使用的多少。
针对8888端口:客户端只循环了5次,所以Recv-Q的使用量只有5个,而全连接队列的大小是10。
先记录这么多,因为没有什么实际的问题作为继续研究的样例,所以没什么再深入的学习了。
队满时会发生什么
Q:为什么 有的应用服务器进程,会单独使用一个线程去调用accept来建立服务端和客户端的连接,比如tomcat;而有的则是一个线程做所有的事,即获取连接后还会做I/O等其他操作?
A:首先SYN队列和ACCEPT队列都不是无限大小的,上面已经提到。既然队列长度是有限的,那就有满的时候。比如当上图中步骤1的执行速度大于第2步执行的速度,SYN队列就会不断增大,直到队列满;第2步的执行速度远大于第3步的执行速度,ACCEPT队列同样会满。第1、2步不是程序可控的,第3步是程序的行为。
先看第一种情况:SYN队列满。如果SYN队列满,则会直接丢弃连接请求。
比如syn floods 攻击就是针对半连接队列的,攻击方不停地建连接,但是建连接的时候只做第一步,第二步中攻击方收到server的syn+ack后故意扔掉什么也不做,server需要一个超时时间把这个连接断开,否则大量这样的连接导致server上这个队列满其它正常请求无法进来。
第二种情况:ACCEPT队列满。这种情况貌似比较复杂,我查了学长的博客,发现学长讨论过这个问题。accept队列满的讨论
如果ACCEPT队列满了,server 通过 /proc/sys/net/ipv4/tcp_abort_on_overflow 来决定如何返回:
tcp_abort_on_overflow 为 0,不会把连接从SYN队列中移除,server过一段时间再次发送syn+ack给client(也就是重新走握手的第二步),这样来回重发几次,次数由 /proc/sys/net/ipv4/tcp_synack_retries(centos默认为 5 ) 指定,如果三次握手第三步的时候 ACCEPT 队列一直是满,那么server扔掉client 发过来的 ACK(在server端认为连接还没建立起来);
tcp_abort_on_overflow 为 1 表示第三步的时候如果 ACCEPT 队列满了,server发送一个RST包给client,表示废掉这个握手过程和这个连接(本来在server端这个连接就还没建立起来),客户端会出现 connection reset by peer 的异常。
PS:还有一个事实是,一般 overflowed 表示全连接队列溢出次数,socket ignored 表示半连接队列溢出次数,这两个是同步增加的,overflow的时候一定会socket ignored++。
所以对应用服务器进程来说,如果ACCEPT队列中有已经建立好的TCP连接,却没有及时的把它取出来,这样一旦导致两个队列满后就会使客户端不能再建立新的连接,引发严重问题。所以,比如tomcat等服务器会用独立的线程只做accept获取连接这一件事,以防止不能及时的去accept获取连接。
网友评论