youyichannel

志于道,据于德,依于仁,游于艺!

0%

TCP 三次挥手?

我们在学习 TCP 挥手的时候,学到的是需要四次来完成 TCP 挥手,但是,在一些情况下,TCP 也可以只有三次挥手。

  1. 为什么 RFC 文档中定义 TCP 挥手过程是 四次?
  2. 什么情况下会出现 三次挥手?

TCP 四次挥手

TCP 四次挥手过程:

  1. Client 主动调用关闭连接的函数,发送 FIN 报文,这个 FIN 报文代表 Client 不会再发送数据,进入 FIN_WAIT_1 状态;
  2. Server 接收到 FIN 报文,内核马上回复一个 ACK 确认报文,此时 Server 进入 CLOSE_WAIT 状态。当接收到 FIN 报文时,TCP 协议栈会为 FIN 包插入一个文件结束符 EOF 到接收缓冲区中,Server 应用程序可以通过 read 调用来感知这个 FIN 包,这个 EOF 会被放在已排队等待的其他已接收的数据之后,所以需要继续 read 接收缓冲区中已经接收的数据;
  3. 当 Server 在 read 数据读到 EOF 时,read() 返回0,此时 Server 应用程序如果有数据要发送的话,就发完数据后才调用关闭连接的函数;如果 Server 应用程序没有数据要发送到的话,可以直接调用关闭连接的函数,此时 Server 就会发一个 FIN 包,这个 FIN 报文代表 Server 不会再发送数据了,之后处于 LAST_ACK 状态;
  4. Client 接收到 Server 的 FIN 报文,并发送 ACK 确认报文给 Server,此时 Client 进入 TIME_WAIT 状态;
  5. Server 接收到 ACK 确认报文之后,进入最后的 CLOSE 状态;
  6. 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 关闭连接的函数其实有两种:

  1. close()函数:同时关闭 socket 的发送方向和读取方法,即 socket 不再具有发送和接收数据的能力。若有多个进程(/ 线程)共享同一个 socket,如果有一个进程调用了 close() 函数只是让 socket 的引用计数减一,并不会导致 socket 不可用,同时也不会发出 FIN 报文,其他进程还是可以正常读写该 socket,直到引用计数变为0,才会发出 FIN 报文。
  2. 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 中定义了延迟等待时间

#define TCP_DELACK_MAX	((unsigned)(HZ/5))	/* maximal time to delay before sending an ACK */
#define TCP_DELACK_MIN ((unsigned)(HZ/25)) /* minimal time to delay before sending an ACK */

关键就需要 HZ 这个数值大小,HZ 表示内核时钟的频率(即每秒中断次数),HZ 是跟系统的时钟频率有关,每个操作系统都不一样。Linux 系统下,可以通过如下方式来获取这个值:

grep CONFIG_HZ /boot/config-$(uname -r)
# or
cat /proc/sys/kernel/hz
# or
sysctl kernel.hz

假设这个值是 1000,那么就可以算出:

  • 最大延迟确认时间是 200 ms = 1000/5
  • 最短延迟确认时间是 40 ms = 1000/25

怎么关闭 TCP 延迟确认机制?

要关闭 TCP 延迟确认机制,可以在 Socket 设置里启用 TCP_QUICKACK。

// 1 表示开启 TCP_QUICKACK,即关闭 TCP 延迟确认机制
int value = 1;
setsockopt(socketfd, IPPROTO_TCP, TCP_QUICKACK, (char*)& value, sizeof(int));

总结

当被动关闭方(比如上述的 Server)在 TCP 挥手过程中,「没有数据要发送」并且「开启了 TCP 延迟确认机制(默认开启)」,那么第二次和第三次挥手就会合并传输,这样就出现了三次挥手。

所以,**出现三次挥手的现象,是因为 TCP 延迟确认机制 导致的。