网络:TCP四次挥手

TCP连接终止(四次挥手)

TCP建立一个连接需要3个分节,终止一个连接需要4个分节。

tcp的连接的断开比建立要复杂一些的原因,本质上是因为资源的申请(初始化)本身比资源的释放简单,以C++为例,构造函数初始化对象很简单,而析构函数则要考虑所有资源安全有效的释放,tcp断开时序除了断开这一重要动作外,另外重要的潜台词是“我要断开连接了,你赶紧把收尾工作做了”

  • 客户端主动调用关闭连接的函数,于是就会发送 FIN 报文,这个 FIN 报文代表客户端不会再发送数据了,进入 FIN_WAIT_1 状态;
  • 服务端收到了 FIN 报文,然后马上回复一个 ACK 确认报文,此时服务端进入 CLOSE_WAIT 状态。在收到 FIN 报文的时候,TCP 协议栈会为 FIN 包插入一个文件结束符 EOF 到接收缓冲区中,服务端应用程序可以通过 read 调用来感知这个 FIN 包,这个 EOF 会被放在已排队等候的其他已接收的数据之后,所以必须要得继续 read 接收缓冲区已接收的数据;
  • 接着,当服务端在 read 数据的时候,最后自然就会读到 EOF,接着 read() 就会返回 0,这时服务端应用程序如果有数据要发送的话,就发完数据后才调用关闭连接的函数,如果服务端应用程序没有数据要发送的话,可以直接调用关闭连接的函数,这时服务端就会发一个 FIN 包,这个 FIN 报文代表服务端不会再发送数据了,之后处于 LAST_ACK 状态;
  • 主动方收到被动方的 ACK 应答报文后,之后进入 FIN_WAIT_2 状态。
  • 客户端接收到服务端的 FIN 包,并发送 ACK 确认包给服务端,此时客户端将进入 TIME_WAIT 状态;
  • 服务端收到 ACK 确认包后,就进入了最后的 CLOSE 状态;
  • 客户端经过 2MSL 时间之后,也进入 CLOSE 状态;

可以看到,每个方向都需要一个 FIN 和一个 ACK,因此通常被称为四次挥手。

注意,主动关闭连接的,才有 TIME_WAIT 状态。

详解

在这里插入图片描述

  • 第一次握手:某个应用程序首先调用close,我们称该端执行主动关闭(active close), 该TCP socket就会发送一段TCP报文:

    • 发送一个FIN分节,表示“请求释放连接”
    • 序号Seq=U
    • 随后该TCP连接就会进入FIN-WAIT-1阶段,即半关闭阶段,并且停止向另一端发送数据(这里的不发送指的是不发送应用数据,而且还是发送ACK等报文)。但是当前应用还是能够接受到另一端发送过来的数据
  • 第二次握手:另一端接受到FIN之后

    • 这个FIN由TCP协议栈处理,我们知道,TCP协议栈为FIN包插入一个文件描述符EOF到接收缓冲区中,应用程序可以通过read调用来感知这个FIN包。一定要注意,这个EOF会被放在已排队等候的其他已接收的数据之后,这就意味着接收端应用程序需要处理这种异常情况,因为EOF表示在该连接上再无额外数据到达
    • 它此时就会结束established阶段,进入CLOSE_WAIT(半关闭状态-- 表示服务器不再接受应用数据,但是本端还是可以向另一端发送数据的)并返回一段TCP报文:
    • 标记位为ACK,表示“接收到客户端发送的释放连接的请求”;
    • 序号为Seq=V;
    • 确认号为Ack=U+1,表示是在收到客户端报文的基础上,将其序号Seq值加1作为本段报文确认号Ack的值;
    • 随后服务器端开始准备释放服务器端到客户端方向上的连接。
    • 主动方收到从被动方发出的TCP报文之后,确认了被动方收到了主动方发出的释放连接请求,随后主动方结束FIN-WAIT-1阶段,进入FIN-WAIT-2阶段
  • 第三次握手:

    • 被动方自从发出ACK确认报文之后,开始准备释放被动方到主动方方向上的连接。
    • 被动方读到EOF之后,其应用程序也调用close关闭它的套接字,这导致它的TCP也发送一个FIN包。这样,被动关闭方将进入LAST_ACK状态。
  • 第四次握手:
    • 主动关闭方接收到对方的FIN包,并确认这个FIN包。主动关闭方进入TIME_WAIT状态,而接收到ACK的被动关闭方则进入CLOSE状态。过了2MSL时间之后,主动关闭方也进入CLOSE状态。

当然,这中间使用shutdown,执行一端到另一端的半关闭也是可以的。

当套接字被关闭时,TCP为其索再端发送一个FIN包。在大多数情况下,这是由应用进程调用close而发生的,值得注意的是,一个进程无论是正常退出(exit或者main函数返回),还是非正常退出(比如,收到SIGKILL信号关闭(kill -9)),所有该进程打开的描述符都会被系统关闭,这也导致TCP描述符对应的连接上发出的一个FIN包

无论是客户端还是服务器,任何一端都可以发起主动关闭。大多数真实情况是客户端执行主动关闭,但是HTTP/1.0 却是由服务器发起主动关闭的。

在这里插入图片描述

第一次挥手丢失了,会怎么样?

主动关闭方的FIN发出去了,此时主动方处于FIN_WAIT_1 状态,客户端处于established状态。

  • 如果这个FIN丢失了,主动方收不到ACK,那么会触发超时重传机制,重传 FIN 报文,重发次数由 tcp_orphan_retries 参数控制。
  • 当重传次数超过 tcp_orphan_retries 后,就不在发送FIN报文,等待一段时间后(时间为上一次超时时间的 2 倍),如果还没有收到第二次挥手,那么就直接进入CLOSE状态

第二次挥手丢失了,会发生什么?

被动方收到第一个FIN之后,会回复一个ACK,并进入CLOSE_WAIT状态。如果这个ACK丢失了,会怎么样呢?

  • ACK报文是不会重传的,如果丢失了,主动方会认为自己的第一个FIN没有发出去,主动方会触发超时重传,重传FIN报文,直到收到第二次挥手,或者达到最大重传次数

第三次挥手丢失了,会发生什么?

场景

此时主动方发送了一个FIN(第一次挥手),并且收到了被动方回复的ACK(第二次挥手),此时主动方将进入FIN_WAIT2状态,并等待被动方的FIN(第三次挥手)。

而被动方收到第一个FIN(第一次挥手)之后,内核会自动回复一个ACK,同时进入CLOSE_WAIT状态。

顾名思义,它表示等待应用程序调用close函数关闭连接。

此时,内核是没有权利替代进程关闭连接,必须由进程主动调用close函数来触发被动方发送FIN报文。

被动方调用close之后,内核就会发送一个FIN(第三次握手),并进入LAST_ACK状态(表示等待最后一个ACK)。如果这个FIN(第三次握手)丢失了,会怎么样呢?

分析

  • 第三次FIN(第三次握手)丢失了,被动方将处于于LAST_ACK状态,被动方会收不到最后一个ACK,出于就会触发超时重传,重发 FIN 报文,重发次数仍然由 tcp_orphan_retries 参数控制,这与客户端重发 FIN 报文的重传次数控制方式是一样的。
  • 主动方此时已经接收到了ACK(第二次握手),但是没有收到FIN(第三次握手),此时它处于FIN_WAIT_2状态
    • 对于主动方close函数关闭的连接,由于无法再发送和接收数据,FIN_WAIT_2不会维持太久,默认是60s(由tcp_fin_timeour控制),如果60s后还是没有收到FIN(第三次握手),主动方就会直接进入CLOSE状态
    • 对于主动方shutdown函数关闭的连接,此时主动关闭方还是可以接收数据的,只是不再可以发送数据了,它会一直处于FIN_WAIT2 状态,死等
  • 关于tcp_orphan_retries

tcp_orphan_retries内核参数的默认值是 0,如果你要自定义 fin 报文的重传次数可以看:
在这里插入图片描述
你可能会好奇,这 0 表示几次?实际上当为 0 时,特指 8 次,从下面的内核源码可知:

在这里插入图片描述

第四次挥手丢失了,会发生什么?

主动方收到FIN(第三次握手)之后,将会发送一个ACK(第四次握手),然后进入TIME_WAIT状态,如果这个ACK(第四次握手)丢失了,会怎么样呢?

  • 此时,被动方处于LAST_ACK 状态。如果它没有收到最后一个ACK报文,它会认为自己的FIN(第三次握手)丢失了,于是就会触发超时重传FIN报文,直到达到最大重传次数(由tcp_orphan_retries 参数控制),或者接收到最后一个ACK为止
  • 此时,主动方出于TIME_WAIT状态,它会开启一个时长为2MSL的定时器,如果途中再次收主动方就会断开连接(注意,ACK报文是不会重传的)
    • 如果在主动方已经断开连接之后,又收到了FIN(第三次握手)会怎么样呢?此时主动方会直接发送一个RST,表示我已经在这里登陆这么长的时间了,已经仁至义尽了,之后发送的我就都不认了,此时被动方就知道主动方已经跑路了

太多的close_wait,怎么解决

CLOSE_WAIT,被动关闭方的状态

  • 被动关闭方在ACK了第一个FIN之后,没有及时把自己的FIN发送出去,导致自己一直出于COLSE_WAIT状态,没有进入LAST_ACK状态

问题:

  • 太多的CLOSE_WAIT可能导致资源耗尽

原因:

  • 某种情况下客户端关闭了连接,但是我方忙于读写,没有关闭连接

解决:

  • 服务器出现大量的close_wait状态,这个锅肯定是服务器端的coder背!
  • 太多的CLOSE_WAIT:基本上是由于没有及时调用close造成的,因此应该及时调用close。
  • 所以,应该有如下手段
    • .如果读写socket出错,那么就要调用close函数
    • 可以使用IP多路复用检测socket的活动状态,一旦异常,那么即使close
    • 定期向连接发送询问数据,检查收到的回复数据包(Heart-Beat线程发送指定格式的心跳数据包)

为什么 TIME_WAIT 等待的时间是 2MSL?

  • MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的,而 IP 头中有一个 TTL 字段,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。
  • MSL 与 TTL 的区别: MSL 的单位是时间,而 TTL 是经过路由跳数。所以 MSL 应该要大于等于 TTL 消耗为 0 的时间,以确保报文已被自然消亡
  • TTL 的值一般是 64,Linux 将 MSL 设置为 30 秒,意味着 Linux 认为数据报文经过 64 个路由器的时间不会超过 30 秒,如果超过了,就认为报文已经消失在网络中了。
  • TIME_WAIT 等待 2 倍的 MSL,比较合理的解释是: 网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。
  • 可以看到 2MSL时长 这其实是相当于至少允许报文丢失一次。比如,若 ACK 在一个 MSL 内丢失,这样被动方重发的 FIN 会在第 2 个 MSL 内到达,TIME_WAIT 状态的连接可以应对。
  • 2MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时。
  • 在 Linux 系统里 2MSL 默认是 60 秒,那么一个 MSL 也就是 30 秒。Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒。

为什么需要 TIME_WAIT 状态?

主动发起关闭连接的一方,才会有 TIME-WAIT 状态。

TIME_WAIT的引入是为了让TCP报文得以自然消失,同时为了被动方能够正常关闭

说明

(1) 防止历史连接中的数据,为了让旧连接的重复分节在网络中自然消失

  • 主动方直接跑路会有一个问题,主动方的端口就直接空出来了,但是被动方不知道,被动方原来发给很多包可能还在路上,如果主动方的端口被一个新的应用占据了,这个新的应用会收到上个连接中被动方发过来的包,虽然序列号是重新生成的,但是这里要上一个双保险,防止产生混乱,因而也需要等待足够长的时间,等到原来被动方发送的所有的包都死翘翘,再空出端口来。
    • 我们知道,在网络中,经常会发生报文经过一段时间才能到达目的地的情况,产生的原因是多种多样的,比如路由器重启、链路突然出现故障等。如果迷失报文到达时,发现TCP连接四元组(源 IP,源端口,目的 IP,目的端口)所代表的连接不复存在,那么很简单,这个报文自然丢弃
    • 我们考虑这样一个场景,在原连接中断后,又重新创建了一个原连接的“化身”,,说是化身其实是因为这个连接和原先的连接四元组完全相同,如果迷失报文经过一段时间也到达,那么这个报文会被误认为是连接“化身”的一个 TCP 分节,这样就会对 TCP 通信产生影响。
    • 所以,TCP 就设计出了这么一个机制,经过 2MSL 这个时间,足以让两个方向上的分组都被丢弃,使得原来连接的分组在网络中都自然消失,再出现的分组一定都是新化身所产生的。

(2)TIME-WAIT第二个作用是等待足够的时间以确保最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭。

  • TCP在设计的时候,做了充分的容错性设计。比如,TCP假设报文会出错,需要重传。在这里,如果图中主机1的ACK报文没有传输成功,那么主机2就会重新发送FIN报文
  • 如果主机1没有维护TIME_WAIT状态,而直接进入CLOSE状态,它就失去了当前状态的上下文,只能回复一个RST操作,从而导致被动关闭方出现错误
  • 现在主机1知道自己出于TIME_WAIT的状态,就可以在接收到FIN报文之后,重新发出一个ACK报文,使得主机2可以进入正常的CLOSE状态。

注意,2MLS的时间是从主机1收到FIN后发送ACK开始计时的;如果TIME_WAIT时间内,因为主机1的ACK没有传输到主机2,主机1又开始接收到了主机2重发的FIN报文,那么2MSL时间将重新计时。道理很简单,因为2MSL的时间,是为了让旧连接的所有报文能自然消亡,现在主机1重新发送了ACK报文,自然需要重新计时,以防止这个ACK报文对新可能的连接化身造成干扰

主动方可以直接起一个进程替换原来的端口,并不需要等待2MLS时间----这就是reuse

为什么要解决TIME_WAIT呢?

过多的TIME_WAIT的主要危害有两种:

  • 第一是占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等;
  • 第二是占用端口资源,一个TCP连接至少消耗一个本地端口,要知道,端口资源也是有限的,一般可以开启的端口为 32768~61000 ,也可以通过net.ipv4.ip_local_port_range指定,如果TIME_WAIT状态过多,会导致无法创建新连接

客户端和服务端 TIME_WAIT 过多,造成的影响是不同的。

(1)如果客户端(主动发起关闭连接方)的 TIME_WAIT 状态过多,占满了所有的端口资源,那么:

  • 无法对[目的IP+目的PORT]都一样的服务器发起连接了,但是被使用的端口,还是可以继续对另一个服务器发起连接的
  • 这是因为内核在定位一个连接的时候,是通过四元组(源IP、源端口、目的IP、目的端口)信息来定位的,并不会因为客户端的端口一样,而导致连接冲突。

(2)如果服务端(主动发起关闭连接方)的 TIME_WAIT 状态过多,并不会导致端口资源受限,因为服务端只监听一个端口,而且由于一个四元组唯一确定一个TCP连接,因此理论上服务器可以建立很多连接,但是TCP连接过多,会占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等

TIME_WAIT太多会出现什么情况?

  • 现象:服务的可用性时好时坏,一段时间可以对外提供服务,一段时间又不可以。
  • 查询:通过netstat命令查看,发生主机上有成千上万处于TIME-WAIT状态的连接
  • 分析:当前这个服务需要荣光发起TCP连接对外提供服务。每个连接会占用一个本地接口,当在高并发的情况下,TIME_WAIT状态的连接过多,多到把本机可用的端口耗尽,应用服务对外表现的症状,就是不能正常工作了。当过了一段时间之后,处于TIME_WAIT的连接被系统回收并关闭后,释放出本地端口可供使用,应用服务对外表现为可以正常工作。这样周而复始,就会出现一会儿不可以,过一两分钟又可以正常工作的现象。

太多的 TIME_WAIT,如何解决?

TIME_WAIT:主动关闭方的状态:

  • 它在收到第二个FIN时并发出最后一个ACK之后就会进入TIME_WAIT状态2MLS
  • 如果在这2MSL内又收到了第二个FIN,那么计时器就会重置并重新发送一个ACK(一直发一直发,不可能)
  • 如果超过了2MSL内都没有收到第二个FIN,那么主动方就认为本次连接已经关闭,进入CLOSE状态

在实际项目中,对于TIME_WAIT:

  • 对于客户端:HTTP请求的头部,要求长连接
  • 对于服务端:设置端口重用和缩小TIME_WAIT的时间
    • 允许 time_wait 状态的 socket 被重用(给socket添加一个SO_REUSEADDR选项)
    • 缩减 time_wait 时间,设置为 1 MSL(即,2 mins)

具体解释

这里给出优化 TIME-WAIT 的几个方式,都是有利有弊:

  • 打开 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 选项;
  • net.ipv4.tcp_max_tw_buckets
  • 程序中使用 SO_LINGER ,应用强制使用 RST 关闭。

(1)方式一:net.ipv4.tcp_tw_reuse 和 tcp_timestamps

  • 如下的 Linux 内核参数开启后,则可以复用处于TIME_WAIT的socket为新的连接所用(推荐使用)
  • Linux 系统对于net.ipv4.tcp_tw_reuse的解释如下:
Allow to reuse TIME-WAIT sockets for new connections when it is safe from protocol viewpoint. Default value is 0.It should not be changed without advice/request of technical experts.
  • 这段话的大意是从协议角度理解如果是安全可控的,可以复用处于TIME_WAIT的套接字为新的连接所用
  • 那么什么是协议角度理解的安全可控呢?主要有两点:
    • 只适用于连接发起方(C/S 模型中的客户端);
    • 对应的TIME_WAIT状态的连接创建时间超过1s才可以被复用
  • 使用这个选项,还有一个前提,需要打开对TCP时间戳的支持,即net.ipv4.tcp_timestamps=1(默认即为 1)。
  • 注意,tcp_tw_reuse功能只能由客户端(发起连接方),因为开启了该功能,在调用connect()函数时,内核会随机找一个time_wait状态超过1s的连接给新的连接复用
net.ipv4.tcp_tw_reuse = 1
  • 使用这个选项,还有一个前提,需要打开对 TCP 时间戳的支持,即
net.ipv4.tcp_timestamps=1(默认即为 1
  • 要知道,TCP协议也在与时俱进,RFC1323中实现了TCP扩展规范,以便保证TCP的高可用,并引入了新的TCP选项,这个时间戳的字段是在 TCP 头部的「选项」里,它由一共 8 个字节表示时间戳,其中第一个 4 字节字段用来保存发送该数据包的时间,第二个 4 字节字段用来保存最近一次接收对方发送到达数据的时间。
  • 由于引入了时间戳,我们在前面提到的 2MSL 问题就不复存在了,因为重复的数据包会因为时间戳过期被自然丢弃。

(2)方式二:net.ipv4.tcp_max_tw_buckets

  • 一个暴力的方法是通过sysctl命令,将系统值调小。
  • 这个值默认为18000,当系统中处于TIME_WAIT的连接一旦超过这个值时,系统就会将所有的TIME_WAIT连接状态重置,并且只打印出警告信息
  • 这个方法过于暴力,而且治标不治本,带来的问题比解决的问题多,不推荐使用

(3)方式三:程序中使用 SO_LINGER(此种方法不可,推荐方法四)

  • linger可以翻译为停留。我们可以通过1设置套接字选项,来设置调用close或者shutdown关闭连接时的行为
int setsockopt(int sockfd, int level, int optname, const void *optval,
        socklen_t optlen);

struct linger {
 int  l_onoff;    /* 0=off, nonzero=on */
 int  l_linger;    /* linger time, POSIX specifies units as seconds */
}
  • 设置linger参数有几种可能:
    • 如果l_onoff为0,那么关闭本选项。l_linger的值被忽略,这对应了默认行为,close或者shutdown立即返回。如果在套接字发送缓存区中有数据残留,系统会试着把这些数据发送出去
    • 如果l_onoff为非 0, 且l_linger值为 0,那么调用 close 后,会立该发送一个 RST 标志给对端,该 TCP 连接将跳过四次挥手,也就跳过了 TIME_WAIT 状态,直接关闭。这种关闭的方式称为“强行关闭”。 在这种情况下,排队数据不会被发送,被动关闭方也不知道对端已经彻底断开。只有当被动关闭方正阻塞在recv()调用上时,接受到 RST 时,会立刻得到一个“connet reset by peer”的异常。
    • 如果l_onoff为非 0, 且l_linger的值也非 0,那么调用 close 后,调用 close 的线程就将阻塞,直到数据被发送出去,或者设置的l_linger计时时间到。
  • 第二种可能为跨越 TIME_WAIT 状态提供了一个可能,不过是一个非常危险的行为,不值得提倡。
struct linger so_linger;
so_linger.l_onoff = 1;
so_linger.l_linger = 0;
setsockopt(s, SOL_SOCKET, SO_LINGER, &so_linger,sizeof(so_linger));

前面介绍的方法都是试图越过 TIME_WAIT状态的,这样其实不太好。虽然 TIME_WAIT 状态持续的时间是有一点长,显得很不友好,但是它被设计来就是用来避免发生乱七八糟的事情。

《UNIX网络编程》一书中却说道:TIME_WAIT 是我们的朋友,它是有助于我们的,不要试图避免这个状态,而是应该弄清楚它。

如果服务端要避免过多的 TIME_WAIT 状态的连接,就永远不要主动断开连接,让客户端去断开,由分布在各处的客户端去承受 TIME_WAIT

既然打开 net.ipv4.tcp_tw_reuse 参数可以快速复用处于 TIME_WAIT 状态的 TCP 连接,那为什么 Linux 默认是关闭状态呢?

这道题其实是在问[如果TIME_WAIT状态持续时间过短或者没有,会有什么问题呢]

tcp_tw_reuse 的作用是让客户端快速复用处于 TIME_WAIT 状态的端口,相当于跳过了 TIME_WAIT 状态,这可能会出现这样的两个问题:

  • 历史RST报文可能会终止后面相同四元组的连接
  • 如果第四次挥手的ACK报文丢失了,有可能被动关闭连接的一方不能被正常的关闭

在 TIME_WAIT 状态的 TCP 连接,收到 SYN 后会发生什么?

这要看SYN的序列号和时间戳是否合法,处于TIME_WAIT 状态的连接收到SYN之后,会根据SYN[序列号和时间戳]做不同的处理。

怎么处理

服务端是主动关闭方:

  • 如果SYN合法,那么会重用此四元组连接,跳过2MSL而转为SYN_RECV状态
  • 如果SYN非法,那么服务端会回复一个ACK(第四次挥手),客户端收到之后,发送不是自己期望的ACK,就会回复一个RST给服务端

什么是合法的时间戳

  • 如果双方都开启了时间戳
    • 合法 SYN:客户端的 SYN 的「序列号」比服务端「期望下一个收到的序列号」要大,并且 SYN 的「时间戳」比服务端「最后收到的报文的时间戳」要大。
    • 非法 SYN:客户端的 SYN 的「序列号」比服务端「期望下一个收到的序列号」要小,或者 SYN 的「时间戳」比服务端「最后收到的报文的时间戳」要小。
  • 如果双方都没有开启了时间戳
    • 合法 SYN:客户端的 SYN 的「序列号」比服务端「期望下一个收到的序列号」要大。
    • 非法 SYN:客户端的 SYN 的「序列号」比服务端「期望下一个收到的序列号」要小。

在 TIME_WAIT 状态,收到 RST 会断开连接吗?

会不会断开,关键在于net.ipv4.tcp_rfc1337 这个内核参数(默认情况是为 0):

  • 如果这个参数设置为 0, 收到 RST 报文会提前结束 TIME_WAIT 状态,释放连接。
  • 如果这个参数设置为 1, 就会丢掉 RST 报文。

四次挥手中收到乱序的 FIN 包会如何处理?

问题

这个问题其实是在问:在FIN_WAIT_2 状态下,是如何处理收到的乱序的FIN报文,然后TCP连接又是怎么才进入到TIME_WAIT这条的

回答

  • 在FIN_WAIT_2 状态,如果收到乱序的FIN报文,那么就会被加入到[乱序队列],并不会进入到TIME_WAIT状态
  • 等再次收到前面被网络延迟的数据宝时,会判断乱序队列中有没有数据,然后会检测乱序队列中是否有可用的数据,如果能在乱序队列中找到与当前报文的序列号保持的顺序的报文,就会看该报文是否有FIN标志,如果有,才会进入 TIME_WAIT 状态。

挥手能不能同时发起呢?

如果双方同时都主动发起了关闭,TCP 会怎么处理这种情况呢?我们看下图:
在这里插入图片描述

  • 双方同时发起关闭后,也同时进入了 FIN_WAIT_1 状态;
  • 然后也因为收到了对方的 FIN,也都进入了 CLOSING 状态;
  • 当双方都收到对方的 ACK 后,最终都进入了 TIME_WAIT 状态。

这也意味着,两端都需要等待 2MSL 的时间,才能复用这个五元组 TCP 连接。这种情况是比较少见的,但是也有

connect reset by peer是怎么回事?如何避免?

  • 网络:connect reset by peer是怎么回事
  • connect reset by peer,意思是对端peer回复了TCP RST(reset),终止了一次连接。即连接被关闭,连接已经建立起来了。我们要找的是在连接建立后发生的RST
  • 发生这个问题的原因有很多,比如网络不稳定、或者防火墙来几个 RST,也都有可能导致类似的 connection reset by peer 的问题
  • 如何尽量避免呢?
    • close()系统调用会走到tcp_close(),在这个函数里,会做判断,如果buffer里还有数据未读,它就直接调用tcp_send_active_reset(),发出RST,并把这个连接直接设置到CLOSED状态,也就是不进入TIME_WAIT
    • 对于这种情况,为了避免close()的时候发出RST,需要检查业务代码,确保在调用close()之前,把接收缓冲区中的数据读取掉

一方发送 FIN,表示这个连接开始关闭了,双方就都不会发送新的数据了?

不是的,一方发送FIN只是表示这一方不再发送新的数据,但是对方仍然可以发送数据。这个也叫做半关闭

  • 一端(A)发送 FIN,表示“我要关闭,不再发送新的数据了,但我可以接收新的数据”。
  • B 可以继续发送新的数据给 A,A 也会回复 ACK 表示确认收到新数据。
  • 在发送完这些新数据后,B 才启动了自己的关闭过程,也就是发送 FIN 给 A,表示“我的事情终于忙好了,我也要关闭,不会再发送新数据了”。

这时候才是真正的两端都关闭了连接。

FIN关闭和RST关闭的区别

  • 用RST的方式断开连接,用意是表示异常,至少让对端知道:我这儿多少有点问题,或者我觉得你这次连接中的表现有点问题,所以我用RST来关闭连接
  • 用FIN关闭意味着:我认为这次咱们通话顺利正常,我准备用安全标准的方式来关闭连接。

为什么 TCP 挥手需要四次呢?

TCP是双工的,所以发送方和接收方都需要FIN和ACK。只不过有一方是被动的,所以看上去就成了四次挥手

服务端收到客户端的FIN报文之后,内核会马上回一个ACK应答报文,但是服务端应用程序可能还有数据要发送,所以并不能马上发送FIN报文,于是将发送FIN报文的控制权交给服务端应用程序

  • 如果服务端应用程序有数据要发送的话,就发完数据后,才调用关闭连接的函数;
  • 如果服务端应用程序没有数据要发送的话,可以直接调用关闭连接的函数

从上面过程可知,**是否要发送第三次挥手的控制权不在内核,而是在被动关闭方(上图的服务端)的应用程序,因为应用程序可能还有数据要发送,由应用程序决定什么时候调用关闭连接的函数,当调用了关闭连接的函数,内核就会发送 FIN 报文了,**所以服务端的 ACK 和 FIN 一般都会分开发送。

FIN 报文一定得调用关闭连接的函数,才会发送吗?

不一定。

如果进程退出了,不管是不是正常退出,还是异常退出(如进程崩溃),内核都会发送 FIN 报文,与对方完成四次挥手

一个FIN就完成了TCP的挥手?

从上面可以看出,FIN和ACK都各有两次。

但是有时候,我们会抓到这样的包:
在这里插入图片描述
可以看到,TCP的挥手阶段只有一个FIN。

这种现象的可能原因是:TCP的一个报文可以搭另一个报文的顺风车(Piggybacking)以提高TCP传输的运载效率,所以TCP挥手不一定需要四个报文,Piggybacking 后,就可能是 3 个报文了。

从上面的最下面两行,我们可以这样理解:

  • 客户端回复了FIN + ACK
  • 服务端回复了ACK

那么第一个ACK在哪个呢?

Wireshark 的主界面还有个特点,就是当它的 Information 列展示的是应用层信息时,这个报文的 TCP 层面的控制信息就不显示了。我们选中上一个POST报文,然后到界面中间的TCP详情去看看:

在这里插入图片描述
可以看到,第一个FIN报文,并没有像常规的那样单独出现,而是合并
(Piggybacking)在 POST 报文里!所以,整个挥手过程,其实依然十分标准,完全遵循了协议规范。仅仅是因为 Wireshark 的显示问题

关于搭便车

挥手时候的四个报文是:

  • 发起端:FIN
  • 对端:ACK
  • 对端:FIN
  • 发起端:ACK

其中2和3经常在一起发送;1经常和发起端的其他报文一起发送

什么情况会出现三次挥手?

当被动方(一般是服务端)在挥手时,[没有数据要发送]并且[开启了TCP延迟确认机制],那么第二次和第三次挥手就会合并,这样就出现了三次挥手

然后因为 TCP 延迟确认机制是默认开启的,所以导致我们抓包时,看见三次挥手的次数比四次挥手还多。

什么是 TCP 延迟确认机制?

当发送没有携带数据的ACK时,它的网络效率是很低的,因为它也有40个字节的IP头和TCP头,但是缺没有发送数据报文。为了解决ACK传输效率低的问题,出现了TCP延迟确认,策略如下:

  • 当有响应数据要发送时,ACK会随着响应一起发送给对方
  • 当没有响应数据要发送时,ACK会延迟一段时间,以等待响应一起搭车
  • 如果在延迟等待ACK期间,对方的第二个数据报文又来了,这时会马上发送ACK

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

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

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

粗暴关闭 vs 优雅关闭

TCP关闭的连接的函数有两种函数:

  • close函数:同时socket关闭发送发现和读取发现,也就是socket不再有发送和接收数据的能力。如果多进程/多线程共享同一个socket,如果有一个进程调用了close关闭只是让socket引用计数-1,并不会导致socket不可用,同时也不会发出FIN报文,其他进程也是可以正常读写该socket,直到引用计数变为0,才会发出FIN报文
  • shutdown函数,可以指定 socket 只关闭发送方向而不关闭读取方向,也就是 socket 不再有发送数据的能力,但是还是具有接收数据的能力。如果有多进程/多线程共享同一个 socket,shutdown 则不管引用计数,直接使得该 socket 不可用,然后发出 FIN 报文,如果有别的进程企图使用该 socket,将会受到影响。

如果客户端是用 close 函数来关闭连接,那么在 TCP 四次挥手过程中,如果收到了服务端发送的数据,由于客户端已经不再具有发送和接收数据的能力,所以客户端的内核会回 RST 报文给服务端,然后内核会释放连接,这时就不会经历完成的 TCP 四次挥手,所以我们常说,调用 close 是粗暴的关闭。

当服务端收到RST之后,内核就会释放连接,当服务端应用程序再次读/写,就能告知到连接已经被释放了:

  • 如果是读操作,那么会返回RST的报错,也就是Connection reset by peer。
  • 如果是写操作,那么程序会程序会产生 SIGPIPE 信号,应用层代码可以捕获并处理信号,如果不处理,则默认进程会终止,异常退出

相对的,shutdown 函数因为可以指定只关闭发送方向而不关闭读取方向,所以即使在 TCP 四次挥手过程中,如果收到了服务端发送的数据,客户端也是可以正常读取到该数据的,然后就会经历完整的 TCP 四次挥手,所以我们常说,调用 shutdown 是优雅的关闭。
但是注意,shutdown 函数也可以指定「只关闭读取方向,而不关闭发送方向」,但是这时候内核是不会发送 FIN 报文的,因为发送 FIN 报文是意味着我方将不再发送任何数据,而 shutdown 如果指定「不关闭发送方向」,就意味着 socket 还有发送数据的能力,所以内核就不会发送 FIN。

如果已经建立了连接,但是客户端突然出现了故障会怎么样?

问题

  • 如果已经建立了连接,但是客户端突然出现了故障会怎么样?
  • 如果已经建立了连接,但是一直没有数据交互会怎么样?

回答

TCP中有一个保活机制:在一个时间段内,如果没有任何连接相关的活动,TCP保活机制就会开始作用,每隔一个时间间隔,发送一个探测报文,该报文数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的TCP连接已经死亡,系统内核将错误信息通知给上层应用程序。

在Liunx内核可以有对应的参数可以设置保活时间、保活探测的次数、保活探测的时间间隔,如下为默认值:

net.ipv4.tcp_keepalive_time=7200
net.ipv4.tcp_keepalive_intvl=75  
net.ipv4.tcp_keepalive_probes=9
  • tcp_keepalive_time=7200:表示保活时间是 7200 秒(2小时),也就 2 小时内如果没有任何连接相关的活动,则会启动保活机制
  • tcp_keepalive_intvl=75:表示每次检测间隔 75 秒;
  • tcp_keepalive_probes=9:表示检测 9 次无响应,认为对方是不可达的,从而中断本次的连接。
  • 也就是说在 Linux 系统中,最少需要经过 2 小时 11 分 15 秒 = 7200 + (75 * 9)才可以发现一个「死亡」连接。

注意,应用程序如果想要使用TCP保活机制需要通过socket接口设置SO_KEEPALVE选项才能生效,如果没有设置,就无法使用TCP保活机制。

如果开启了TCP保活机制,需要考虑如下几种情况:

  • 第一种:对端程序是正常工作的。当TCP保活的探测报文发送给了对端,对端会正常响应,这样TCP保活时间会被重置,等待下一个TCP保活时间到了
  • 第二种,对端程序崩溃并且重启。当TCP保活探测报文发送给对端后,对端会响应,但是由于没有该连接的有效信息,会产生一个RST报文,这样很快就会发现TCP连接已经被重置
  • 第三种,对端程序崩溃,或者对端由于其他原因导致报文不可达。当TCP保活的探测报文发送给对端后,对端无法响应,连续机制都没有响应,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡

TCP保活的这个机制探测时间有点长,我们可以在应用层自己实现一个心跳机制。

比如,web 服务软件一般都会提供 keepalive_timeout 参数,用来指定 HTTP 长连接的超时时间。如果设置了 HTTP 长连接的超时时间是 60 秒,web 服务软件就会启动一个定时器,如果客户端在完成一个 HTTP 请求后,在 60 秒内都没有再发起新的请求,定时器的时间一到,就会触发回调函数来释放该连接。

即,TCP保活机制(tcp keepalive)可以在双方没有数据交互的情况下,通过探测报文,来确定双方的TCP连接是否存活

注意,应用程序若想使用 TCP 保活机制需要通过 socket 接口设置 SO_KEEPALIVE 选项才能够生效,如果没有设置,那么就无法使用 TCP 保活机制。

如果已经建立了连接,主机崩溃和进程崩溃有什么区别?

问题

一个TCP连接,没有开启keepalive,一直没有数据交互,进程崩溃和主机崩溃的区别。

分析

(2)如果客户端的「主机崩溃」了,会发生什么?

  • 客户端主机崩溃了,服务端是无法感知的
    • 如果服务端会发生数据,由于客户端已经不存在,收不到响应,服务端就会触发超时重传,当重传总间隔时长达到一定阈值(内核会根据 tcp_retries2 设置的值计算出一个阈值)后,会断开 TCP 连接;
    • 如果服务端不发送数据
      • 如果服务端没有开启TCP keepalive 机制,服务端的TCP连接将会一直存在,并且一直保持在 ESTABLISHED 状态。
      • 如果开启TCP keepalive ,那么当一段时间没有数据传输,那么会触发 TCP keepalive ,探测对方是否存在,如果对方已经死亡,则会断开和自身的连接

(3) 如果客户端的「进程崩溃」了,会发生什么?

  • TCP 的连接信息是由内核维护的,所以当服务端的进程崩溃后,内核需要回收该进程的所有 TCP 连接资源,于是内核会发送第一次挥手 FIN 报文,后续的挥手过程也都是在内核完成,并不需要进程的参与,所以即使服务端的进程退出了,还是能与客户端完成 TCP四次挥手的过程。
  • 所以,即使没有开启 TCP keepalive,且双方也没有数据交互的情况下,如果其中一方的进程发生了崩溃,这个过程操作系统是可以感知的到的,于是就会发送 FIN 报文给对方,然后与对方进行 TCP 四次挥手

如果已经建立了连接,而且有数据传输

客户端主机宕机,又快速重启,会发生什么?

客户端宕机了,服务端的报文无法得到响应,服务端会超时重传

如果在重传报文的过程中,客户机主机重启完成,那么客户机的内核就会接收重传的报文,然后根据报文的信息传递给对应的进程。

  • 如果客户机上没有进程绑定该TCP报文的目标端口号,那么客户机内核就会回复RST报文,重置该TCP连接
  • 如果客户机上进程绑定该TCP报文的目标端口号,由于客户机重启了,之前的TCP连接的数据结构已经丢失,客户机内核协议栈中找不到该TCP连接的socket结构体,于是就会回复RTSP报文,重置该TCP连接

所以,只要有一方重启完成后,收到之前 TCP 连接的报文,都会回复 RST 报文,以断开连接

客户端主机宕机,又一直没有重启,会发生什么?

此时,服务端超时重传达到一定次数后,内核就会判断该TCP有问题,然后通过socket接口告知应用程序该TCP连接出现问题了,于是TCP连接会断开

为什么握手是三次,挥手是四次

  • 对于握手:
    • 握手目的有两个:
      • 建立连接
      • 需要确认双向通信时的初始化序号,保证通信不会乱序
    • 什么叫做建立连接呢?
      • 为什么要三次,而不是两次?按理来说,两个人打招呼,一来一回就可以了啊?为了可靠,为什么不是四次?
      • 我们还是假设这个通路是非常不可靠的,A要发起一个连接,当发起了第一个请求渺无音信的时候,会有很多可能去,比如第一个请求包丢了;再比如没有丢,但是饶了弯路,超时了;还有就是B没有响应,不想和我连接。
      • A不能确认结果,于是再发,再发,终于,有一个请求包到了B,但是请求包到了B的这个事情,目前A还是不知道的,A还有可能再发。
      • B收到了请求包,知道了A的存在,并且知道A要和它建立连接。如果B不乐意建立连接,则A会重试一阵后放弃,连接建立失败,没有问题;如果B是乐意建立连接的,则会发送应答包给A。
      • 当然对于B来讲,这个包也是一入网络深似海,不知道能不能到达A。这个时候B自然不能认为连接是建立好了,因为应答包仍然会丢,会绕弯路,或者A已经挂了都有可能。
      • 而且这个B还能碰到的一个诡异的现象是,A和B原来建立了连接,做了简单通信后,结束了连接。还记得吗?A建立连接的时候,请求包重复发了几次,有的请求包饶了一大圈又回来了,B认为这也是一个正常的请求的话,因此建立了连接,可以想象,这个连接不会进行下去,也没有个终结的时候。因而两次握手肯定不行。
      • B的应答可能会发送多次,但是只要一次到达A,A就认为连接已经建立了,因为对于A来讲,它的信息有去有回。A会给B发送应答值应答,而B也在等这个消息,才能确认连接的建立,只有等到了这个消息,对于B来讲,才算它的消息有去有回。
      • 当然A发送给B的应答之应答也会丢,也会绕路,甚至B挂了。按理来说,还应该有关应答之应答之应答,这样下去就没底了,所以四次握手也是可以的,四十次都可以,关键是四百次也不能保证就真的可靠了,只要双方的消息都有去回,就基本可以了
      • 好在大部分情况下,A和B建立了连接之后,A会马上发送数据的,一旦A发送数据,则很多问题就得到了解决。比如A发给B的应答丢了,当A后续发送的数据到达的时候,B可以认为这个连接已经建立,或者B压根就挂了,A发送的数据,会报错,说B不可达,A就知道B出问题了。
      • 当然有可能A比较坏,就是不发送数据,建立连接后空着。我们在程序设计的时候,可以要求开启keepalive机制,即使没有真实的数据报,也有探活包。
      • 对于,对服务段B来说,对于A这种长时间不发包的客户端,可以主动关闭,从而空出资源来给其他客户端使用。
    • 为什么要知道初始化序号问题呢?
      • A要告诉B,我这面发起的包的序号起始是从哪个号开始的,B同样也要靠近A,B发起的包的序号起始是从哪个号开始的,为什么序列不能都从1开始呢?因为这样往往会出现冲突。
      • 比如,A连上B之后,发送了1、2、3三个包,但是发送3的时候,中间丢了,或者绕路了,于是重新发送,后来A掉线了,重新连上B之后,序号又从1开始,仍然会发送2,但是压根没想到发送3,但是上次绕路的那个3又回来了,发给了B,B自然认为,这是下一个包,于是发生了错误
      • 因此,每个连接都要有不同的序号。这个序号的起始时间是随着时间编号的,可以看出一个32位的计数器,每4ms加1,如果到重复,需要4个多小时,那个绕路的包早就死翘翘了,因为我们都知道IP包头里面有个TTL,也就是生存时间。
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值