TCP

定义

TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。 面向连接:一定是「一对一」才能连接,不能像 UDP 协议可以一个主机同时向多个主机发送消息,也就是一对多是无法做到的; 可靠的:无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端; 字节流:用户消息通过 TCP 协议传输时,消息可能会被操作系统「分组」成多个的 TCP 报文,如果接收方的程序如果不知道「消息的边界」,是无法读出一个有效的用户消息的。并且 TCP 报文是「有序的」,当「前一个」TCP 报文没有收到的时候,即使它先收到了后面的 TCP 报文,那么也不能扔给应用层去处理,同时对「重复」的 TCP 报文会自动丢弃。 用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括 Socket、序列号和窗口大小称为连接。 建立一个 TCP 连接是需要客户端与服务端达成上述三个信息的共识:

  • Socket:由 IP 地址和端口号组成

  • 序列号:用于解决乱序问题。

  • 窗口大小:用于流量控制。 TCP 四元组可以唯一的确定一个连接,四元组包括如下:

  • 源地址

  • 源端口

  • 目的地址

  • 目的端口 源地址和目的地址的字段(32 位)是在 IP 头部中,作用是通过 IP 协议发送报文给对方主机。 源端口和目的端口的字段(16 位)是在 TCP 头部中,作用是告诉 TCP 协议应该把报文发给哪个进程。 Q1:有一个 IP 的服务端监听了一个端口,它的 TCP 的最大连接数是多少? 理论值计算公式 = 客户端的ip数 * 客户端的端口数 实际上,客户端的 IP 数最多是 2 的 32 次方,客户端的端口数最多是 2 的 16 次方,所以 TCP 的最大连接数是 2 的 48 次方。 实际场景中,服务端最大并发 TCP 连接数远不能达到理论上限,会受以下因素影响:

  • 文件描述符限制,每个 TCP 连接都是一个文件,如果文件描述符被占满了,会发生 Too many open files。Linux 对可打开的文件描述符的数量分别作了三个方面的限制:

    • 系统级:当前系统可打开的最大文件数,当前系统可打开的最大数量,通过 cat /proc/sys/fs/file-max 查看;

    • 用户级:当前用户可打开的最大文件数,指定用户可打开的最大数量,通过 cat /etc/security/limits.conf 查看;

    • 程序级:单个进程可打开的最大文件数,单个进程可打开的最大数量,通过 cat /proc/sys/fs/nr_open 查看;

    • 内存大小,每个 TCP 连接在内核中都会消耗一定的内存,操作系统的内存是有限的,如果内存不足,会发生 Out of memory。

    • 网络设备性能,例如带宽、网关、路由器等硬件设备,都会限制网络的最大速度。

协议构成

TCP报文头部

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

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

控制位:

  • SYN(Synchronize Sequence Numbers)位用来同步序列号。服务器在第二次握手中发送SYN位是为了告诉客户端它的初始序列号,并要求客户端确认这一序列号。这个步骤在建立连接时是必要的,因为每一方都需要知道对方的初始序列号,以便后续的数据传输能够正确地进行。

  • ACK(Acknowledgment)位用来确认接收到的数据。在第二次握手时,服务器设置ACK位为1,并将ack字段设置为客户端的初始序列号加1(x + 1)。这是为了确认服务器已经成功接收到客户端的SYN报文,并且知道客户端的初始序列号。TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1。

  • FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。

  • RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。

  • PSH:该位为 1 时,表示接收方应该立即将这个报文段交给应用层,而不是等到缓冲区满后再交付给应用层。

  • URG:该位为 1 时,表示报文段中有紧急数据,接收方应该尽快处理这些数据。

首部长度: TCP(传输控制协议)在进行数据传输时需要使用首部(header)来携带必要的控制信息。首部长度是TCP首部的一个字段,用于指示首部的字节长度。 首部长度字段是一个4位的二进制数,它的取值范围是0-15。TCP首部的长度可以根据该字段的值来确定,它表示首部占据的32位字(4字节)的数量。 TCP首部的长度是可变的,它的最小长度是20字节(5个32位字),而最大长度是60字节(15个32位字)。首部长度的可变性允许TCP在不同的情况下携带不同数量的选项和扩展字段。 为什么TCP需要可变长度的首部呢?这是因为TCP协议需要灵活地适应不同的网络环境和需求。通过使用可变长度的首部,TCP可以在需要时添加额外的选项字段,以支持各种功能和扩展,如窗口缩放、选择性确认等。

在IP协议中,协议字段占8个位(即一个字节),用于指示封装在IP包中的上层协议。常见的协议及其协议编号如下:

  • ICMP (Internet Control Message Protocol) - 协议编号 1

  • IGMP (Internet Group Management Protocol) - 协议编号 2

  • TCP (Transmission Control Protocol) - 协议编号 6

  • UDP (User Datagram Protocol) - 协议编号 17

  • GRE (Generic Routing Encapsulation) - 协议编号 47

  • ESP (Encapsulating Security Payload) - 协议编号 50

  • AH (Authentication Header) - 协议编号 51

  • EIGRP (Enhanced Interior Gateway Routing Protocol) - 协议编号 88

  • OSPF (Open Shortest Path First) - 协议编号 89

  • SCTP (Stream Control Transmission Protocol) - 协议编号 132

存在必要性

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

与UDP区别

UDP协议

UDP 不提供复杂的控制机制,利用 IP 提供面向「无连接」的通信服务。 UDP 协议真的非常简,头部只有 8 个字节(64 位),UDP 的头部格式如下: UDP头部

  • 目标和源端口:主要是告诉 UDP 协议应该把报文发给哪个进程。

  • 包长度:该字段保存了 UDP 首部的长度跟数据的长度之和。

  • 校验和:校验和是为了提供可靠的 UDP 首部和数据而设计,防止收到在网络传输中受损的 UDP 包。

对比

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

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

  3. 可靠性 TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按序到达。 UDP 是尽最大努力交付,不保证可靠交付数据。但是我们可以基于 UDP 传输协议实现一个可靠的传输协议,比如 QUIC 协议,具体可以参见这篇文章:如何基于 UDP 协议实现可靠传输?

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

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

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

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

由于 TCP 是面向连接,能保证数据的可靠性交付,因此经常用于FTP 文件传输、HTTP / HTTPS等。 由于 UDP 面向无连接,它可以随时发送数据,再加上 UDP 本身的处理既简单又高效,因此经常用于包总量较少的通信,如 DNS 、SNMP 、视频、音频等多媒体通信、广播通信等。

TCP数据的长度 = IP总长度 - IP头部长度 - TCP头部长度

连接

三次握手

一开始,客户端和服务端都处于 CLOSE 状态。先是服务端主动监听某个端口,处于 LISTEN 状态 三次握手第一个报文

客户端会随机初始化序号(client_isn),将此序号置于 TCP 首部的「序号」字段中,同时把 SYN 标志位置为 1,表示 SYN 报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 SYN-SENT 状态。 A -> B : [SEQ = X, ACK = 0, SYN_FLAG = 1]

三次握手第二个报文 服务端收到客户端的 SYN 报文后,首先服务端也随机初始化自己的序号(server_isn),将此序号填入 TCP 首部的「序号」字段中,其次把 TCP 首部的「确认应答号」字段填入 client_isn + 1, 接着把 SYN 和 ACK 标志位置为 1。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN-RCVD 状态。 B -> A : [SEQ = Y, ACK = X+1, SYN_FLAG = 1,ACK_FLAG=1] 三次握手第三个报文 客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部 ACK 标志位置为 1 ,其次「确认应答号」字段填入 server_isn + 1 ,最后把报文发送给服务端,这次报文可以携带客户到服务端的数据,之后客户端处于 ESTABLISHED 状态。 A -> B : [SEQ = X+1, ACK = Y+1, ACK_FLAG = 1] 服务端收到客户端的应答报文后,也进入 ESTABLISHED 状态。 第三次握手是可以携带数据的,前两次握手是不可以携带数据的 一旦完成三次握手,双方都处于 ESTABLISHED 状态,此时连接就已建立完成,客户端和服务端就可以相互发送数据了。

第一次发送数据 在三次握手完成后,主机A和主机B的连接已经建立,双方已经同步了序列号和确认号。此时,主机A可以向主机B发送数据。假设主机A发送数据给主机B,数据的长度为L: A -> B : [DATA, SEQ=X + 1, ACK=Y + 1, PSH_FLAG = 1, ACK_FLAG = 1, 数据长度=L]

第一次确认数据 主机B收到数据后,发送ACK报文段,确认收到的数据。 B -> A : [ACK, SEQ=Y + 1, ACK=X + 1 + L, ACK_FLAG = 1]

序列号与确认应答号

公式一:序列号 = 上一次发送的序列号 + len(数据长度)。特殊情况,如果上一次发送的报文是 SYN 报文或者 FIN 报文,则改为 上一次发送的序列号 + 1。 公式二:确认号 = 上一次收到的报文中的序列号 + len(数据长度)。特殊情况,如果收到的是 SYN 报文或者 FIN 报文,则改为上一次收到的报文中的序列号 + 1。 序列号:在建立连接时由内核生成的随机数作为其初始值,通过 SYN 报文传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。 确认号:指下一次「期望」收到的数据的序列号,发送端收到接收方发来的 ACK 确认报文以后,就可以认为在这个序号以前的数据都已经被正常接收。用来解决丢包的问题。 **控制位:**用来标识 TCP 报文是什么类型的报文,比如是 SYN 报文、数据报文、ACK 报文,FIN 报文等。

TCP分段

TCP(Transmission Control Protocol)是一种面向连接的协议,用于在网络中传输数据。为了提高网络传输的效率和可靠性,TCP会在多种情况下进行分段(segmentation)。以下是一些常见的情况:

  1. 数据包超过MTU MTU(Maximum Transmission Unit)是网络层能够传输的最大数据包大小。如果应用层传递给TCP的数据包大于MTU,TCP会将数据包分成多个小于或等于MTU大小的段,以确保数据能够在网络上传输而不被丢弃。

  2. 应用层数据较大 当应用层发送的数据量非常大时(例如,传输一个大文件),TCP会将数据分成多个段进行传输。这是因为TCP不能一次发送非常大的数据块,而是需要将其分割成若干个小段,以便在网络中逐步传输。

  3. 窗口大小限制 TCP使用流量控制机制,具体表现为滑动窗口协议。接收方会通知发送方自己可以接收的最大数据量(窗口大小)。如果发送的数据量超过了接收方的窗口大小,发送方必须将数据分成多个段,以适应接收方的窗口限制。

  4. 网络拥塞控制 TCP还使用拥塞控制机制来避免网络拥塞。拥塞控制算法(如慢启动和拥塞避免)会动态调整发送方的发送速率。如果网络出现拥塞,发送方会减少发送的数据量,将数据分成更小的段,以降低网络负荷。

  5. 重传机制 在数据传输过程中,如果某些段没有被成功接收(例如,接收方没有收到或确认某些段),发送方会重传这些段。有时,重传的数据量也会被分成更小的段,以提高重传的成功率。

  6. 网络协议栈的实现 不同操作系统和网络设备可能对TCP的实现有所不同,有些实现可能会根据特定的优化策略进行分段,例如基于网络条件动态调整分段大小。

为什么需要三次握手

防止历史连接

考虑一个场景,客户端先发送了 SYN(seq = 90)报文,然后客户端宕机了,而且这个 SYN 报文还被网络阻塞了,服务端并没有收到,接着客户端重启后,又重新向服务端建立连接,发送了 SYN(seq = 100)报文(注意!不是重传 SYN,重传的 SYN 的序列号是一样的)。 防止历史连接 客户端连续发送多次 SYN(都是同一个四元组)建立连接的报文,在网络拥堵情况下:

  • 一个「旧 SYN 报文」比「最新的 SYN」 报文早到达了服务端,那么此时服务端就会回一个 SYN + ACK 报文给客户端,此报文中的确认号是 91(90+1)。

  • 客户端收到后,发现自己期望收到的确认号应该是 100 + 1,而不是 90 + 1,于是就会回 RST 报文。

  • 服务端收到 RST 报文后,就会释放连接。

  • 后续最新的 SYN 抵达了服务端后,客户端与服务端就可以正常的完成三次握手了。 两次握手的场景

如果采用两次握手建立 TCP 连接的场景下,服务端在向客户端发送数据前,并没有阻止掉历史连接,导致服务端建立了一个历史连接,又白白发送了数据,妥妥地浪费了服务端的资源。

要解决这种现象,最好就是在服务端发送数据前,也就是建立连接之前,要阻止掉历史连接,这样就不会造成资源浪费,而要实现这个功能,就需要三次握手。

第三次握手的ACK包丢失不会立即导致数据传输失败,但它会导致连接建立的延迟。主机A将不得不重传ACK包,等待主机B确认连接建立后,才能顺利进行数据传输。因此,第三次握手的ACK包对确保连接的正确建立和数据传输的顺利进行非常重要。

同步双方初始序列号

TCP 协议的通信双方, 都必须维护一个「序列号」, 序列号是可靠传输的一个关键因素,它的作用:

  • 接收方可以去除重复的数据;

  • 接收方可以根据数据包的序列号按序接收;

  • 可以标识发送出去的数据包中,哪些是已经被对方收到的(通过 ACK 报文中的序列号知道) 序列号在 TCP 连接中占据着非常重要的作用,所以当客户端发送携带「初始序列号」的 SYN 报文的时候,需要服务端回一个 ACK 应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。 四次握手

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

避免资源浪费

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

半连接和全半连接

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

  • 半连接队列,也称 SYN 队列;

  • 全连接队列,也称 accept 队列; 半连接与全半连接 正常流程:

  • 当服务端接收到客户端的 SYN 报文时,会创建一个半连接的对象,然后将其加入到内核的「 SYN 队列」;

  • 接着发送 SYN + ACK 给客户端,等待客户端回应 ACK 报文;

  • 服务端接收到 ACK 报文后,从「 SYN 队列」取出一个半连接对象,然后创建一个新的连接对象放入到「 Accept 队列」;

  • 应用通过调用 accpet() socket 接口,从「 Accept 队列」取出连接对象。 不管是半连接队列还是全连接队列,都有最大长度限制,超过限制时,默认情况都会丢弃报文。

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

  • 调大 netdev_max_backlog;

  • 增大 TCP 半连接队列;

  • 开启 tcp_syncookies;

  • 减少 SYN+ACK 重传次数

# 方式一:调大 netdev_max_backlog,当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包。控制该队列的最大值如下参数,默认值是 1000,我们要适当调大该参数的值,比如设置为 10000:
net.core.netdev_max_backlog = 10000
# 方式二:增大 TCP 半连接队列,增大 TCP 半连接队列,要同时增大下面这三个参数
增大 net.ipv4.tcp_max_syn_backlog
增大 listen() 函数中的 backlog
增大 net.core.somaxconn
# 方式三:开启 net.ipv4.tcp_syncookies
# 方式四:减少 SYN+ACK 重传次数

开启 syncookies 功能就可以在不使用 SYN 半连接队列的情况下成功建立连接,相当于绕过了 SYN 半连接来建立连接。 ![开启 syncookies 功能](https://cdn.jsdelivr.net/gh/zysok2023/cloudImg/blogs/picture/开启 syncookies 功能.webp)

具体过程:

  • 当 「 SYN 队列」满之后,后续服务端收到 SYN 包,不会丢弃,而是根据算法,计算出一个 cookie 值;

  • 将 cookie 值放到第二次握手报文的「序列号」里,然后服务端回第二次握手给客户端;

  • 服务端接收到客户端的应答报文时,服务端会检查这个 ACK 包的合法性。如果合法,将该连接对象放入到「 Accept 队列」。

  • 最后应用程序通过调用 accpet() 接口,从「 Accept 队列」取出的连接。 可以看到,当开启了 tcp_syncookies 了,即使受到 SYN 攻击而导致 SYN 队列满时,也能保证正常的连接成功建立。 net.ipv4.tcp_syncookies 参数主要有以下三个值:

  • 0 值,表示关闭该功能;

  • 1 值,表示仅当 SYN 半连接队列放不下时,再启用它;

  • 2 值,表示无条件开启功能; 那么在应对 SYN 攻击时,只需要设置为 1 即可。

当服务端受到 SYN 攻击时,就会有大量处于 SYN_REVC 状态的 TCP 连接,处于这个状态的 TCP 会重传 SYN+ACK ,当重传超过次数达到上限后,就会断开连接。

那么针对 SYN 攻击的场景,我们可以减少 SYN-ACK 的重传次数,以加快处于 SYN_REVC 状态的 TCP 连接断开。 SYN-ACK 报文的最大重传次数由 tcp_synack_retries内核参数决定(默认值是 5 次),比如将 tcp_synack_retries 减少到 2 次:

四次挥手

双方都可以主动断开连接,断开连接后主机中的「资源」将被释放,四次挥手的过程如下图: 四次挥手过程

  • 客户端打算关闭连接,此时会发送一个 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 状态,至此客户端也完成连接的关闭。

Q&A

Q1:如何在 Linux 系统中查看 TCP 状态? TCP 的连接状态查看,在 Linux 可以通过 netstat -napt 命令查看。

Q2:为什么是三次握手?不是两次、四次?

  • 三次握手才可以阻止重复历史连接的初始化(主要原因)

  • 三次握手才可以同步双方的初始序列号

  • 三次握手才可以避免资源浪费

Q3:为什么每次建立 TCP 连接时,初始化的序列号都要求不一样呢 主要原因有两个方面:

  • 为了防止历史报文被下一个相同四元组的连接接收(主要方面);

  • 为了安全性,防止黑客伪造的相同序列号的 TCP 报文被对方接收; 如果每次建立连接,客户端和服务端的初始化序列号都是一样的话,很容易出现历史报文被下一个相同四元组的连接接收的问题。 每次初始化序列号不一样很大程度上能够避免历史报文被下一个相同四元组的连接接收,注意是很大程度上,并不是完全避免了(因为序列号会有回绕的问题,所以需要用时间戳的机制来判断历史报文

Q4:初始序列号 ISN 是如何随机产生的? 起始 ISN 是基于时钟的,每 4 微秒 + 1,转一圈要 4.55 个小时。 RFC793 提到初始化序列号 ISN 随机生成算法:ISN = M + F(localhost, localport, remotehost, remoteport)。

  • M 是一个计时器,这个计时器每隔 4 微秒加 1。

  • F 是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成一个随机数值。要保证 Hash 算法不能被外部轻易推算得出,用 MD5 算法是一个比较好的选择。 随机数是会基于时钟计时器递增的,基本不可能会随机成一样的初始化序列号。

Q5:既然 IP 层会分片,为什么 TCP 层还需要 MSS 呢 当 IP 层有一个超过 MTU 大小的数据(TCP 头部 + TCP 数据)要发送,那么 IP 层就要进行分片,把数据分片成若干片,保证每一个分片都小于 MTU。把一份 IP 数据报进行分片以后,由目标主机的 IP 层来进行重新组装后,再交给上一层 TCP 传输层。 那么当如果一个 IP 分片丢失,整个 IP 报文的所有分片都得重传 因为 IP 层本身没有超时重传机制,它由传输层的 TCP 来负责超时和重传。 当某一个 IP 分片丢失后,接收方的 IP 层就无法组装成一个完整的 TCP 报文(头部 + 数据),也就无法将数据报文送到 TCP 层,所以接收方不会响应 ACK 给发送方,因为发送方迟迟收不到 ACK 确认报文,所以会触发超时重传,就会重发「整个 TCP 报文(头部 + 数据)」。 为了达到最佳的传输效能 TCP 协议在建立连接的时候通常要协商双方的 MSS 值,当 TCP 层发现数据超过 MSS 时,则就先会进行分片,当然由它形成的 IP 包的长度也就不会大于 MTU ,自然也就不用 IP 分片了。 经过 TCP 层分片后,如果一个 TCP 分片丢失后,进行重发时也是以 MSS 为单位,而不用重传所有的分片,大大增加了重传的效率。

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

  • TCP的超时时间是由内核参数tcp_syn_retries决定的。该参数用于指定TCP连接建立过程中SYN(同步)包的重传次数。 net.ipv4.tcp_syn_retries

  • 这个参数决定了TCP在重传数据包之前等待确认的初始超时时间。默认值是1秒。 net.ipv4.tcp_retransmit_timeout

  • 这个参数决定了TCP重传超时的最小值,默认值是200毫秒。 net.ipv4.tcp_rto_min

  • 这个参数决定了TCP重传超时的最大值,默认值是120秒 net.ipv4.tcp_rto_max 当TCP发送一个数据包后,如果在net.ipv4.tcp_retransmit_timeout指定的时间内没有收到确认,它会进行重传。重传的间隔会指数级增加,直到达到net.ipv4.tcp_rto_max的最大值。 通常,第一次超时重传是在 1 秒后,第二次超时重传是在 2 秒,第三次超时重传是在 4 秒后,第四次超时重传是在 8 秒后,第五次是在超时重传 16 秒后。没错,每次超时的时间是上一次的 2 倍。 当第五次超时重传后,会继续等待 32 秒,如果服务端仍然没有回应 ACK,客户端就不再发送 SYN 包,然后断开 TCP 连接。 所以,总耗时是 1+2+4+8+16+32=63 秒,大约 1 分钟左右。 第1次握手丢失

Q7:第二次握手丢失了,会发生什么? 当服务端收到客户端的第一次握手后,就会回 SYN-ACK 报文给客户端,这个就是第二次握手,此时服务端会进入 SYN_RCVD 状态。 第二次握手的 SYN-ACK 报文其实有两个目的 :

  • 第二次握手里的 ACK, 是对第一次握手的确认报文;

  • 第二次握手里的 SYN,是服务端发起建立 TCP 连接的报文; 因为第二次握手报文里是包含对客户端的第一次握手的 ACK 确认报文,所以,如果客户端迟迟没有收到第二次握手,那么客户端就觉得可能自己的 SYN 报文(第一次握手)丢失了,于是客户端就会触发超时重传机制,重传 SYN 报文。 然后,因为第二次握手中包含服务端的 SYN 报文,所以当客户端收到后,需要给服务端发送 ACK 确认报文(第三次握手),服务端才会认为该 SYN 报文被客户端收到了。如果第二次握手丢失了,服务端就收不到第三次握手,于是服务端这边会触发超时重传机制,重传 SYN-ACK 报文。 在 Linux 下,SYN-ACK 报文的最大重传次数由 tcp_synack_retries内核参数决定,默认值是 5

sysctl net.ipv4.tcp_synack_retries

因此,当第二次握手丢失了,客户端和服务端都会重传:

  • 客户端会重传 SYN 报文,也就是第一次握手,最大重传次数由 tcp_syn_retries内核参数决定;

  • 服务端会重传 SYN-ACK 报文,也就是第二次握手,最大重传次数由 tcp_synack_retries 内核参数决定。 第2次握手丢失

  • 当客户端超时重传 1 次 SYN 报文后,由于 tcp_syn_retries 为 1,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到服务端的第二次握手(SYN-ACK 报文),那么客户端就会断开连接。

  • 当服务端超时重传 2 次 SYN-ACK 报文后,由于 tcp_synack_retries 为 2,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到客户端的第三次握手(ACK 报文),那么服务端就会断开连接。

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

  • 当服务端超时重传 2 次 SYN-ACK 报文后,由于 tcp_synack_retries 为 2,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到客户端的第三次握手(ACK 报文),那么服务端就会断开连接。

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

Q20:为什么挥手需要四次?

  • 关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据。

  • 服务端收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。 服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK 和 FIN 一般都会分开发送,因此是需要四次挥手。