我们在学习 TCP 挥手的时候,学到的是需要四次来完成 TCP 挥手,但是,在一些情况下,TCP 也可以只有三次挥手。
- 为什么 RFC 文档中定义 TCP 挥手过程是 四次?
- 什么情况下会出现 三次挥手?
TCP 四次挥手
TCP 四次挥手过程:
- Client 主动调用关闭连接的函数,发送 FIN 报文,这个 FIN 报文代表
Client 不会再发送数据,进入
FIN_WAIT_1
状态; - Server 接收到 FIN 报文,内核马上回复一个 ACK 确认报文,此时 Server
进入
CLOSE_WAIT
状态。当接收到 FIN 报文时,TCP 协议栈会为 FIN 包插入一个文件结束符 EOF 到接收缓冲区中,Server 应用程序可以通过 read 调用来感知这个 FIN 包,这个 EOF 会被放在已排队等待的其他已接收的数据之后,所以需要继续 read 接收缓冲区中已经接收的数据; - 当 Server 在 read 数据读到 EOF 时,
read()
返回0,此时 Server 应用程序如果有数据要发送的话,就发完数据后才调用关闭连接的函数;如果 Server 应用程序没有数据要发送到的话,可以直接调用关闭连接的函数,此时 Server 就会发一个 FIN 包,这个 FIN 报文代表 Server 不会再发送数据了,之后处于LAST_ACK
状态; - Client 接收到 Server 的 FIN 报文,并发送 ACK 确认报文给 Server,此时
Client 进入
TIME_WAIT
状态; - Server 接收到 ACK 确认报文之后,进入最后的
CLOSE
状态; - Client 经过 2 MSL 时间之后,也进入
CLOSE
状态。
由于每一个方向都需要一个 FIN 报文和一个 ACK 报文,因此被称为四次挥手。
为什么 TCP 挥手需要四次?
Server 在接收到 Client 的 FIN 报文时,内核会马上回一个 ACK 确认报文,但是 Server 应用程序可能还有数据要发送,所以并不能马上发送 FIN 报文,而是将发送的 FIN 报文的控制权交给 Server 应用程序,如果 Server 应用程序:
- 有数据要发送,在发送完成数据后,才调用关闭连接的函数
- 没有数据要发送,可以直接调用关闭连接的函数
由此可知,是否要发送第三次挥手报文的控制权不在内核,而是在被动关闭方(例如 Server)的应用程序,因为应用程序可能还有数据要发送,由应用程序决定什么时候调用关闭连接的函数,当调用了关闭连接的函数,内核就会发送 FIN 报文了,所以 Server 的 ACK 和 FIN 报文一般都会分开发送。
FIN 报文一定要 被动调用方 调用关闭连接的函数之后才会发送吗?
不一定。如果进程退出了,不管是不是正常退出,还是异常退出(比如进程崩溃),内核都会发送 FIN 报文,与对方完成四次挥手。
优雅关闭 VS 粗暴关闭
TCP 关闭连接的函数其实有两种:
close()
函数:同时关闭 socket 的发送方向和读取方法,即 socket 不再具有发送和接收数据的能力。若有多个进程(/ 线程)共享同一个 socket,如果有一个进程调用了close()
函数只是让 socket 的引用计数减一,并不会导致 socket 不可用,同时也不会发出 FIN 报文,其他进程还是可以正常读写该 socket,直到引用计数变为0,才会发出 FIN 报文。shutdown()
函数:可以指定 socket 只关闭发送方向而不关闭读取方向,即 socket 不再有发送数据的能力,但是还具有接收数据的能力。若有多个进程(/ 线程)共享同一个 socket,shutdown()
函数不会管引用计数,将直接使得该 socket 不可用,然后发出 FIN 报文,如果有别的进程(/ 线程)企图使用该 socket,将会受到影响。
如果 Client 使用 close()
函数来关闭连接,那么在 TCP
四次挥手中,如果收到了 Server 发送的数据,由于 Client
已经不再具有发送和接收数据的能力,所以 Client 的内核会回 RST 报文给
Server,然后内核释放连接,此时就不会经历完整的 TCP 四次挥手。所以称调用
close()
函数关闭是粗暴关闭。
当 Server 接收到 RST 后,内核就会释放连接。当 Server 应用程序再次发起读操作或者写操作时,就能够感知到连接已经被释放了:
- 如果是读操作,则会返回 RST 的错误,也就是 Connection reset by peer;
- 如果是写操作,那么程序会产生 SIGPIPE 信号,应用层代码可以捕获并处理信号,如果不处理,则默认情况下进程会终止,异常退出。
如果 Client 使用
shutdown()
函数来关闭连接,因为该函数可以指定只关闭发送方向,而不关闭读取方向,所以在
TCP 四次挥手的过程中,如果收到了 Server
发送的数据,客户端也是可以正常读取到该数据的,然后就会经历完整的 TCP
四次挥手。所以称调用 shutdown()
函数关闭是优雅关闭。
📢注意:shutdown()
函数也可以指定「只关闭读取方向,而不关闭发送方向」,但是这个时候内核也是不会发送
FIN 报文的,因为发送 FIN 报文意味着我方不再发送任何数据,而
shutdown()
如果指定「不关闭发送方向」,就意味着 socket
还有发送数据的能力,所以内核就不会发送 FIN 报文。
什么情况下会出现三次挥手?
结论:当被动关闭方(比如上述的 Server)在 TCP 挥手过程中,「没有数据要发送」并且「开启了 TCP 延迟确认机制」,那么第二次和第三次挥手就会合并传输,这样就出现了三次挥手。
因为 TCP 延迟确认机制是默认开启的,所以在实际抓包的时候,看见三次挥手的次数实际上是比四次挥手还多的。
TCP 延迟确认机制
当没有携带数据的 ACK 确认报文,它的网络效率是很低的,因为它也有40字节的 IP 头和 TCP 头,但却没有携带数据报文。为了解决 ACK 报文传输效率低的问题,就衍生了TCP 延迟确认。
TCP 延迟确认策略:
- 当有响应数据要发送时,ACK 会随着相应数据一起立刻发送给对方
- 当没有响应数据要发送时,ACK 将会延迟一段时间,以等待是否有响应时间可以一起发送
- 如果在延迟等待发送 ACK 期间,对方的第二个数据报文又到达了,此时会立刻发送 ACK
在 Linux 内核源码 include/net/tcp.h
中定义了延迟等待时间
关键就需要 HZ 这个数值大小,HZ 表示内核时钟的频率(即每秒中断次数),HZ 是跟系统的时钟频率有关,每个操作系统都不一样。Linux 系统下,可以通过如下方式来获取这个值:
grep CONFIG_HZ /boot/config-$(uname -r) |
假设这个值是 1000,那么就可以算出:
- 最大延迟确认时间是 200 ms = 1000/5
- 最短延迟确认时间是 40 ms = 1000/25
怎么关闭 TCP 延迟确认机制?
要关闭 TCP 延迟确认机制,可以在 Socket 设置里启用 TCP_QUICKACK。
// 1 表示开启 TCP_QUICKACK,即关闭 TCP 延迟确认机制 |
总结
当被动关闭方(比如上述的 Server)在 TCP 挥手过程中,「没有数据要发送」并且「开启了 TCP 延迟确认机制(默认开启)」,那么第二次和第三次挥手就会合并传输,这样就出现了三次挥手。
所以,**出现三次挥手的现象,是因为 TCP 延迟确认机制 导致的。