秋招复习笔记——八股文部分:网络TCP

TCP 三次握手和四次挥手

TCP 基本认识

TCP 的报文格式

序列号:在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。

确认应答号:指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决丢包的问题。

控制位:

  • ACK:该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1 。
  • RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。
  • SYN:该位为 1 时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。
  • FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。

分层模型

IP 层是「不可靠」的,它不保证网络包的交付、不保证网络包的按序交付、也不保证网络包中的数据的完整性。为了能够可靠传递就需要上层(传输层)的 TCP 协议负责。TCP 是一个工作在传输层的可靠数据传输的服务,它能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的

TCP 传输层通信协议特点

TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。面向连接,就是指一定是「一对一」的连接,不像 UDP 可以一对多;可靠的就是指 TCP 能保证一个报文到达接收端;字节流是指 TCP 报文有序,即使后面的报文先到达也不会给应用层而是等待,同时重复报文会自动丢弃。

连接所需信息

用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括 Socket、序列号和窗口大小称为连接。Socket 就是由 IP 地址和端口号组成;序列号之前已经学习,就是解决乱序问题;窗口大小就是用来流量控制。

那么确认连接,依靠的就是 TCP 的四元组,包含了:源地址和目的地址,32 位在 IP 头部中;源端口和目的端口,16 位,在 TCP 头部。

TCP 最大连接数

上述是 TCP 理论上服务端的最大连接数,对 IPv4 来说,客户端 IP 最多 2^32,客户端端口最多 2^16,也就是说理论上最大连接数为 2^48。当然,还会有以下的影响因素:

  • 文件描述符限制,每个 TCP 连接都是一个文件,如果文件描述符被占满了,会发生 Too many open files。Linux 对可打开的文件描述符的数量分别作了三个方面的限制:
    • 系统级:当前系统可打开的最大数量,通过 cat /proc/sys/fs/file-max 查看;
    • 用户级:指定用户可打开的最大数量,通过 cat /etc/security/limits.conf 查看;
    • 进程级:单个进程可打开的最大数量,通过 cat /proc/sys/fs/nr_open 查看;
  • 内存限制,每个 TCP 连接都要占用一定内存,操作系统的内存是有限的,如果内存资源被占满后,会发生 OOM。

那么,为什么有 UDP 还有 TCP 呢?因为 UDP 非常简单,但不能提供复杂机制,头部只有 8 个字节:

UDP 报文格式
两者的区别如下:

  • 连接

TCP 是面向连接的传输层协议,传输数据前先要建立连接。
UDP 是不需要连接,即刻传输数据。

  • 服务对象

TCP 是一对一的两点服务,即一条连接只有两个端点。
UDP 支持一对一、一对多、多对多的交互通信。

  • 可靠性

TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按序到达。
UDP 是尽最大努力交付,不保证可靠交付数据。但是我们可以基于 UDP 传输协议实现一个可靠的传输协议,比如 QUIC 协议。

  • 拥塞控制、流量控制

TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。
UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。

  • 首部开销

TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是 20 个字节,如果使用了「选项」字段则会变长的。
UDP 首部只有 8 个字节,并且是固定不变的,开销较小。

  • 传输方式

TCP 是流式传输,没有边界,但保证顺序和可靠。
UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序。

  • 分片不同

TCP 的数据大小如果大于 MSS 大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装 TCP 数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片。
UDP 的数据大小如果大于 MTU 大小,则会在 IP 层进行分片,目标主机收到后,在 IP 层组装完数据,接着再传给传输层。

所以,TCP 是保证数据的可靠性的,多用于 FTP 文件传输以及 HTTP/HTTPS;而 UDP 处理简单,常用于包总量较少的通信 DNS、SNMP 等,还有视频和广播通信等。

TCP 有「首部长度」字段,因为 TCP 是可变长的,UDP 不会变化,所以没有这个字段。

TCP 实际数据量
TCP 的数据长度,IP 相关的内容在 IP 首部格式中都已知,而 TCP 首部长度也可以通过头部格式中的信息获取,所以 TCP 数据长度是可以计算的,不需要「包长度」字段;UDP 则因为各种问题,是有「包长度」字段的,靠谱的说法是首部长度需要是 4 的倍数,加上这个字段就正好。

**这里注意,TCP 和 UDP 是可以使用同一个端口的。**数据链路层,MAC 地址来寻找局域网中主机;网络层通过 IP 地址寻找主机;传输层通过端口来寻址找到同一计算机中通信的不同应用程序。端口号就是为了区分同一主机上不同的应用程序的数据包。所以说,TCP、UDP 端口号相互独立,不冲突。

TCP 连接建立

三次握手

建立连接是通过三次握手来进行的。三次握手的过程如下图:

三次握手流程

  • 最开始,客户端和服务端都是 CLOSE 状态。服务端主动监听某个端口,处于 LISTEN 状态。

第一个报文:SYN 报文

  • 客户端随机初始化序列号(client_isn),然后 SYN 位置 1,表示 SYN 报文。然后发送 SYN 报文,这里不包含应用层数据,之后客户端处于 SYN-SENT 状态

第二个报文:SYN + ACK 报文

  • 服务端收到 SYN 报文,也随机初始化自己序号(server_isn),然后把 TCP 头部的「确认应答号」字段填入 client_isn + 1, 接着把 SYN 和 ACK 标志位置为 1。最后发回给客户端,也不包含应用层数据,之后服务端处于 SYN-RCVD 状态

第三个报文:ACK 报文

  • 客户端收到后,需要回应应答报文。把 ACK 标志位置 1其次「确认应答号」字段填入 server_isn + 1 ,最后把报文发送给服务端,这次报文可以携带客户到服务端的数据,之后客户端处于 ESTABLISHED 状态
  • 服务端收到后,也进入 ESTABLISHED 状态。

记住:第三次握手是可以携带数据的,前两次握手是不可以携带数据的!

在 Linux 可以通过 netstat -napt 命令查看 TCP 连接状态。

三次握手的原因

三次握手的原因(为什么一定是三次):

  • 阻止重复历史连接的初始化(主要原因)

首要原因是为了防止旧的重复连接初始化造成混乱。如果客户端发了旧的 SYN 报文然后网络拥堵或者宕机,然后恢复后发了新的 SYN 报文,此时服务端回发的 SYN + ACK 报文就会对不上客户端期望的确认应答号,就会回发 RST 报文,服务端收到后就会释放连接;在之后新的 SYN 报文就到了,会重新三次握手。

两次握手,就无法阻止历史连接,因为此时服务端没有中间状态给客户端来阻止历史连接,导致服务端可能建立一个历史连接,造成资源浪费。要解决这种现象,最好就是在服务端发送数据前,也就是建立连接之前,要阻止掉历史连接,这样就不会造成资源浪费,而要实现这个功能,就需要三次握手

  • 同步双方初始序列号

TCP 协议的通信双方, 都必须维护一个「序列号」,因为依靠序列号接收方可以去除重复数据;接收方可以以此按序接收;通过序列号来标识已经被接收到的报文(通过 ACK 报文序列号)。

因此,客户端发送带「初始序列号」的 SYN 报文的时候,需要服务端回一个 ACK 应答报文,表示客户端的 SYN 报文已被服务端成功接收;同理服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。通过四次握手,也就是两次互相发 SYN 和 ACK 信号,就能完成序列号同步。那么可以将服务端回复 SYN 和发送 ACK 合并到一起,就变成了同时发 SYN + ACK,也就变成了三次握手

  • 避免浪费资源

如之前所说,只有两次握手,那么就会有历史连接,因为服务端不确定客户端是否收到 ACK 报文,就只能每收到一个 SYN 报文都先主动建立一个连接。这样就会造成资源浪费。

初始序列号不同的原因

每次建立 TCP 连接,初始化的序列号不一样,是因为:

  • 防止历史报文被下一个相同四元组连接接收(主要);
  • 安全性,防止相同序列号 TCP 报文被接受。

针对第一个方面,例如双方已经都 ESTABLISHED 之后,客户端传资源超时,服务端断电重启,那么久会在重启的时候,建立失效进而导致服务端回发 RST 让客户端进入 CLOSED。然后如果再次建立连接,此时序列号是一样的,而上一次连接发送的那个报文正好抵达服务端,那么现在的序列号没变就会导致数据传递是有效的,那么服务端就会正常接收导致数据产生混乱。如果序列号不一样,那么第二次连接生效的序列号就发生了改变,也就不会导致接收成功了,会把该报文直接丢弃。

初始序列号随机产生

初始序列号 ISN 是基于时钟,每 4 微秒 + 1,转一圈要 4.55 个小时。随机生成算法:ISN = M + F(localhost, localport, remotehost, remoteport)。所以基本不会有一样的初始化序列号 ISN。

TCP 的 MSS存在意义

IP 层会分片,那为什么还要在 TCP 层设置 MSS?

MSS 和 MTU

IP 层的分片就是把数据(TCP 头部 + TCP 数据)发送出去,如果超过 MTU大小,就进行分片重组再交给 TCP 传输层。如果 IP 分片有一个丢失了,那么整个 IP 报文所有分片都得重传。IP 本身没有超时重传机制,那么就会造成传输层 TCP 来负责超时和重传。这样就会导致,整个 TCP 的报文都得重发。所以,为了达到最佳的传输效能 TCP 协议在建立连接的时候通常要协商双方的 MSS 值,当 TCP 层发现数据超过 MSS 时,则就先会进行分片,当然由它形成的 IP 包的长度也就不会大于 MTU ,自然也就不用 IP 分片了。这样,如果一个 TCP 分片丢失,重发也是以 MSS 为单位,不用重传所有分片,大大提升效率

第一次握手丢失

当客户端想和服务端建立 TCP 连接的时候,首先第一个发的就是 SYN 报文,然后进入到 SYN_SENT 状态。在这之后,如果客户端迟迟收不到服务端的 SYN-ACK 报文(第二次握手),就会触发「超时重传」机制,重传 SYN 报文,而且重传的 SYN 报文的序列号都是一样的。

每次重传判断超时的时间是写在内核里的,最大重传次数由 tcp_syn_retries内核参数控制,这个参数是可以自定义的,默认值一般是 5。每次超时重传的时间是上一次的 2 倍。也就是说,会等到 tcp_syn_retries 次,客户端直接断开连接

第二次握手丢失

当服务端收到客户端的第一次握手后,就会回 SYN-ACK 报文给客户端,这个就是第二次握手,此时服务端会进入 SYN_RCVD 状态。相当于发了针对第一次握手的确认 ACK 报文,同时发了服务端建立 TCP 连接的 SYN 报文。

所以,如果第二次丢失,对第一次握手的确认报文就没有了,客户端就会触发超时重传机制,重传 SYN 报文。而服务端发送的 SYN 报文也就没有回复的确认报文,服务端这边会触发超时重传机制,重传 SYN-ACK 报文。SYN-ACK 报文的最大重传次数由 tcp_synack_retries内核参数决定,默认值是 5。

第三次握手丢失

客户端收到服务端的 SYN-ACK 报文后,就会给服务端回一个 ACK 报文,也就是第三次握手,此时客户端状态进入到 ESTABLISH 状态。因为这个第三次握手的 ACK 是对第二次握手的 SYN 的确认报文,所以当第三次握手丢失了,如果服务端那一方迟迟收不到这个确认报文,就会触发超时重传机制,重传 SYN-ACK 报文,直到收到第三次握手,或者达到最大重传次数。因为 ACK 报文是没有重传的,所以只能有对方重传相应的 SYN 报文。

SYN 攻击

攻击者短时间伪造不同 IP 地址的 SYN 报文,服务端每接收到一个 SYN 报文,就进入SYN_RCVD 状态,但服务端发送出去的 ACK + SYN 报文,无法得到未知 IP 主机的 ACK 应答,久而久之就会占满服务端的半连接队列,使得服务端不能为正常用户服务。

半连接队列,就是 SYN 队列;全连接队列,就是 accept 队列。

TCP 连接正常流程

SYN 攻击方式最直接的表现就会把 TCP 半连接队列打满,这样当 TCP 半连接队列满了,后续再在收到 SYN 报文就会丢弃,导致客户端无法和服务端建立连接。

解决方法:调大 netdev_max_backlog;增大 TCP 半连接队列;开启 tcp_syncookies(echo 把 1 写入);减少 SYN+ACK 重传次数。

TCP 连接断开

四次挥手

四次挥手过程

  • 客户端打算关闭连接,此时会发送一个 TCP 首部 FIN 标志位被置为 1 的报文,也即 FIN 报文,之后客户端进入 FIN_WAIT_1 状态。
  • 服务端收到该报文后,就向客户端发送 ACK 应答报文,接着服务端进入 CLOSE_WAIT 状态。
  • 客户端收到服务端的 ACK 应答报文后,之后进入 FIN_WAIT_2 状态。
  • 等待服务端处理完数据后,也向客户端发送 FIN 报文,之后服务端进入 LAST_ACK 状态。
  • 客户端收到服务端的 FIN 报文后,回一个 ACK 应答报文,之后进入 TIME_WAIT 状态
  • 服务端收到了 ACK 应答报文后,就进入了 CLOSE 状态,至此服务端已经完成连接的关闭。
  • 客户端在经过 2MSL 一段时间后,自动进入 CLOSE 状态,至此客户端也完成连接的关闭。

每个方向都需要一个 FIN 和一个 ACK,因此通常被称为四次挥手。这里一点需要注意是:主动关闭连接的,才有 TIME_WAIT 状态

挥手四次的原因

  • 关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据
  • 服务端收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。

所以,服务端通常需要等待完成数据的发送处理,所以 ACK 和 FIN 包是分开发送的。当然特殊情况下可以变成三次挥手,之后会学习

第一次挥手丢失

当客户端(主动关闭方)调用 close 函数后,就会向服务端发送 FIN 报文,试图与服务端断开连接,此时客户端的连接进入到 FIN_WAIT_1 状态。正常情况下,如果能及时收到服务端(被动关闭方)的 ACK,则会很快变为 FIN_WAIT2状态。

如果第一次丢失,那相当于客户端一直等不到服务端 ACK,就会触发超时重传,重传 FIN 报文,重发次数由 tcp_orphan_retries 参数控制。每一次的等待时间都是上一次的 2 倍。如果等到 tcp_orphan_retries + 1 次,就会进入 close 状态。

第二次挥手丢失

当服务端收到客户端的第一次挥手后,就会先回一个 ACK 确认报文,此时服务端的连接进入到 CLOSE_WAIT 状态

因为 ACK 不会重传,所以第二次挥手丢失,那就会等到客户端触发超时重传,重传 FIN 报文,直到收到服务端的第二次挥手,或者达到最大的重传次数。

当客户端收到第二次挥手,也就是收到服务端发送的 ACK 报文后客户端就会处于 FIN_WAIT2 状态,在这个状态需要等服务端发送第三次挥手,也就是服务端的 FIN 报文。对于 close 函数关闭的连接,由于无法再发送和接收数据,所以 FIN_WAIT2 状态不可以持续太久,而 tcp_fin_timeout 控制了这个状态下连接的持续时长,默认值是 60 秒

这意味着对于调用 close 关闭的连接,如果在 60 秒后还没有收到 FIN 报文,客户端(主动关闭方)的连接就会直接关闭

但是注意,如果主动关闭方使用 shutdown 函数关闭连接,指定了只关闭发送方向,而接收方向并没有关闭,那么意味着主动关闭方还是可以接收数据的。此时,如果主动关闭方一直没收到第三次挥手,那么主动关闭方的连接将会一直处于 FIN_WAIT2 状态(tcp_fin_timeout 无法控制 shutdown 关闭的连接)。

第三次挥手丢失

当服务端(被动关闭方)收到客户端(主动关闭方)的 FIN 报文后,内核会自动回复 ACK,同时连接处于 CLOSE_WAIT 状态,顾名思义,它表示等待应用进程调用 close 函数关闭连接。此时,内核是没有权利替代进程关闭连接,必须由进程主动调用 close 函数来触发服务端发送 FIN 报文

服务端处于 CLOSE_WAIT 状态时,调用了 close 函数,内核就会发出 FIN 报文,同时连接进入 LAST_ACK 状态,等待客户端返回 ACK 来确认连接关闭。如果没有 ACK,服务端会重发 FIN 报文,重发次数仍然由 tcp_orphan_retries 参数控制,这与客户端重发 FIN 报文的重传次数控制方式是一样的。

第四次挥手

当客户端收到服务端的第三次挥手的 FIN 报文后,就会回 ACK 报文,也就是第四次挥手,此时客户端连接进入 TIME_WAIT 状态。在 Linux 系统,TIME_WAIT 状态会持续 2MSL 后才会进入关闭状态

如果第四次挥手的 ACK 报文没有到达服务端,服务端就会重发 FIN 报文,重发次数仍然由前面介绍过的 tcp_orphan_retries 参数控制。

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时长 这其实是相当于至少允许报文丢失一次。而起始计算的时间是从客户端接收到 FIN 后发送 ACK 开始计时,如果在 TIME-WAIT 时间内客户端接收到重发的 FIN 报文,2MSL 会重新计时

Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒。

为什么要 TIME_WAIT 状态

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

主要是两个原因:

  • 防止历史连接中的数据,被后面相同四元组的连接错误的接收;
  • 保证「被动关闭连接」的一方,能被正确的关闭。

序列号是 TCP 一个头部字段,标识了 TCP 发送端到接收端的数据流的一个字节,是一个 32 位的无符号数,因此在到达 4G 之后再循环回到 0。初始序列号,是客户端和服务端会各自生成的一个随机数,可被视为一个 32 位的计数器,该计数器的数值每 4 微秒加 1,循环一次需要 4.55 小时。两者并不是无限递增的,会发生回绕为初始值的情况,这意味着无法根据序列号来判断新老数据。

如果服务端关闭连接前发送了 SEQ 的报文,然后服务端以相同四元组重新打开新连接,则之前的 SEQ 报文就会抵达客户端,该序列号又刚好落在客户端接收窗口内,就会使得客户端正常接收,导致数据错乱。

因此 TCP 设计了 TIME_WAIT 状态,状态会持续 2MSL 时长,这个时间足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。

TIME-WAIT 还有一个作用是等待足够的时间以确保最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭。如果客户端(主动关闭方)最后一次 ACK 报文(第四次挥手)在网络中丢失了,那么按照 TCP 可靠性原则,服务端(被动关闭方)会重发 FIN 报文。假设客户端没有 TIME_WAIT 状态,而是在发完最后一次回 ACK 报文就直接进入 CLOSE 状态,如果该 ACK 报文丢失了,服务端则重传的 FIN 报文,而这时客户端已经进入到关闭状态了,在收到服务端重传的 FIN 报文后,就会回 RST 报文。此时就发生了异常情况,这对于可靠协议而言还是有问题的。

TIME_WAIT 过多的危害

  • 第一是占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等;
  • 第二是占用端口资源,端口资源也是有限的,一般可以开启的端口为 32768~61000,也可以通过 net.ipv4.ip_local_port_range 参数指定范围。

如果客户端(主动发起关闭连接方)的 TIME_WAIT 状态过多,占满了所有端口资源,就无法对「目的 IP+ 目的 PORT」都一样的服务端发起连接了,但已经被使用的端口还是可以继续对另一个服务端发起连接的。(四元组来定位,客户端端口一样并不影响连接)

如果服务端(主动发起关闭连接方)的 TIME_WAIT 状态过多,并不会导致端口资源受限,因为服务端只监听一个端口,但是 TCP 连接过多,会占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等。

优化 TIME_WAIT

打开 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 选项;复用处于 TIME_WAIT 的 socket 为新的连接所用。tcp_tw_reuse 功能只能用客户端(连接发起方),因为开启了该功能,在调用 connect() 函数时,内核会随机找一个 time_wait 状态超过 1 秒的连接给新的连接复用。(这个值置 1),还有一个前提,需要打开对 TCP 时间戳的支持(默认开启)。在 TCP 头部的「选项」里,它由一共 8 个字节表示时间戳,其中第一个 4 字节字段用来保存发送该数据包的时间,第二个 4 字节字段用来保存最近一次接收对方发送到达数据的时间。由于引入了时间戳,我们在前面提到的 2MSL 问题就不复存在了,因为重复的数据包会因为时间戳过期被自然丢弃。

net.ipv4.tcp_max_tw_buckets,这个值默认为 18000,当系统中处于 TIME_WAIT 的连接一旦超过这个值时,系统就会将后面的 TIME_WAIT 连接状态重置,这个方法比较暴力。

可以通过设置 socket 选项,来设置调用 close 关闭连接行为。如下所示,l_onoff 非 0,l_linger 为 0,那么 close 后会直接发送 RST 给对面,跳过四次挥手。这个并不提倡。

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。

服务器出现大量 TIME_WAIT

首先要知道 TIME_WAIT 状态是主动关闭连接方才会出现的状态,也就是说现在是服务器主动断开很多 TCP 连接。

会有如下三个场景:

  • HTTP 没有使用长连接

从 HTTP/1.1 开始, 就默认是开启了 Keep-Alive;如果要关闭 HTTP Keep-Alive,需要在 HTTP 请求或者响应的 header 里添加 Connection:close 信息,也就是说,只要客户端和服务端任意一方的 HTTP header 中有 Connection:close 信息,那么就无法使用 HTTP 长连接的机制。

虽然 RFC 文档中,请求和响应的双方都可以主动关闭 TCP 连接。不过,根据大多数 Web 服务的实现,不管哪一方禁用了 HTTP Keep-Alive,都是由服务端主动关闭连接,那么此时服务端上就会出现 TIME_WAIT 状态的连接。

客户端禁用了 HTTP Keep-Alive,服务端开启 HTTP Keep-Alive,服务端是主动关闭方。HTTP 是请求-响应模型,发起方一直是客户端HTTP Keep-Alive 的初衷是为客户端后续的请求重用连接,如果我们在某次 HTTP 请求-响应模型中,请求的 header 定义了 connection:close 信息,那不再重用这个连接的时机就只有在服务端了,所以我们在 HTTP 请求-响应这个周期的「末端」关闭连接是合理的。

当客户端开启了 HTTP Keep-Alive,而服务端禁用了 HTTP Keep-Alive,这时服务端在发完 HTTP 响应后,服务端也会主动关闭连接。在服务端主动关闭连接的情况下,只要调用一次 close() 就可以释放连接,剩下的工作由内核 TCP 栈直接进行了处理,整个过程只有一次 syscall;如果是要求 客户端关闭,则服务端在写完最后一个 response 之后需要把这个 socket 放入 readable 队列,调用 select / epoll 去等待事件;然后调用一次 read() 才能知道连接已经被关闭,这其中是两次 syscall,多一次用户态程序被激活执行,而且 socket 保持时间也会更长。

**当服务端出现大量的 TIME_WAIT 状态连接的时候,可以排查下是否客户端和服务端都开启了 HTTP Keep-Alive。**任意一方没开都会导致服务端在处理完一个 HTTP 请求后,就主动关闭连接,此时服务端上就会出现大量的 TIME_WAIT 状态的连接。

解决办法也很简单,让客户端和服务端都开启 HTTP Keep-Alive 机制

  • HTTP 长连接超时

HTTP 长连接的特点是,只要任意一端没有明确提出断开连接,则保持 TCP 连接状态

假设设置了 HTTP 长连接的超时时间是 60 秒,nginx 就会启动一个「定时器」,如果客户端在完后一个 HTTP 请求后,在 60 秒内都没有再发起新的请求,定时器的时间一到,nginx 就会触发回调函数来关闭该连接,那么此时服务端上就会出现 TIME_WAIT 状态的连接

当服务端出现大量 TIME_WAIT 状态的连接时,如果现象是有大量的客户端建立完 TCP 连接后,很长一段时间没有发送数据,那么大概率就是因为 HTTP 长连接超时,导致服务端主动关闭连接,产生大量处于 TIME_WAIT 状态的连接。

解决方法:排查网络问题。

  • HTTP 长连接请求数量达到上限

Web 服务端通常会有个参数,来定义一条 HTTP 长连接上最大能处理的请求数量,当超过最大限制时,就会主动关闭连接。如果达到这个参数设置的最大值时,则 nginx 会主动关闭这个长连接。

对于一些 QPS 比较高的场景,比如超过 10000 QPS,甚至达到 30000 , 50000 甚至更高,如果 keepalive_requests 参数值是 100,这时候就 nginx 就会很频繁地关闭连接,那么此时服务端上就会出大量的 TIME_WAIT 状态

解决方法:调大对应参数。

服务器出现大量 CLOSE_WAIT 状态

CLOSE_WAIT 状态是「被动关闭方」才会有的状态,而且如果「被动关闭方」没有调用 close 函数关闭连接,那么就无法发出 FIN 报文,

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值