开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第4天,点击查看活动详情
我正在参与掘金会员专属活动-源码共读第一期,点击参与
启动项目
按照大佬说的监听8888端口,用nc进行连接,发送任意文字都会回应响应文字
言归正传,找到在 sun.nio.ch.ServerSocketChannelImpl#bind
backlog到底是个什么东西呢,有什么作用呢?
要想搞清楚 backlog 的含义以及用途,就不得不从 TCP 的三次握手开始说起了。
众所周知,TCP 是面向连接的,它是靠谱的传输协议。UDP 是面向无连接的,是不靠谱的传输协议。
那么面向连接又是什么意思呢?所谓的连接,并不是指通信的两端之间背后通过插一根物理网线而连接起来的,而是客户端和服务端之间通过创建相应的数据结构,来维护双方的状态,并通过这样的数据结构来保持面向连接的特性。所谓的连接就是客户端和服务端之间数据结构状态的协同,如果状态能对应的上,那么就说服务端和客户端之间创建了。
谈到 TCP 协议,大家都知道 TCP 三次握手、四次挥手的特性,那么在三次握手之间到底干了哪些事情呢?和上面提到的数据结构又有什么关系呢?(关于 TCP 的四次挥手本文先不分析)
TCP 是一个靠谱的传输协议,而三次握手就是 TCP 用来保证靠谱的手段。下图是 TCP 三次握手的示意图:
-
首先,客户端和服务端都处于关闭状态,即 CLOSED 状态。
-
当服务端调用操作系统的 bind() 函数和 listen() 函数后,服务端就会处于 LISTENING 状态。
-
当客户端调用 connect() 函数主动向服务端发起连接时,就开始 TCP 的三次握手过程了。
- 第一步:客户端会先向服务端发送一个 SYN 包,告诉服务端我要来连接了,发完这个 SYN 包后,客户端就会处于 SYN_SENT 状态。
- 第二步:当服务端收到客户端发来的 SYN 包后,需要返回给客户端一个 SYN 包以及确认收到 SYN 的 ACK 包,告诉客户端,我知道你要来连接了,发完这个包后,服务端就处于 SYN_RCVD 状态了。
- 第三步:当客户端收到了服务端发来的包后,客户端还需要告诉服务端,我收到了你发来的 SYN 包,即服务端的 SYN 的 ACK 确认包。发完这个包后,客户端就处于 ESTABLISHED 状态了。当服务端收到客户端的 ACK 包后,也会处于 ESTABLISHED 状态。
-
当双方均处于 ESTABLISHED 状态后,就可以进行数据传输了。
半连接队列和全连接队列
前面提到了,当创建 TCP 连接时,双方需要维护一定的数据结构来保证面向连接的特性。这些数据结构当中,就包含了上图中的两个数据结构:半连接队列(syns queue) 和全连接队列(accept queue)。
这两个队列干什么用的呢?当客服端发来一个 SYN 包后,服务端会将这个连接保存到 syns queue 队列
当中,由于此时 TCP 连接还没有完全建立完成,因此称 syns queue 是一个半连接的队列。当服务端收到客户端发来的 ACK 包后,会将这个连接从 syns queue 队列
中移到 accept queue 队列
中,此时连接已经建立完成,所以称之为全连接队列。处于全队列中的连接,可以通过操作系统的 accept() 方法为其创建对应的套接字,这样服务端和客户端就能开始进行数据的传输了。
另外在 TCP 的头部协议中,有一个 32 位长度的包序号,这个包序号是用来保证包的顺序的,解决乱序问题。因为 TCP 是面向连接的嘛,需要保证每个连接和发送的包之间的对应关系,这样才不会混乱,客户端和服务端之间就能区分这个包是哪个连接发送过来的。这个包序号用示意图来表示,就是如下图所示。
第一次,客户端发送 SYN 包时,需要指定一个包序号 x。当服务端收到后,会返回一个 ACK 包,这个 ACK 的序号就是 x+1,表示的是我回应的是包序号是 x 的 SYN 包,同时服务端还会发送一个包序号为 y 的 SYN 包;当客户端收到服务端发送来需要为 y 的 SYN 包后,会回复一个 ACK 包,序号为 y+1,表示的是确认收到了需要为 y 的 SYN 包。
backlog 与 TCP 的三次握手
说了这么多,backlog 和 TCP 到底有什么关系呢?在上面提到了两个队列:半连接队列(syns queue)和全连接队列(accept queue)。这两个队列是用来存放连接的,既然是队列,就会有大小限制,而 backlog
这个参数就是用来限制全连接队列的大小的。操作系统中有一个内核参数:somaxconn
,默认是 128(和操作系统的版本有关系)。可以通过如下命令查看:
[root@localhost ~]# sysctl -a|grep somaxconn net.core.somaxconn = 128
backlog 和 somaxconn 两者中的较小者
:min(backlog,somaxconn),就是全连接队列
的最大容量限制。backlog 的值就是我们在代码中创建套接字时指定的值,如果用户没有指定,Java 中默认是 50
。
与全连接队列类似,半连接队列也要一个容量限制:max(64,/proc/sys/net/ipv4/tcp_max_syn_backlog)
。
tcp_max_syn_backlog
是操作系统的一个内核参数,可以通过如下命令查看具体的值。半连接队列的容量上限是 64 和 tcp_max_syn_backlog 参数值中的较大者
。
cat /proc/sys/net/ipv4/tcp_max_syn_backlog 或者 sysctl -a|grep tcp_max_syn_backlog
半连接队列
在 TCP 的三次握手中,当客户端向服务端发起第一次握手时,即:发送第一个 SYN 包时,如果服务端的半连接队列没有满,那么就会将当前连接添加到半连接队列中。
如果半连接队列已经满了,那么服务端就会直接丢弃这个 SYN 包。此时客户端等一段时间后,一直没有收到服务端发送回来的 ACK 包,所以客户端就会重发 SYN 包,如果服务端半连接队列还是已满状态,所以依旧会将 SYN 包丢弃,那么过一会,客户端还会重发 SYN 包,直到收到服务端发送来的 ACK 包或者重发次数达到上限,客户端才会停止重发。
SYN 包重发次数上限也是一个内核参数:tcp_syn_retries
。可以通过如下命令查看 tcp_syn_retries。如果客户端重发 SYN 包次数达到上限后,就会出现 connection timeout 异常。
全连接队列
当在第三次握手时,服务端接收到客户端发来的 ACK 包后,在正常情况下(全连接队列没有满),会把当前连接从半连接队列中移到全连接队列中。但是对于非正常情况,也就是全连接队列已经满了,服务端此时又会怎么处理呢?
此时又有一个内核参数:tcp_obort_on_onerflow
,服务端会根据这个内核参数来决定怎么处理。可以通过如下命令行来查看 tcp_obort_on_onerflow 参数的值。
当 tcp_abort_on_overflow 的值为 0 时,在全连接队列满了以后,服务端会直接丢弃掉客户端传来的 ACK 包。由于服务端将这个 ACK 包丢弃了,那么服务端会认为自己给客户端发送的 SYN+ACK 包一直没有响应,因此服务端会等待一会以后重新发送 SYN+ACK 包给客户端,这个重试次数也有一个上限,可以通过内核参数 tcp_synack_retries 来修改。通过如下命令可以查看 tcp_synack_retries 参数的值。如果服务端在重试期间,客户端由于设置的超时时间较短,TCP 三次握手没有完成,就会出现 connection timeout 异常。
当 tcp_abort_on_overflow 的值为 1 时,在全连接队列满了以后,服务端会直接向客户端发送一个 RST 通知,即 reset 包,表示废除当前的握手过程。此时客户端收到 RST 通知后就会出现 connection reset by peer 异常。
总结
最后总结一下,本文从 backlog 参数出发,详细分析了 TCP 三次握手过程,以及在握手过程中涉及到的两个队列:半连接队列和全连接队列,当完成第一次握手后,会将连接放入到半连接队列中,当完成第三次握手后,会将连接放入到全连接队列中,当调用操作系统的 accept()函数时,会将连接从全连接队列中取出,从而创建一个客户端套接字,供后面的数据读写。