Skip to content

1. TCP连接

详见此文

1.1 基本认识

1.1.1 TCP 头部格式

TCP 是一个工作在传输层的可靠数据传输的服务,它能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的

其头部格式如下: 头部格式

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

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

  • 控制位:

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

1.1.2 什么是TCP

TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。

  • 面向连接:一定是一对一才能连接,不能像 UDP 协议可以一个主机同时向多个主机发送消息,也就是一对多是无法做到的;

  • 可靠的:无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端;

  • 字节流:用户消息通过 TCP 协议传输时,消息可能会被操作系统「分组」成多个的 TCP 报文,如果接收方的程序如果不知道「消息的边界」,是无法读出一个有效的用户消息的。并且 TCP 报文是「有序的」,当「前一个」TCP 报文没有收到的时候,即使它先收到了后面的 TCP 报文,那么也不能扔给应用层去处理,同时对「重复」的 TCP 报文会自动丢弃。

1.1.3 什么是TCP连接

RFC 793 是如何定义「连接」的:

Connections: The reliability and flow control mechanisms described above require that TCPs initialize and maintain certain status information for each data stream. The combination of this information, including sockets, sequence numbers, and window sizes, is called a connection.

即:用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括 Socket序列号窗口大小称为连接。

1.1.4 如何唯一确定一个 TCP 连接

TCP 四元组可以唯一的确定一个连接:

  • 源地址
  • 源端口
  • 目的地址
  • 目的端口

源地址和目的地址的字段(32 位)是在 IP 头部中,作用是通过 IP 协议发送报文给对方主机。

源端口和目的端口的字段(16 位)是在 TCP 头部中,作用是告诉 TCP 协议应该把报文发给哪个进程。

1.1.5 最大TCP连接数

具有一个IP的服务端监听了一个端口。

因为服务端通常固定在某个本地端口上监听,等待客户端的连接请求。 因此,客户端 IP 和端口是可变的,其理论值计算公式如下:

最大TCP连接数=客户端IP数 \* 客户端端口数

对 IPv4,客户端的 IP 数最多为 2 的 32 次方,客户端的端口数最多为 2 的 16 次方,也就是理论上服务端单机最大 TCP 连接数,约为 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。

1.1.6 UDP 和 TCP 的区别

UDP 不提供复杂的控制机制,利用 IP 提供面向无连接的通信服务。

UDP 协议真的非常简,头部只有 8 个字节(64 位),UDP 的头部格式如下: UDP 的头部格式

区别如下: 1. 连接 - TCP 是面向连接的,传输数据之前需要先建立连接 - UDP 不用建立连接。 2. 服务对象 - TCP 是一对一的两点服务, - UDP 是一对一、一对多、多对多的交互通信 3. 可靠性 - TCP 是可靠的,保证数据 无差错、无重复、无丢失、按序到达 - UDP 则尽最大可能交付,不保证可靠交付数据。 4. 拥塞控制、流量控制 - TCP 使用拥塞控制机制和流量控制机制 控制传输速度 - UDP 则没有流量控制机制,网络状况不影响发送速率 5. 传输方式 - TCP 是没有边界的流式传输,但是保证可靠和顺序 - UDP 是一个包一个包发送的,是有边界的,但可能出现丢包和乱序 6. 分片机制 - TCP 数据如果超过了 MSS(Maximum Segment Size,最大分片大小) ,则会在传输层进行分片,,目标主机收到后,也同样在传输层组装 TCP 数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片。 - UDP 数据超过 MTU (Maximum Transmission Unit,最大传输单元) 时,会在IP层进行分片,到达目标主机时在网络层进行组装,接着再传给传输层。 - 通常情况下,MSS 的值会小于或等于 MTU 的值。这是因为在数据链路层以太网中,MTU 通常为 1500 字节(不包括头部和尾部),而在 TCP 协议中,MSS 的典型值为 1460 字节(减去 TCP 和 IP 头部的长度)。

1.1.6.1 为什么 UDP 头部没有「首部长度」字段,而 TCP 头部有「首部长度」字段呢

因为UDP头部是固定的8个字节,TCP头部有可变长的选项字段。

1.1.6.2 为什么 UDP 头部有「包长度」字段,而 TCP 头部则没有「包长度」字段呢?

1.1.7 TCP的包长度

可以通过计算得出:

TCP包长=IP包总长度-IP包头部长度-TCP包头部长度

其中 IP 总长度 和 IP 首部长度,在 IP 首部格式是已知的。TCP 首部长度,则是在 TCP 首部格式已知的,所以就可以求得 TCP 数据的长度。

1.1.8 UDP 的包长

UDP 的数据长度并不直接依赖于 IP 包的长度。

在传输过程中,UDP 协议会将应用层的数据封装在 UDP 报文中,然后将 UDP 报文放入 IP 包中进行传输。IP 包的长度(即 IP 包总长)由 IP 头部中的长度字段指定。UDP 的数据长度可以小于、等于或大于 IP 包的长度,因为 UDP 报文可以被分片处理,并且可以跨越多个 IP 包进行传输。

因此,UDP 的数据长度并不直接依赖于 IP 包的长度,而是由 UDP 报文头部中的长度字段来确定。

1.1.8.1 MSS 和 MTU

  • MSS(Maximum Segment Size,最大分段大小)是指在传输层(通常是TCP协议)中,数据包中所能携带的最大有效载荷数据量。有效载荷部分不包括 TCP 头部和 IP 头部等协议控制信息。MSS 的大小主要受到网络链路、操作系统和网络设备的限制。

  • MTU(最大传输单元)是指在数据链路层(如以太网)中单个数据包可以通过的最大长度。MTU 定义了数据链路层传输数据包的最大大小,它包括了整个数据帧的头部、有效载荷和尾部。

MSS 和 MTU 之间存在一定的关系。当使用 TCP 协议在网络上传输数据时,TCP 层会根据 MSS 的大小将数据分割为适当大小的数据段,并在传输过程中根据网络的 MTU 来进行分段重组。TCP 层会根据 MTU 的大小来调整 MSS,以确保数据段能够被正确地发送和接收。

通常情况下,MSS 的值会小于或等于 MTU 的值。这是因为在数据链路层以太网中,MTU 通常为 1500 字节(不包括头部和尾部),而在 TCP 协议中,MSS 的典型值为 1460 字节(减去 TCP 和 IP 头部的长度)。

IP 包总长指的是在网络层(如 IPv4 和 IPv6)中,IP 数据包的总长度,包括了 IP 头部和数据部分。IP 头部中的长度字段指示了整个 IP 包的长度。IP 包总长要小于或等于 MTU,否则就需要进行分片处理,将较大的 IP 包拆分成多个较小的片段进行传输。

因此,在计算 MTU 时,需要考虑整个数据帧的大小,而在 IP 数据包长度方面,则只涉及 IP 头部和有效负载的大小。

1.1.9 TCP 和 UDP 可以使用同一个端口吗

详见此文

可以。

TCP 和 UDP,在内核中是两个完全独立的软件模块。

当主机收到数据包后,可以在 IP 包头的「协议号」字段知道该数据包是 TCP/UDP,所以可以根据这个信息确定送给哪个模块(TCP/UDP)处理,送给 TCP/UDP 模块的报文根据「端口号」确定送给哪个应用程序处理。

1.2 连接建立

1.2.1 TCP 三次握手过程

  1. 客户端会随机初始化序号(client_isn),将此序号置于 TCP 首部的序列号字段中,同时把 SYN 标志位置为 1,表示 SYN 报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 SYN-SENT 状态。
  2. 服务端收到客户端的 SYN 报文后,首先服务端也随机初始化自己的序号(server_isn),将此序号填入 TCP 首部的序列号字段中,其次把 TCP 首部的确认应答号字段填入 client_isn + 1, 接着把 SYNACK 标志位置为 1。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN-RCVD状态。
  3. 客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部 ACK 标志位置为 1 ,其次确认应答号字段填入 server_isn + 1 ,最后把报文发送给服务端,这次报文可以携带客户到服务端的数据,之后客户端处于 ESTABLISHED 状态。服务端收到客户端的应答报文后,也进入 ESTABLISHED 状态。

即使服务端还是在 syn_received 状态,收到了客户端发送的数据,还是可以建立连接的,并且还可以正常收到这个数据包。这是因为数据报文中是有 ack 标识位,也有确认号,这个确认号就是确认收到了第二次握手。

1.2.2 为什么是三次握手

重要的是为什么三次握手才可以初始化 Socket、序列号和窗口大小并建立 TCP 连接。下面是三点原因:

  1. 三次握手才可以阻止重复历史连接的初始化(主要原因)
  2. 三次握手才可以同步双方的初始序列号
  3. 三次握手才可以避免资源浪费

1.2.3 1. 避免历史连接(主要原因)

1.2.3.1 三次握手的情况

客户端连续发送多次 SYN(都是同一个四元组,但序列号不同。假设第一次 序列号是 90,第二次序列号是 100。)建立连接的报文,在网络拥堵情况下:

【旧报文】->【RST】-> 【新报文】的情况:

  • 旧报文比新报文先到达,服务器回复 确认号=90+1 的报文给客户端
  • 客户端发现服务端回复的报文不是期望的 100+1 ,则发送 RST 报文给服务端
  • 服务端收到之后就会断开连接
  • 客户端的新报文到达服务端之后开始正常的三次握手流程

【旧报文】-> 【新报文】->【RST】的情况:

  • 服务端收到 旧报文 之后回复 确认号=91 的报文
  • 服务端收到 新报文之后,回复的这个 Challenge Ack 报文并不是确认收到「新 SYN 报文」的,而是上一次的 ack 确认号91
  • 客户端收到之后发现并不是期望的确认号 101,就 发送 RST 报文

1.2.3.2 两次握手的情况

  • 如果服务器没有中间状态直接进入到建立连接的状态,会导致服务器可能建立一个历史连接。服务器可以发送数据给客户端,但此时客户端还是未连接的状态,服务端只有在收到了 RST 报文之后才会断开连接。建立历史连接和发送数据都白白浪费了资源。

1.2.4 2. 同步双方初始序列号

序列号是可靠传输的一个关键因素,它的作用:

  • 接收方可以去除重复的数据;
  • 接收方可以根据数据包的序列号按序接收;
  • 可以标识发送出去的数据包中, 哪些是已经被对方收到的(通过 ACK 报文中的序列号知道);

1.2.4.1 三次握手的情况

当客户端发送携带「初始序列号」的 SYN 报文的时候,需要服务端回一个 ACK 应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。

1.2.4.2 四次握手的情况

四次握手其实也能够可靠的同步双方的初始化序号,但由于第二步和第三步可以优化成一步,所以就成了「三次握手」。

1.2.4.3 两次握手的情况

而两次握手只保证了连接发起方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。

1.2.5 3. 避免资源浪费

如果只有「两次握手」,当客户端发生的 SYN 报文在网络中阻塞,客户端没有接收到 ACK 报文,就会重新发送 SYN ,由于没有第三次握手,服务端不清楚客户端是否收到了自己回复的 ACK 报文,所以服务端每收到一个 SYN 就只能先主动建立一个连接。 如果客户端发送的 SYN 报文在网络中阻塞了,重复发送多次 SYN 报文,那么服务端在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。

1.2.5.1 重复发送 SYN 且序列号一致的情况

客户端连续发送多次 SYN 且序列号一致的情况:

  • 服务器已经收到并响应了客户端的SYN请求,但是客户端并没有收到服务器的SYN-ACK响应,于是客户端会重新发送SYN请求。这种情况下,服务器只需要重新响应客户端的SYN请求即可,不会引起任何问题。
  • 客户端发送的SYN请求是重复的,即使在之前的连接已经建立过程中,服务器已经收到并响应了该请求,但是由于某些原因(例如网络延迟等),客户端并没有收到服务器的SYN-ACK响应,于是又重新发送了同样的SYN请求。这种情况下,服务器会认为客户端试图发起一个新的连接,而不是重新打开之前的连接,因此会为这个新连接分配新的资源并关闭之前的连接,这可能会导致服务器资源的浪费。

第二种情况的解决方法:

  1. 启用SYN cookies:SYN cookies是一种技术,可以在服务器端处理SYN请求时,将一部分TCP连接的信息编码到SYN-ACK响应中,并将其发送到客户端。当客户端发送ACK响应时,服务器可以从SYN-ACK响应中解码之前的信息,以验证连接是否是合法的。如果客户端发送的SYN请求是重复的,服务器可以根据SYN cookies中的信息判断是否已经为该连接分配了资源,从而避免重复分配。这种技术可以有效地减少服务器资源的浪费,但是可能会影响性能。

  2. 使用连接复用技术:连接复用技术是一种技术,可以让多个TCP连接共享同一个端口和IP地址。例如,HTTP/1.1协议中就使用了这种技术,通过在HTTP头部中设置Connection: keep-alive,可以让客户端和服务器在同一个TCP连接上发送多个HTTP请求和响应。如果服务器检测到客户端发送的SYN请求是重复的,可以尝试使用之前建立的连接,而不是为该连接分配新的资源。这种技术可以减少服务器资源的浪费,但是需要协议的支持,并且可能会导致连接的复杂性增加。

  3. 使用防火墙和负载均衡器:防火墙和负载均衡器可以在网络层级别上过滤和管理TCP连接。例如,防火墙可以阻止重复的SYN请求到达服务器,而负载均衡器可以将重复的SYN请求转发到之前建立的连接,从而减少服务器资源的浪费。这种技术可以在网络层级别上处理问题,但是需要额外的设备和配置。

总之,处理重复的SYN请求需要采用一些技术和策略,以避免服务器资源的浪费和网络性能的降低。具体的处理方式可以根据实际情况和需求进行选择和调整。

1.2.5.2 重复 SYN 且序列号不一致的情况

客户端连续发送多次 SYN(都是同一个四元组,但序列号不同。假设第一次 序列号是 90,第二次序列号是 100。)建立连接的报文,在网络拥堵情况下:

【旧报文】->【RST】-> 【新报文】的情况: - 旧报文比新报文先到达,服务器回复 确认号=90+1 的报文给客户端 - 客户端发现服务端回复的报文不是期望的 100+1 ,则发送 RST 报文给服务端 - 服务端收到之后就会断开连接 - 客户端的新报文到达服务端之后开始正常的三次握手流程

【旧报文】-> 【新报文】->【RST】的情况:

  • 服务端收到 旧报文 之后回复 确认号=91 的报文
  • 服务端收到 新报文之后,回复的这个 ack 报文并不是确认收到「新 SYN 报文」的,而是上一次的 ack 确认号91
  • 客户端收到之后发现并不是期望的确认号 101,就 发送 RST 报文

1.2.6 为什么每次建立 TCP 连接时,初始化的序列号都要求不一样

主要原因有两个方面:

  • 为了防止历史报文被下一个相同四元组的连接接收(主要方面);
  • 为了安全性,防止黑客伪造的相同序列号的 TCP 报文被对方接收;

如果每次建立连接,客户端和服务端的初始化序列号都是一样的话,很容易出现历史报文被下一个相同四元组的连接接收的问题。例:

  • client 和 serve 建立了一个TCP连接,其中的一个数据包 A 由于网络原因被阻塞了,然后 client 超时重传了这个数据包
  • 此时serve由于断电之类的原因重启,之前建立的连接丢失了,如何serve在closed 状态收到 client 的数据包之后回复了 RST 报文断开了连接
  • 紧接着 client 和 server 建立了与上一个连接相同四元组的连接
  • 新连接建立完成后,A到达了server,其序列号又正好在server 的接收窗口内,那么该数据包就会被 server 接收,造成数据错乱

所以,每次初始化序列号不一样很大程度上能够避免历史报文被下一个相同四元组的连接接收,注意是很大程度上,并不是完全避免了(因为序列号会有回绕的问题,所以需要用时间戳的机制来判断历史报文,详细看篇:TCP 是如何避免历史报文的)。

1.2.7 既然 IP 层会分片,为什么 TCP 层还需要 MSS 呢

当 IP 层有一个超过 MTU 大小的数据(TCP 头部 + TCP 数据)要发送,那么 IP 层就要进行分片,把数据分片成若干片,保证每一个分片都小于 MTU。把一份 IP 数据报进行分片以后,由目标主机的 IP 层来进行重新组装后,再交给上一层 TCP 传输层。

当某一个 IP 分片丢失后,接收方的 IP 层就无法组装成一个完整的 TCP 报文(头部 + 数据),也就无法将数据报文送到 TCP 层,所以接收方不会响应 ACK 给发送方,因为发送方迟迟收不到 ACK 确认报文,所以会触发超时重传,就会重发整个 TCP 报文(头部 + 数据)

所以,为了达到最佳的传输效率 TCP 协议在建立连接的时候通常要协商双方的 MSS 值

1.2.8 第一次握手丢失了,会发生什么

client发送SYN 报文之后进入 syn-sent 状态,等不到服务端的syn+ack报文时会触发超时重传机制,会重传 SYN 报文,序列号都是一样的

在 Linux 里,客户端的 SYN 报文最大重传次数由 tcp_syn_retries 内核参数控制,这个参数是可以自定义的,默认值一般是 5。

每次超时的等待时间是上一次的 2 倍。总等待时长就是 2 的 n+1 次方 -1 个间隔

所以如果是重传5次的话,如果第一次等待1s,第二次就是2s,五次就是等待 1+2+4+8+16+32 = 63s

如果还没收到client就会断开连接。

1.2.9 第二次握手丢失了,会发生什么

当服务端收到客户端的第一次握手后,就会回 SYN-ACK 报文给客户端,这个就是第二次握手,此时服务端会进入 SYN_RCVD 状态。

  • 因为 client 没有收到第二次握手,所以client会持续重传 SYN 报文,超过总时长之后断开连接
  • 因为 server 收不到client 的第三次握手,所以server 也会触发超时重传机制,持续重传 SYN_ACK 报文,超过总时长之后断开连接

在 Linux 里,客户端的 SYN 报文最大重传次数由 tcp_syn_retries 内核参数控制,这个参数是可以自定义的,默认值一般是 5。 在 Linux 下,服务端的 SYN-ACK 报文的最大重传次数由 tcp_synack_retries 内核参数决定,默认值是 5。 和 client 超时等待机制一样,每次重试之间等待的时间间隔也是上次的两倍。

1.2.10 第三次握手丢失了,会发生什么

收到 server 的第二次握手之后,client 向server 发送 ACK 报文。client进入 ESTABLISHED 状态,server处于 SYN-SENT 状态。

1.2.10.1 服务端的情况

由于server迟迟收不到第三次握手,而ACK 报文是不会重传的,就会重发 SYN-ACK 报文,直到收到 ACK 报文建立连接 或 超过总时长之后断开连接。

在 Linux 下,服务端的 SYN-ACK 报文的最大重传次数由 tcp_synack_retries 内核参数决定,默认值是 5。 每次等待时长为上次的两倍。

1.2.10.2 客户端的情况
  • 如果客户端没发送数据包,一直处于 ESTABLISHED 状态,会启动TCP保活机制,然后经过 2 小时 11 分 15 秒才可以发现一个「死亡」连接,于是客户端连接就会断开连接。
  • 如果客户端发送了数据包,一直没有收到服务端对该数据包的确认报文,则会一直重传该数据包,直到重传次数超过 tcp_retries2 值(默认值 15 次)后,客户端就会断开 TCP 连接。

1.2.11 TCP 半连接和全连接队列

在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是:

  • 半连接队列,也称 SYN 队列
  • 全连接队列,也称 Accpet 队列

不管是半连接队列还是全连接队列,都有最大长度限制,超过限制时,默认情况都会丢弃报文。

正常连接流程:

  1. server 收到 SYN 报文,建立半连接对象存放到内核的 SYN 队列
  2. server 发送 SYN-ACK 报文
  3. server 收到 ACK 报文,从 SYN 队列取出半连接对象,创建新的全连接对象存放到 accept 队列
  4. 应用程序调用 accept() socket 接口从 accept 队列取出全连接对象

1.2.12 什么是 SYN 攻击?如何避免

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

当 TCP 半连接队列满了,后续再在收到 SYN 报文就会丢弃,导致客户端无法和服务端建立连接。

避免 SYN 攻击方式,可以有以下四种方法:

  • 调大 netdev_max_backlog;
  • 增大 TCP 半连接队列;
  • 开启 tcp_syncookies;
  • 减少 SYN+ACK 重传次数

1.2.12.1 1

  1. 调大 netdev_max_backlog 当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包。控制该队列的最大值如下参数,默认值是 1000,我们要适当调大该参数的值,比如设置为 10000:
    net.core.netdev_max_backlog = 10000
    

1.2.12.2 2

  1. 增大 TCP 半连接队列 增大 TCP 半连接队列,要同时增大下面这三个参数:

  2. 增大 net.ipv4.tcp_max_syn_backlog

  3. 增大 net.core.somaxconn
  4. 增大 listen() 函数中的 backlog

前两个参数是内核参数,后一个参数是在Web服务器(Nginx等)中设置的。

详情参考:TCP 半连接队列和全连接队列满了会发生什么?又该如何应对?

1.2.12.3 3

  1. 开启 net.ipv4.tcp_syncookies

  2. 当 「 SYN 队列」满之后,后续服务端收到 SYN 包,不会丢弃,而是根据算法,计算出一个 cookie 值(它是通过通信双方的 IP 地址端口、时间戳、MSS等信息进行实时计算的);

  3. 将 cookie 值放到第二次握手报头的「序列号」里,然后服务端回第二次握手给客户端;
  4. 服务端接收到客户端的应答报文时,服务端会检查这个 ACK 包的合法性。如果合法,将该连接对象放入到「 Accept 队列」。
  5. 最后应用程序通过调用 accpet() 接口,从「 Accept 队列」取出的连接。

net.ipv4.tcp_syncookies 参数主要有以下三个值:

  • 0 值,表示关闭该功能;
  • 1 值,表示仅当 SYN 半连接队列放不下时,再启用它;
  • 2 值,表示无条件开启功能; 那么在应对 SYN 攻击时,只需要设置为 1 即可。
    $ echo 1 > /proc/sys/net/ipv4/tcp_syncookies
    

cookies 方案为什么不直接取代半连接队列?

cookies 方案虽然能防 SYN Flood 攻击,但是也有一些问题。因为服务端并不会保存连接信息,所以如果传输过程中数据包丢了,也不会重发第二次握手的信息(因为在使用 TCP SYN cookies 机制时,服务器不会保持与客户端的连接状态,没有存放到半连接队列里自然也不会超时重发了)。

另外,编码解码 cookies,都是比较耗 CPU 的,利用这一点,如果此时攻击者构造大量的第三次握手包(ACK 包),同时带上各种瞎编的 cookies 信息,服务端收到 ACK 包后以为是正经 cookies,憨憨地跑去解码(耗 CPU),最后发现不是正经数据包后才丢弃。

这种通过构造大量 ACK 包去消耗服务端资源的攻击,叫 ACK 攻击,受到攻击的服务器可能会因为 CPU 资源耗尽导致没能响应正经请求。

1.2.12.4 4

  1. 减少 SYN+ACK 重传次数 减少 SYN-ACK 的重传次数,以加快处于 SYN_REVC 状态的 TCP 连接断开。
    $ echo 2 > /proc/sys/net/ipv4/tcp_synack_retries
    

1.3 连接断开

1.3.1 TCP 四次挥手过程

连接双方都可以主动断开连接,下文如无特殊说明则都是默认client主动断开连接。

四次挥手

  1. Client 打算关闭连接,发送一个 首部 标志位 FIN=1 的报文给 Server,进入 FIN_WAIT_1 的状态
  2. Server 收到后回应一个 ACK 报文,之后进入 CLOSE_WAIT 状态
  3. Client 收到 ACK 报文后进入 FIN_WAIT_2 的状态,主动方的发送通道就关闭了。
  4. Server 等待应用进程处理完数据之后,也向 Client发送一个FIN报文,之后进入 LAST_ACK状态
  5. Client 收到后回应一个ACK报文,之后进入 TIME_WAIT 状态
  6. Server 接收到后进入CLOSE状态,至此 被动方 完成连接的关闭
  7. Client 在等待 2MLS 之后自动进入CLOSE状态,至此 Client 完成连接的关闭。在 Linux 系统下大约等待 1 分钟后,TIME_WAIT 状态的连接才会彻底关闭。

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

CLOSING:这个状态是一个比较特殊的状态,也比较少见,正常情况下不会出现,但是当双方同时都作为主动的一方,调用 close() 关闭连接的时候,两边都进入FIN_WAIT_1 的状态,此时期望收到的是ACK包,进入 FIN_WAIT_2 的状态,但是却先收到了对方的FIN包,这个时候,就会进入到 CLOSING 的状态,然后给对方一个ACK,接收到 ACK 后直接进入到 CLOSED 状态。

1.3.2 为什么需要四次挥手

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

从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK 和 FIN 一般都会分开发送,因此是需要四次挥手。

但是在特定情况下,四次挥手是可以变成三次挥手的,参考:TCP 四次挥手,可以变成三次吗?

1.3.3 第一次挥手丢失了,会发生什么

当客户端(主动关闭方)调用 close 函数(无法再发送和接收数据)后,就会向服务端发送 FIN 报文,试图与服务端断开连接,此时客户端的连接进入到 FIN_WAIT_1 状态。

如果客户端迟迟收不到被动方的 ACK 的话,也就会触发超时重传机制,重传 FIN 报文,重发次数由 tcp_orphan_retries 参数控制。 每次重试的等待时间为上一次超时时间的 2 倍。如果还是没能收到第二次挥手,那么直接进入到 CLOSE 状态。

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

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

如果服务端的第二次挥手丢失了:

  • 客户端就会触发超时重传机制,重传 FIN 报文,直到收到服务端的第二次挥手;或者达到最大的重传次数,再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到服务端的第二次挥手(ACK 报文),那么客户端就会断开连接。。
  • ACK 报文是不会重传的

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

1.3.5.1  Server

Server (被动关闭方)收到 Client (主动关闭方)的 FIN 报文后,内核会自动回复 ACK,同时连接处于 CLOSE_WAIT 状态。

顾名思义,它表示等待应用进程调用 close 函数关闭连接。此时,内核是没有权利代替进程关闭连接的,必须等待进程调用 close 函数触发 Server 发生FIN报文。

服务端处于 CLOSE_WAIT 状态时,调用了 close 函数,内核就会发出 FIN 报文,同时连接进入 LAST_ACK 状态,等待客户端返回 ACK 来确认连接关闭。

Server 由于迟迟收不到 Client 的 ACK 报文,就会重发 FIN 报文。重发次数仍然由 tcp_orphan_retries 参数控制,与 Client 一致。如果最后还是没能收到客户端的第四次挥手(ACK报文),那么服务端就会断开连接。

1.3.5.2  Client

Client 接收到 Server 的ACK报文后,进入 FIN_WAIT2 状态。这里 Client 的处理要分情况讨论,看 Client 是调用的 close 还是 shutdown 函数。

1.3.6 1. 调用 close 函数关闭

对于 close 函数关闭的连接,由于无法再发送和接收数据,所以 FIN_WAIT2 状态不可以持续太久,而 tcp_fin_timeout 控制了这个状态下连接的持续时长,默认值是 60 秒。

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

1.3.7 2. 调用 shutdown 函数关闭

如果主动关闭方使用 shutdown 函数关闭连接,指定了只关闭发送方向,而接收方向并没有关闭,那么意味着主动关闭方还是可以接收数据的。

此时,如果主动关闭方一直没收到第三次挥手,那么主动关闭方的连接将会一直处于 FIN_WAIT2 状态(tcp_fin_timeout 无法控制 shutdown 关闭的连接)。如下图:

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

当客户端收到服务端的第三次挥手的 FIN 报文后,就会回 ACK 报文,也就是第四次挥手,此时客户端连接进入 TIME_WAIT 状态。此时 Server 处于LAST_ACK 状态。

在 Linux 系统,TIME_WAIT 状态会持续 2MSL 后才会进入关闭状态。

  • 如果第四次挥手的 ACK 报文没有到达服务端,服务端就会重发 FIN 报文,重发次数仍然由前面介绍过的 tcp_orphan_retries 参数控制。如果还是没能收到客户端的第四次挥手(ACK 报文),那么服务端就会断开连接。
  • Client 每收到一次 FIN 报文,就重置一次 2MLS 定时器,当等待 2MSL 时长后,客户端就会断开连接。

1.3.9 TIME_WAIT

1.3.9.1 为什么 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 倍的时间

比如,如果被动关闭方没有收到断开连接的最后的 ACK 报文,就会触发超时重发 FIN 报文,另一方接收到 FIN 后,会重发 ACK 给被动关闭方, 一来一去正好 2 个 MSL。

可以看到 2MSL时长 这其实是相当于至少允许报文丢失一次。比如,若 ACK 在一个 MSL 内丢失,被动方重发的 FIN 报文会在第二个 MSL 内到达,TIME_WAIT 状态可以处理这种情况。

在 Linux 系统里 2MSL 默认是 60 秒,那么一个 MSL 也就是 30 秒。Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒。

1.3.9.1.1 为什么不是更长时间

为什么不是 4 或者 8 MSL 的时长呢?你可以想象一个丢包率达到百分之一的糟糕网络,连续两次丢包的概率只有万分之一,这个概率实在是太小了,忽略它比解决它更具性价比。

1.3.9.2 为什么需要 TIME_WAIT 状态

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

1.3.9.3 防止历史连接中的数据,被后面相同四元组的连接错误的接收

  • 序列号是一个 32 位的无符号数,因此在到达 4G 之后再循环回到 0。
  • 初始序列号,在 TCP 建立连接的时候,客户端和服务端都会各自生成一个初始序列号,它是基于时钟生成的一个随机数,来保证每个连接都拥有不同的初始序列号。初始化序列号可被视为一个 32 位的计数器,该计数器的数值每 4 微秒加 1,循环一次需要 4.55 小时。

序列号和初始化序列号并不是无限递增的,会发生回绕为初始值的情况,这意味着无法根据序列号来判断新老数据。

假设 TIME-WAIT 没有等待时间或时间过短,服务端以相同的四元组重新打开了新连接,被延迟的数据包抵达后又正好处于接收窗口内,那么就会造成数据错乱。

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

1.3.9.3.1 2.

1.3.9.4 保证 被动关闭连接 的一方,能被正确的关闭

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

如果没有 TIME_WAIT 状态或者过短的话, Server 重发的 FIN 包就会被 Client 回复 RST 报文(此时 Client 已处于 close 状态),服务端收到这个 RST 并将其解释为一个错误(Connection reset by peer),这对于一个可靠的协议来说不是一个优雅的终止方式。

1.3.9.5 TIME_WAIT 过多有什么危害

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

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

1.3.9.6 客户端(主动发起关闭连接方)的 TIME_WAIT 状态过多

如果占满了所有端口资源,那么就无法对目的 IP+ 目的 PORT都一样的服务端发起连接了。

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

1.3.9.7 服务端(主动发起关闭连接方)的 TIME_WAIT 状态过多

不会导致端口资源受限,因为服务端只监听一个端口,而且由于一个四元组唯一确定一个 TCP 连接,因此理论上服务端可以建立很多连接,但是 TCP 连接过多,会占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等。

1.3.9.8 如何优化 TIME_WAIT

三种方法,都是利弊共存:

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

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

1.3.9.8.1 1.
  1. 打开 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 内核参数选项

如此可以复用处于 TIME_WAIT 的 socket 为新的连接所用。

net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_timestamps=1(默认即为 1

tcp_tw_reuse 功能只能用客户端(连接发起方),因为开启了该功能,在调用 connect() 函数时,内核会随机找一个 time_wait 状态超过 1 秒的连接给新的连接复用。

所以就需要有一种机制判断新旧数据包,防止数据错乱。

这个时间戳的字段是在 TCP 头部的选项里,它由一共 8 个字节表示时间戳,其中第一个 4 字节字段用来保存发送该数据包的时间,第二个 4 字节字段用来保存最近一次接收对方发送到达数据的时间。

由于引入了时间戳,我们在前面提到的 2MSL 问题就不复存在了,因为重复的数据包会因为时间戳过期被自然丢弃。

1.3.9.8.2 2.
  1. net.ipv4.tcp_max_tw_buckets

这个值默认为 18000,当系统中处于 TIME_WAIT 的连接一旦超过这个值时,系统就会将后面的 TIME_WAIT 连接状态重置(即不等待 2MSL ,直接关闭连接),这个方法比较暴力。

超过设定值时, TCP 连接可能会遇到以下情况:

  • 连接被拒绝:系统无法为新的连接分配 TIME_WAIT 状态所需的资源,因此可能会拒绝新的连接请求。这可能会导致客户端连接超时或出现连接失败的错误。
  • 连接被立即关闭:由于无法为新连接分配足够的 TIME_WAIT 资源,系统可能会立即关闭新的连接,而不是将其置于 TIME_WAIT 状态。这样做可能会增加连接重置(RST)包的数量,因为另一端可能仍然认为连接处于活动状态。
  • 旧连接被快速清理:当出现超过设定值的 TIME_WAIT 连接时,系统可能会优先清理较早的 TIME_WAIT 连接,以为新连接腾出资源。这可能会导致 TIME_WAIT 状态较短,从而增加连接重置(RST)包的可能性。
1.3.9.8.3 3.
  1. 程序中使用 SO_LINGER 可以通过设置 socket 选项,来设置调用 close 关闭连接行为:
    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));
    
    如果l_onoff为非 0, 且l_linger值为 0,那么调用close后,会立该发送一个RST标志给对端,该 TCP 连接将跳过四次挥手,也就跳过了TIME_WAIT状态,直接关闭。

但这为跨越TIME_WAIT状态提供了一个可能,不过是一个非常危险的行为,不值得提倡。

1.3.9.9 服务器出现大量 TIME_WAIT 状态的原因有哪些

服务端出现大量的 TIME_WAIT 说明是服务端主动断开了连接。以下场景会导致这种主动断开的行为:

  • HTTP 没有使用长连接
  • HTTP 长连接超时
  • HTTP 长连接的请求数量达到上限
1.3.9.9.1 1.

1.3.9.10 HTTP 没有使用长连接(Keep-Alive)

先说结论:客户端和服务端双方有任意一方没有开启 HTTP 长连接,导致 Server 主动断开连接。

长连接机制 在 HTTP/1.0 中默认是关闭的,若要开启双方必须在请求头和响应头指定 Connection: Keep-Alive。 这样做,TCP 连接就不会中断,而是保持连接。当客户端发送另一个请求时,它会使用同一个 TCP 连接。这一直到客户端或服务器端提出断开连接。

从 HTTP/1.1 开始, 就默认是开启了 Keep-Alive。如果要关闭 HTTP Keep-Alive,只要客户端和服务端任意一方的 HTTP header 中有 Connection: close 信息,那么就无法使用 HTTP 长连接的机制。

关闭 HTTP 长连接机制后,每次请求都要经历这样的过程:建立 TCP连接 -> 请求资源 -> 响应资源 -> 释放TCP连接,那么此方式就是 HTTP 短连接

在 RFC 文档中,并没有明确由谁来关闭连接,请求和响应的双方都可以主动关闭 TCP 连接。

不过,根据大多数 Web 服务的实现,不管哪一方禁用了 HTTP Keep-Alive,都是由服务端主动关闭连接。

1.3.9.10.1 客户端禁用,服务端开启的情况

HTTP Keep-Alive 的初衷是为客户端后续的请求重用连接,如果我们在某次 HTTP 请求-响应模型中,请求的 header 定义了 connection:close 信息,那不再重用这个连接的时机就只有在服务端了

1.3.9.10.2 客户端开启,服务端禁用的情况

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

1.3.9.10.1 2.

1.3.9.11 HTTP 长连接超时

为了避免资源浪费的情况,web 服务软件一般都会提供一个参数,用来指定 HTTP 长连接的超时时间,比如 nginx 提供的 keepalive_timeout 参数。

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

如果现象是有大量的客户端建立完 TCP 连接后,很长一段时间没有发送数据,那么大概率就是因为 HTTP 长连接超时。

可以往网络问题的方向排查,比如是否是因为网络问题,导致客户端发送的数据一直没有被服务端接收到,以至于 HTTP 长连接超时。

1.3.9.11.1 3.

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

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

比如 nginx 的 keepalive_requests 这个参数(默认值是 100 ),这个参数是指一个 HTTP 长连接建立之后,nginx 就会为这个连接设置一个计数器,记录这个 HTTP 长连接上已经接收并处理的客户端请求的数量。如果达到这个参数设置的最大值时,则 nginx 会主动关闭这个长连接,那么此时服务端上就会出现 TIME_WAIT 状态的连接。

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

1.3.10 服务器出现大量 CLOSE_WAIT 状态的原因有哪些

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

结论:

  • 服务器负载过高:如果服务器的负载过高,可能会导致处理关闭连接的请求的速度跟不上,从而使得大量的连接保持在 CLOSE_WAIT 状态。
  • 应用程序未正确关闭连接:这是最常见的原因。服务端程序可能存在设计上的问题,未正确地关闭连接。这可能是由于程序没有正确调用关闭连接的操作,或者存在资源释放不完整的情况,导致连接无法正常关闭并进入 CLOSE_WAIT 状态。

下面一个普通的 TCP 服务端的流程:

  1. 创建服务端 socket,bind 绑定端口、listen 监听端口
  2. 将服务端 socket 注册到 epoll
  3. epoll_wait 等待连接到来,连接到来时,调用 accpet 获取已连接的 socket
  4. 将已连接的 socket 注册到 epoll
  5. epoll_wait 等待事件发生
  6. 对方连接关闭时,我方调用 close

可能导致服务端没有调用 close 函数的原因,如下。

  • 第一个原因:第 2 步没有做,没有将服务端 socket 注册到 epoll,这样有新连接到来时,服务端没办法感知这个事件,也就无法获取到已连接的 socket,那服务端自然就没机会对 socket 调用 close 函数了。

不过这种原因发生的概率比较小,这种属于明显的代码逻辑 bug,在前期 read view 阶段就能发现的了。

  • 第二个原因: 第 3 步没有做,有新连接到来时没有调用 accpet 获取该连接的 socket,导致当有大量的客户端主动断开了连接,而服务端没机会对这些 socket 调用 close 函数,从而导致服务端出现大量 CLOSE_WAIT 状态的连接。

发生这种情况可能是因为服务端在执行 accpet 函数之前,代码卡在某一个逻辑或者提前抛出了异常。

  • 第三个原因:第 4 步没有做,通过 accpet 获取已连接的 socket 后,没有将其注册到 epoll,导致后续收到 FIN 报文的时候,服务端没办法感知这个事件,那服务端就没机会调用 close 函数了。

发生这种情况可能是因为服务端在将已连接的 socket 注册到 epoll 之前,代码卡在某一个逻辑或者提前抛出了异常。之前看到过别人解决 close_wait 问题的实践文章,感兴趣的可以看看:一次 Netty 代码不健壮导致的大量 CLOSE_WAIT 连接原因分析(opens new window)

  • 第四个原因:第 6 步没有做,当发现客户端关闭连接后,服务端没有执行 close 函数,可能是因为代码漏处理,或者是在执行 close 函数之前,代码卡在某一个逻辑,比如发生死锁等等。

1.3.11 TCP 保活机制

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

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

  • tcp_keepalive_time=7200:表示保活时间是 7200 秒(2小时),也就 2 小时内如果没有任何连接相关的活动,则会启动保活机制
  • tcp_keepalive_intvl=75:表示每次检测间隔 75 秒;
  • tcp_keepalive_probes=9:表示检测 9 次无响应,认为对方是不可达的,从而中断本次的连接。
TCP保活时间 = tcp_keepalive_time + (tcp_keepalive_intvl * tcp_keepalive_probes)

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

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

  1. 对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。

  2. 对端主机宕机并重启。当 TCP 保活的探测报文发送给对端后,对端是可以响应的,但由于没有该连接的有效信息,会产生一个 RST 报文,这样很快就会发现 TCP 连接已经被重置。

  3. 对端主机宕机(注意不是进程崩溃,进程崩溃后操作系统在回收进程资源的时候,会发送 FIN 报文,而主机宕机则是无法感知的,所以需要 TCP 保活机制来探测对方是不是发生了主机宕机),或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡

TCP 保活的这个机制检测的时间有点长,我们可以自己在应用层实现一个心跳机制。比如,web 服务软件一般都会提供 keepalive_timeout 参数,用来指定 HTTP 长连接的超时时间。

1.3.12 如果已经建立了连接,但是服务端的进程崩溃会发生什么

TCP 的连接信息是由内核维护的,所以当服务端的进程崩溃后,内核需要回收该进程的所有 TCP 连接资源,于是内核会发送第一次挥手 FIN 报文,后续的挥手过程也都是在内核完成,并不需要进程的参与,所以即使服务端的进程退出了,还是能与客户端完成 TCP 四次挥手的过程。

1.4 socket编程

1.4.1 针对 TCP 应该如何 Socket 编程

  • 客户端和服务端初始化socket,获取文件描述符
  • 服务端调用bind,将 socket 绑定IP地址和端口
  • 服务端调用listen监听连接
  • 服务端调用accept等待新的连接
  • 客户端调用connect向服务端的IP地址和端口发起连接请求
  • 服务端accep返回用于传输的socket的文件描述符
  • 客户端调用 write 写入数据;服务端调用 read 读取数据
  • 客户端断开连接时,会调用 close,那么服务端 read 读取数据的时候,就会读取到了 EOF,待处理完数据后,服务端调用 close,表示连接关闭。

监听的 socket 和真正用来传送数据的 socket,是「两个」 socket,一个叫作监听 socket,一个叫作已完成连接 socket

1.4.2 listen 时候参数 backlog 的意义

Linux内核中会维护两个队列:

  • 半连接队列(SYN 队列):接收到一个 SYN 建立连接请求,处于 SYN_RCVD 状态;
  • 全连接队列(Accpet 队列):已完成 TCP 三次握手过程,处于 ESTABLISHED 状态;

int listen (int socketfd, int backlog)
  • 参数一 socketfd 为 socketfd 文件描述符
  • 参数二 backlog,这参数在历史版本有一定的变化 在早期 Linux 内核 backlog 是 SYN 队列大小,也就是未完成的队列大小。

在 Linux 内核 2.2 之后,backlog 变成 accept 队列,也就是已完成连接建立的队列长度,所以现在通常认为 backlog 是 accept 队列。

但是上限值是内核参数 somaxconn 的大小,也就说 accpet 队列长度 = min(backlog, somaxconn)

想详细了解 TCP 半连接队列和全连接队列,可以看这篇:TCP 半连接队列和全连接队列满了会发生什么?又该如何应对?

1.4.3 accept 发生在三次握手的哪一步

客户端 connect 成功返回是在第二次握手,服务端 accept 成功返回是在三次握手成功之后。

1.4.4 客户端调用 close 了,连接断开的流程

  1. 客户端调用 close,表明客户端没有数据需要发送了,则此时会向服务端发送 FIN 报文,进入 FIN_WAIT_1 状态;
  2. 服务端接收到了 FIN 报文,TCP 协议栈会为 FIN 包插入一个文件结束符 EOF 到接收缓冲区中,应用程序可以通过 read 调用来感知这个 FIN 包。这个 EOF 会被放在已排队等候的其他已接收的数据之后,这就意味着服务端需要处理这种异常情况,因为 EOF 表示在该连接上再无额外数据到达。此时,服务端进入 CLOSE_WAIT 状态;
  3. 接着,当处理完数据后,自然就会读到 EOF,于是也调用 close 关闭它的套接字,这会使得服务端发出一个 FIN 包,之后处于 LAST_ACK 状态;
  4. 客户端接收到服务端的 FIN 包,并发送 ACK 确认包给服务端,此时客户端将进入 TIME_WAIT 状态;
  5. 服务端收到 ACK 确认包后,就进入了最后的 CLOSE 状态;
  6. 客户端经过 2MSL 时间之后,也进入 CLOSE 状态;

1.4.5 没有 accept,能建立 TCP 连接吗

详见此文

accpet 系统调用并不参与 TCP 三次握手过程,它只是负责从 TCP 全连接队列取出一个已经建立连接的 socket,用户层通过 accpet 系统调用拿到了已经建立连接的 socket,就可以对该 socket 进行读写操作了。

1.4.6 没有 listen,能建立 TCP 连接吗

详见此文

客户端是可以自己连自己的形成连接(TCP自连接),也可以两个客户端同时向对方发出请求建立连接(TCP同时打开),这两个情况都有个共同点,就是没有服务端参与,也就是没有 listen,就能 TCP 建立连接。