%% # 纲要 > 主干纲要、Hint/线索/路标 # Q&A #### 已明确 #### 待明确 > 当下仍存有的疑惑 **❓<font color="#c0504d"> 有什么问题?</font>** # Buffer ## 闪念 > sudden idea ## 候选资料 > Read it later %% # TCP 协议 > TCP (Transmission Control Protocol) 传输控制协议 TCP 是**面向连接的、可靠的、基于字节流**的传输层通信协议,并且提供了流量控制、拥塞控制等功能。 - **面向连接的**: TCP 需要在两个通信端点之间建立 1 对 1 的 TCP 连接。 - **可靠的**:提供可靠的数据传输服务,确保接收方能够正确、完整地接收数据。 - **基于字节流的**:将数据视为一连串**无边界的字节流**。发送和接收双方无需关心数据的具体结构或边界。 TCP 在 IP 不可靠的尽力而为的服务之上提供了一种**可靠的数据传输服务**, 确保一个进程从其接收缓存中读出的数据流是**无损坏、无间隙、非冗余、按序的**,即与发送方发出的字节流完全相同。 > [!NOTE] TCP 工作在 "传输层",在 "**不可靠的网络层 ==IP 协议==**" 之上提供 "**可靠" 的传输层通信**。 > > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-772C42F428953E20FC36558FB4A4E802.png|563]] > <br><br> # TCP 协议的核心机制 - **==TCP 连接管理==**:建立与断开 - **三次握手**: SYN、SYN-ACK、ACK; - **四次挥手**:主动方 FIN、ACK、被动方 FIN、ACK - **==滑动窗口机制==**(实现**高效传输** & **流量控制**) - **==高效传输==**: - 滑动窗口机制 - **减少小报文传输**: - 发送方:**Nagle 算法**; - 接收方:**延迟确认**、不通告小窗口; - **==可靠性传输==**(实现可靠性传输的具体机制,保证数据 **不出错、不重复、不失序、不丢失**) - 有序传输:头部**序列号**; - 差错检验:根据头部**序列号**、**校验和**字段 - 确认应答机制:**累积确认**、**选择确认**(SACK)、**D-SACK** - 重传机制:**超时重传**、**快速重传** - **==流量控制==**:基于 "滑动窗口" 机制; - **==拥塞控制==**: **慢启动**、**拥塞避免**、**快速恢复**三个阶段 > [!example] 「流量控制」与「拥塞控制」的区别 > > - 目的不同: > - 流量控制: **防止发送方发送的数据==超过接收方的处理能力==**,即 **防止接收方的==接收缓冲区溢出==**。 > - 拥塞控制: **==网络拥塞时==限制发送方的数据发送量,避免网络拥塞进一步恶化**。 > > - 机制不同: > - 实现流量控制的机制:**滑动窗口** > - 实现拥塞控制的机制:**慢启动**、**拥塞避免**、**快速重传**、**快速恢复**等算法。 > > <br><br> # TCP 报文段格式 > ![image-20240104164013804|653](_attachment/02-开发笔记/07-计算机网络/传输层/TCP%20协议.assets/IMG-TCP%20协议-1F4D943C0094958035CCC797B80DC767.png) TCP 头部中包括多个字段: | | 位数 | 说明 | | --------------------------- | --- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **序列号**<br>(Seq Num) | 32 | 该 TCP 报文段 **"首字节序号"** <br>- 即该报文段的**首个字节在被传输的整个字节数据流中的字节流编号**,位置标识。 | | **确认序列号** <br>(Ack Num) | 32 | 该 TCP 报文段的源端 "**期望从目的端收到的下一字节的序号**"。 | | **首部长度**<br>(Header Length) | 4 | 指示 "**TCP 头部**" 的长度(单位:**==4 字节==**)。 <br>- 由于 TCP 头部中的 "选项(Option)" 字段是可选的,**长度可变**,因此整个 TCP 头部的长度不固定,需要由该字段指示。 | | **状态位 / 标志** <br>(flag) | 6 | - `URG`: 为 1 表示 **"紧急指针"字段** 有效,即报文中包含被发送端上层实体标记为"紧急"的数据; <br>- `ACK`:为 1 表示 **"确认号"字段有效**; (即该报文段包括一个对已被成功接收的报文段的确认) <br>- `PSH`:为 1 表示当前报文段需要请求推(push) 操作,即**接收方应立即将数据交给上层**; <br>- `RST`:为 1 表示**复位/重置 TCP 连接** <br>- `SYN`:为 1 表示请**求建立连接**,且**同步序号** <br>- `FIN`:释放 TCP 连接时标识 **==发送方==比特流结束** | | **窗口大小** | 16 | 表示**源端主机**在请求接收端等待确认之前需要接收的字节数。 <br>用于流量控制,标识**接收方还可接收的字节量**。窗口大小根据网络拥塞情况和资源可用性进行增减。 | | **校验和** | 16 | 用于检查 TCP 数据包头和数据的一致性。 | | 紧急指针 | 16 | 用于指示 **紧急数据在数据流中的"结束"位置**,字段值是一个"偏移量",从当前报文段的序列号开始计算。 <br>- 该字段与TCP头部的 `URG` 标志位一起工作,用于实现 TCP 的紧急模式。 | | **选项** <br>(options) | | 可变,长度不固定但为 8 位的整数倍,**最大 40 字节**。<br><br>用于发送方与接收方协商最大报文段长度(MSS)时,或者在高速网络环境下用作窗口调节因子时使用。<br>- **SACK 选项**:用于实现选择确认机制。<br>- **MSS 选项**:确认最大报文段大小(仅 TCP 有效数据载荷,不含头部)<br>- 窗口缩放选项:用于扩大接收窗口的范围<br>- 时间戳(用于更准确地估计往返时间 RTT) | > [!NOTE] TCP 报文段长度说明 > > - **TCP ==头部==长度**: `[20, 60]` 字节 > - **无"选项"** 时 `20` 字节,如果有则为 **"4 字节"整数倍**(要求对齐),**最多 60 字节**(选项最多 40 字节) > - 每个 TCP 头部的**实际长度由 "首部长度" 字段标识**。 > - **TCP 报文段的==理论最大长度==**:**65,535 字节** > - IP 层头部的 "总长度" 字段是 16 位,最大可表示 65,535。 > - "窗口大小"字段为 16 位,即最大也只能接收 65535 字节的报文段。 > - **TCP 报文段的理论最大==有效数据载荷==长度**:`[65475, 65515]` > - 不考虑 IP 头部和任何可选 TCP 头部,则最大有效数据载荷为 **65,515 字节**(65,535 - 20); > - 考虑到最大 TCP 头部(60 字节),则最大有效载荷为**65,475**字节(65,535 - 60) > > **在实际应用中,TCP 报文段的实际长度通常远远小于理论值,根据 MSS,以太网中 TCP 报文段的最大有效载荷为 1460 字节**。 > 这一限制是为了避免 TCP 报文段在网络层中因超过 MTU 而被分片,从而提高数据传输的效率。 > > 在一个网络层的 IP 数据报中,**TCP 的有效数据载荷 = IP 数据报总长度 - IP 头部长度 - TCP 头部长度** > > [!info] `PSH` 与 `URG` 标志位 > > 在现代TCP的实际应用中,**`PSH`、`URG` 和 "紧急指针"字段未被使用**,而是通过其它机制(如设置优先级标记或使用不同的通信通道)来处理高优先级的数据。 <br> ### TCP 头部序列号 > 序列号的作用:实现 **有序传输** & **差错检验**。 TCP 发送方基于**初始序列号**,为发送数据中每个字节进行编号——每个 TCP 报文段的序号取为 **TCP 数据载荷中==首字节的字节流序号==**。 当接收方收到 TCP 报文时,根据**序列号**可判断: - 根据**序列号是否连续** 可知道是否丢包; - 根据**序列号的间隔** 可确认丢失的数据范围; - 根据**序列号是否过旧** 可判断是否为重传的旧报文(即序列号是否在当前接收窗口内) > [!info] 关于 "**序列号**" > > - "**序列号**" 是一个 **==32 位的无符号数==**(故存在**回绕**的情况) > - "**初始序列号**" 可视为一个 32 位的计数器,其**每 4 微秒递增 1**(约 4 个半小时回绕一次),以此实现 "**==随机化==**"。 > > TCP 连接的客户端、服务器端会 **==分别==生成自己的随机初始序列号**: > - 建立 TCP 连接时,双方**交换确认**对方的**初始序列号**; > - 后续 TCP 通信过程中,通过**双方的序列号**跟踪每一个消息: > - 一方报文中的"**确认号字段**" 为 "**预期收到的==对方下一条报文的首字节序号==**"。 > > ^c8ed8i > [!example] TCP 报文的 "序列号" 字段 > > 示例:假设 TCP 传输一个 500 000 字节的文件,即字节流总长为 500000,**TCP 连接的 MMS 为 1000 字节**,数据流的**首字节编号为 0**,则 TCP 会为该数据流构建 500 个报文段,第一个报文段的序号为 0,第二个报文段的序号为 1000,第三个报文段序号为 2000,以此类推。 > > ![image-20240104153740843|741](_attachment/02-开发笔记/07-计算机网络/传输层/TCP%20协议.assets/IMG-TCP%20协议-A765572B1B88A3B8B3364447757714AD.png) > > [!example] TCP 报文的 "确认序列号" 字段 > > > 示例一: > > 主机A 已收到主机B的序号`0~535` 的所有字节,其等待主机B的数据流中字节 `536` 及之后的所有字节,则主机A发给主机B的**下一报文段中的确认号字段将是 `536`**。 > > 示例二: > > ![image-20240105210217554|621](_attachment/02-开发笔记/07-计算机网络/传输层/TCP%20协议.assets/IMG-TCP%20协议-A4BCC2E0D1667A4905C8B5DC43E0EE07.png) > <br><br><br> # TCP 连接 **TCP 连接**(实际上即TCP Socket连接)由一个**四元组**唯一确认: `(源IP地址,源端口,目标IP地址,目标端口)` 。 在 Linux 系统上,每个 TCP 连接都是**一个 socket 套接字文件**,在 TCP**通信过程中该文件始终保持打开状态**。 > [!NOTE] TCP 四元组 > > ![image-20240104161300946|663](_attachment/02-开发笔记/07-计算机网络/传输层/TCP%20协议.assets/IMG-TCP%20协议-17C6C27C38D0F1F985D4B068CFEBE45E.png) > > 注:"TCP 数据段头部" 中不包含源 IP 地址和目标 IP 地址,IP 地址是在 "**IP 数据报头部**" 中加入的。 > [!NOTE] TCP 连接建立与断开过程 > > - 建立连接:**三次握手** > - 断开连接:**四次挥手** > > ![image-20230911145428338|343](_attachment/02-开发笔记/07-计算机网络/传输层/TCP%20协议.assets/IMG-TCP%20协议-77488257B4E29C32A417AA97E7013850.png) > <br> ## TCP 连接状态 #### 客户端状态 | 状态(6 个) | 描述 | | -------------- | ------------------------------------------ | | **CLOSED** | 关闭状态,也即初始状态,没有活跃的或挂起的连接。 | | **SYN_SENT** | 已发送连接建立请求 SYN 报文段,等待建立连接 | | **ESTABLISH** | 已建立连接 | | **FIN_WAIT_1** | 客户端主动发起关闭连接,**发送了 FIN 报文段,等待服务器的 ACK** | | **FIN_WAIT_2** | 客户端收到服务器的 ACK 后,**等待服务器端的 FIN 报文段** | | **TIME_WAIT** | 客户端回发 ACK 后,**等待足够的时间以确保服务器接收到最后一个 ACK 包** | > [!NOTE] TCP 客户端经历的典型 TCP 状态序列 > > ![image-20240107123943060|694](_attachment/02-开发笔记/07-计算机网络/传输层/TCP%20协议.assets/IMG-TCP%20协议-D25FC3926BC372489B291A397343EC06.png) > #### 服务器状态 | 状态(6 个) | 描述 | | -------------- | ------------------------------------ | | **LISTEN** | 监听中,等待来自客户端的连接请求 | | **SYN_RECV** | 服务器收到客户端的连接请求并**回应 SYN ACK 报文段** | | **ESTABLISH** | 已建立连接 | | **CLOSE_WAIT** | 服务器**收到客户端的 FIN 包**,等待本地应用程序关闭连接 | | **LAST_ACK** | 服务器**发送 FIN 包**,**等待客户端的最后一个 ACK 包** | | **CLOSED** | 连接完全关闭 | > [!NOTE] TCP 服务器经历的典型 TCP 状态序列 > > ![image-20240107134219701|728](_attachment/02-开发笔记/07-计算机网络/传输层/TCP%20协议.assets/IMG-TCP%20协议-9A9FF5AE2622FFCA8771199AA48DF9E8.png) > <br><br> ## TCP 连接建立(三次握手) TCP 连接通过 **三次握手** 机制建立: - 第一次握手:客户端发送 **SYN 请求** - TCP 客户端向服务器发送一个特殊的 **==SYN 报文段==**: - **`SYN`标志位为1,`ACK`标志位为0,"序列号" 字段为==客户端的随机初始序号==`client_isn`,无数据载荷。** - 第二次握手:服务器回应 **SYN-ACK 应答** - TCP 服务器收到客户端的连接请求后,为该 TCP 连接分配 TCP 缓存和变量 - TCP 服务器回发一个特殊的 **==SYN-ACK 报文段==**: - **`SYN` & `ACK` 标志位为1,"序列号"字段为服务器端的==随机初始序号==`server_isn`,"确认号" 字段为 `client_isn + 1`,无数据载荷。** - 第三次握手:客户端发送 **ACK 确认** (此时可携带数据) - TCP 客户端收到 SYNACK 报文段后,为 TCP 连接分配缓存和变量 - TCP 客户端回发一个确认报文: - **头部 `SYN` 标志位为0,`ACK`标志位为1,"序列号"字段为`client_isn + 1`,"确认号"字段为 `server_isn + 1`,可携带数据载荷**。 > [!example] 三次握手过程示意 > > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-47ABC49C00C15A64FA235D7DE357BED4.png|451]] > ![[02-开发笔记/07-计算机网络/传输层/TCP 协议#^d9y5vw]] > [!caution] 已处于 `ESTABLISHED` 状态的服务端,收到客户端的 **==乱序 SYN 报文==** 时,将会 **==重发上一个 ACK 报文==**,称之为 "**Challenge ACK**"。 > > **客户端**收到该 Chanllange ACK 后,发现确认号并非自己期望的,于是回发 RST 报文。 > > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-B3D5CA4CB7DF226BDA6B509A7BAFF6F6.png|543]] > <br> ### 三次握手报文形式 > ![image-20230911140652661|605](_attachment/02-开发笔记/07-计算机网络/传输层/TCP%20协议.assets/IMG-TCP%20协议-4297505650735F5B15375C257BF0A092.png) > > ![image-20230911140714044|587](_attachment/02-开发笔记/07-计算机网络/传输层/TCP%20协议.assets/IMG-TCP%20协议-5EDFA2D7F51E28878F285F487D7F4A3F.png) > > ![image-20230911140852134|572](_attachment/02-开发笔记/07-计算机网络/传输层/TCP%20协议.assets/IMG-TCP%20协议-D556C2C63F39A03B246AC45221CBA736.png) <br> ### 握手报文丢失的情况 - **第一次握手丢失**: - 客户端**超时重传 SYN 报文**,每次重传后**等待时间翻倍**。超过**最大重传次数**后,若超时还未收到**第二次握手应答**,则断开连接。 - **第二次握手丢失**:客户端、服务器端都会 **==分别进行重传==**。 - 客户端同上,由于未收到第二次握手而**重传 ==SYN 报文==**。 - **服务器端**在首次收到 SYN 报文后进入 `SYN_RECV` 状态,在该状态下同样会**超时重传 ==SYN-ACK 报文==**。 - **第三次握手丢失**: - **服务器端**同上,由于未收到 ACK 而 **==超时重传 SYN-ACK 报文==**。 - **客户端**在 `ESTABLISH` 状态下,仅当**每次收到服务器端重传的 SYN-ACK 报文**时,才会**重传 ACK**。(不会主动重传) > [!summary] > > - 第 **1、2 次**握手丢失:对客户端而言动作一样,**超时重传 SYN 报文**; > - 第 **2、3 次**握手丢失:对服务器而言动作一样,**超时重传 SYN-ACK 报文**; > > [!example] 第二次握手丢失 > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-BC0A25A3C981ADE3BCB1A0443D120AFA.png|687]] > [!example] 第三次握手丢失 > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-E1537DB7F1004E9133CC46843CAFF306.png|580]] <br><br> ### ❓TCP 连接需要三次握手的原因 **三个关键原因**: - 三次握手才能 **确认双端的==接收和发送能力==**; - 三次握手才能 **保证同步双方的==初始序列号==**; - 三次握手才能 **防止服务器端为 "==旧的连接请求==" 建立连接**,**阻止历史连接**的初始化,避免资源浪费。 ##### 原因一&二:三次握手才能确认双端的接收和发送能力 & 确保同步双方的初始序列号 - 第一次握手(客户端发送 SYN)向服务器证明了**客户端的发送能力** - 第二次握手(服务器发送 SYN-ACK)向客户端证明了**服务器的接收能力&发送能力**,且明确**服务器端已知 "客户端的初始序列号"**; - 第三次握手(客户端发送 ACK)向服务器证明了**客户端的接收能力**,同时明确**客户端已知 "服务器端的初始序列号"**。 > [!caution] 若仅两次握手,则服务器端==**无法确认 "客户端" 的接收能力**==,也不能确定**客户端**是否已知晓 "**==服务器端==的初始序列号**"。 ##### 原因三:防止服务器端为 "旧的连接请求" 建立连接。 如果**仅有两次握手**,意味着**服务器端在 "收到客户端的==首次握手==" 后就会建立连接**。 例如,客户端由于网络拥塞,先后发起了 **两个 SYN 连接请求**,而后**旧的连接请求先到达服务器**,此时服务器 **将基于==旧连接==请求建立连接**。直到服务器端向客户端发送数据后,**客户端发现序列号不对而回发的 ==RST 报文==** 到达服务器,服务器才断开连接。 三次握手防止了这种情况的发生,首次握手后,仅加入 "**==半连接队列==**",**只有当客户端再次确认,即服务器端==收到第三次握手后,连接才真正建立==(才加入全连接队列),从而确保所有的连接都是当前有效且所需的**。 > [!example] 三次握手,防止服务器端为 "==**旧的连接请求**==" 建立连接。 > > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-9A62310F719873861670BD5DAFA76087.png|949]] > <br> ### ❓TCP 连接初始序列号随机化的原因 为什么每次建立 TCP 连接时,初始化的序列号都要求不一样? 1. 避免 **上一个==历史连接==** 中发出的 **==历史报文==**,**在==重新建立的 TCP 连接==(四元组完全一致)中被接收**。 2. 为了安全性,防止黑客伪造的相同序列号的 TCP 报文被对方接收; > [!example] 原因一示例 > 例如,客户端与服务器端**建立的 TCP 连接**因某些原因**中断**,而此时由于网络阻塞,**还存在未收到的 TCP 消息**。 > > 当客户端与服务器端**重新建立 TCP 连接**后(IP、端口不变,完全一致), > 如果**两次连接的初始序列号完全一样**,则**上一个历史连接中的历史报文就==很有可能被接收==**。 > > 双端的**初始序列号 "随机化"**,使得历史报文的序列号**不位于对方的接收窗口**,从而**很大程度上避免**了历史报文(**并不是完全避免**)。 > > 如下图所示: > > ![img|614](_attachment/02-开发笔记/07-计算机网络/传输层/TCP%20协议.assets/IMG-TCP%20协议-64C4156C789C3DC65D3BCE03D6996DB1.png) > <br><br> ## TCP 连接断开(四次挥手) > [!note] 双端都可以**主动断开连接**,只有 **==主动方==** 才有 `TIME_WAIT` 状态。下面 **以 "客户端" 发起断开为例**。 TCP 连接通过 **四次挥手** 机制实现: - 第一次挥手:**客户端发送 FIN** - 客户端主动**发起连接断开**,发送 **FIN 报文段**( `FIN` 标志位为1),此后**客户端进入`FIN_WAIT_1`状态**,等待服务器的 ACK。 - 第二次挥手: **服务器回应 ACK** - 服务器收到 FIN 后,回应一个ACK报文。此后,**服务器进入`CLOSE_WAIT`状态**。 - 客户端收到ACK后,进入`FIN_WAIT_2`状态,等待服务器的 FIN。 - 第三次挥手:**服务器发送 FIN** - `CLOSE_WAIT` 状态下,当服务器**完成数据发送任务后**,由 **==进程调用 `close()`==,发送 ==FIN 报文==给客户端**。 - 此后,**服务器进入 `LAST_ACK` 状态**,等待客户端的 ACK。 - 第四次挥手:**客户端发送 ACK** - 客户端收到服务器的FIN后,**==发送最后的ACK报文==**,进入 **==`TIME_WAIT`状态==并启动一个定时器,等待 ==`2MSL`时间== 以确保服务器收到 ACK**。 - 服务器**收到客户端的ACK后,关闭连接**,进入`CLOSED` 状态。 - 客户端**进入 `TIME_WAIT` 并经过 `2MSL`** 后,自动关闭连接,进入 `CLOSE` 状态。 四次挥手的过程确保了**双方都能够完成数据传输,并且关闭两端的 TCP 连接**。 > [!info] FIN 报文:仅表明发送 FIN 报文的一端 "**==不再发送数据==**",但**还能接收数据**。 > > [!caution] 服务器端仅当 ==调用 `close()` 后== 才会发送 ==FIN 报文==,进入 `LAST_ACK` 状态!否则将一直停留在 `CLOSE_WAIT` 状态。 > > 服务器端收到 FIN 报文后,TCP 协议栈会 **向 FIN 包中插入一个 ==`EOF` 文件结束符==** 到 "**接收缓冲区**" 中。 > 服务器端在**通过 `read()` 读取到 `EOF` 后==手动调用 `close()`==**,**从而发送 `FIN` 并进入 `LAST_ACK` 状态**。 > [!info] MLS:Maximum Segment Lifetime,**==最大报文生存时间==**。Linux 中默认为 30 秒。 > [!note] **`TIME_WAIT` 状态计时器** 通常计时为 2MLS > > `TIME_WAIT` 状态计时器为 2MLS,原因是首先要**至少等待客户端发送的最后 ACK 能到达服务端**(1MLS), > 若**丢失则服务器端重发 FIN**,客户端还要 **==等待看是否有重发的 FIN 到达==(1MLS)**,因此**至少需要 2MLS**。 > > [!example] TCP 四次挥手过程 > > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-8A67BCE50EEA65469CFD33DAC34FA9DF.png|727]] <br><br> ### 「TCP 半关闭」 ⭐ TCP 连接的 "**半关闭**"(Half-close)是指**主动方通过调用 `shutdown(, SHUT_WR)` 仅==关闭发送方向==**(并发送 FIN),而仍保留 "**==接收通道==**"。 该情况下,通信仍可以单向进行,即 **FIN 发送端** 仍然能 **接收对端的数据** 并回发 **==ACK 应答==**,直至对方也发送 FIN 关闭其发送通道。 > [!NOTE] TCP 半关闭状态 > > 参见[^1] > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-FF9AAFCBC23C4C95C8399B772E0D8B6C.png|618]] > <br> #### 通过 close() 或 shutdown() 关闭 TCP 连接的区别 - `close(sockfd)` 调用后:同时关闭 **该 fd 上的 "==发送通道==" 和 "==接收通道=="**(要求 `sockfd` 引用计数为 0),**彻底释放 fd**。 - 对于 "**内核 TCP 发送缓冲区**": - 进程:即刻无法再向其中写入数据; - 内核将**继续发送 TCP 发送缓冲区中的剩余数据**,发完后,**再发送 FIN 报文**,标记进入 `FIN_WAIT_1` 状态。 - 对于 "**内核 TCP 接收缓冲区**" - 进程:即刻无法再从中读取数据,**剩余数据都将被丢弃**。 - 内核: - 在 `TIME_WAIT` 状态前(即 `FIN_WAIT_2` 状态下),可**继续接收对端数据**(这些数据最后会销毁),**回复 ==ACK 报文==**; - 若缓冲区满,则**回发 ==RST 报文==**; - 在 `TIME_WAIT` 状态后,**TCP 连接完全关闭**,**不再接收数据**,若再收到对端数据将**回发 ==RST 报文==**。 - `shutdown(sockfd, SHUT_WR)` 调用后:仅关闭**该 fd 上的 "发送通道"**,仍保留 "**==接收通道==**",即 "**==半关闭状态==**"(Half-Close) - 对于 "**内核 TCP 发送缓冲区**": - 进程:即刻无法再向其中写入数据; - 内核将**继续发送 TCP 发送缓冲区中的剩余数据**,直至为空后,**再发送 FIN 报文**,标记进入 `FIN_WAIT_1` 状态 - 对于 "**内核 TCP 接收缓冲区**" - 在 `TIME_WAIT` 状态前(即 `FIN_WAIT_2` 状态下) - 进程:**==仍可以读取其中数据==**; - 内核:可**继续接收对端数据**(这些数据最后会销毁),**回复 ==ACK 报文==**; - 在 `TIME_WAIT` 状态后,**TCP 连接完全关闭**,**不再接收数据**,若再收到对端数据将**回发 ==RST 报文==**。 > [!NOTE] 客户端调用 `close()` 与 `shutdown(fd, SHUT_WR)` 主动关闭连接时的差异 > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-09551FA8D260C4CD69E6ED97EBF11F2B.png|910]] > [!caution] `shutdown(fd, SHUT_WR)` 调用时**仅关闭写端** 且立即发送 `FIN` 进行四次挥手,但**不会彻底释放 fd**,之后**还需要调用 `close(fd)` 来释放套接字**,否则将导致**文件描述符泄露**。 > [!caution] 注:`close()` 受引用计数影响 [^1] > > - 调用 `close(sockfd)` 会使 `sockfd` 引用计数-1,**若未减为 0**(例如 fork 形成父子进程共享 fd),则**不会进行 TCP 四次挥手**,**不会发送 FIN**; > - 调用 `shutdown(sockfd, howto)` 时,将 **无视引用计数**,触发 TCP 四次挥手,发送 FIN。 <br> ### 挥手报文丢失的情况 第一次、第三次挥手的 FIN 报文存在 "**==超时重传==**" 机制,而 ACK 报文则不具有。 因此,以客户端主动断开连接为例: - 若**第一次 or 第二次挥手**丢失,**客户端**会触发==**超时重传**==(重发次数由参数控制)**重发 `FIN` 报文**。 - 超过**重发次数**后,不再发送。若等待一段时间后若仍未收到 "**第二次挥手**",则**客户端直接进入到 `CLOSED` 状态**。 - 若**第三次挥手**丢失, - 对于 **客户端**(`FIN_WAIT2` 状态),区分两种情况: - 若客户端是调用 `close()` 关闭连接,则而是 **==等待指定时长==**(默认 60s),若**超时仍未收到服务器的 `FIN` 则直接进入 `CLOSED` 状态**。 - 若客户端是调用 `shutdown()` 关闭连接,只关闭 "**发送方向**",则将**一直等待于 `FIN_WAIT2` 状态**。 - 对于**服务器端**,触发==**超时重传**==(重发次数由参数控制)**重发 `FIN` 报文**,超过**重发次数**后,不再发送。 - 若**第四次挥手**丢失: - 对于**服务器端**,触发==**超时重传**==(重发次数由参数控制)**重发 `FIN` 报文**,超过**重发次数**后,不再发送。 - 对于**客户端**(`TIME_WAIT` 状态),每次服务器**收到重传的 `FIN` 报文**就 "**==重置计时器==**"。 > [!note] 第三次挥手丢失,客户端 `FIN_WAIT2` 状态等待的两种情况 > > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-469457161588C0D6F3851412FB3C27BC.png|897]] > > > [!note] 第四次挥手丢失的情况 > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-B2C4FD32D787649D0E281D5A84EF0AEC.png|488]] <br><br> ### FAQ #### ❓ 为什么需要四次挥手? TCP 是全双工协议,每一端发送 FIN 时仅表示其自身 "**不再发送数据**",但**还可以接收数据**。 例如,**服务器端收到 `FIN` 后可能还有数据需要处理和发送**,其将维持在 `CLOSE_WAIT` 状态,直至**不再发送数据时,调用连接关闭函数,发送 `FIN` 报文告知**, 因此,每端各需要一个 FIN 与 ACK,**保证==数据被完整传输==**,共四次。 在被动端 **没有数据发送**,且开启了「TCP **延迟应答机制**」时,第二次挥手的 FIN 与第三次挥手的 ACK 会合并,即**总共==三次挥手==**,完成连接断开[^2]。 ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-7EBF8824C50F895F4A084D60EC00951E.png|514]] #### ❓为什么需要 TIME_WAIT 状态? 两个原因: - (1)保证 "**被动关闭连接**" 的一方,能够**收到==最后的 ACK== 报文**。 - => 在 **第四次挥手报文==丢失==** 时,保证**客户端收到服务器==重传的第三次挥手 FIN== 后,==重发 ACK==**,同时**重置计时器**。 - (2)防止**历史连接中的==历史报文==**,**被==重新建立的 TCP 连接==(相同四元组)错误接收**。 - `TIME_WAIT` 下等待 2MLS 时间,**足以让==两个方向==上的数据包都被丢弃**。 > [!example] 原因一示例 > > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-DE87AF72A896AC14F10578CB0BE51AE3.png|968]] > > [!example] 原因二示例 > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-74465B824A4440AC98C2B8F2469DD695.png|651]] <br><br> ## TCP 保活机制(TCP Keepalive) TCP 连接在**长时间未进行任何相关活动**时,将**定期**发送一个 "**==探测报文==**"[^3]。 如果连续几个**探测报文未被响应**,则内核将认为**当前 TCP 连接已死亡**,内核将**错误信息通知给上层应用程序**。 > TCP 保活机制示意: > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-AEA1C5B24A019E6AAE9493A59787EBBD.png|441]] > [!info] 开启 "TCP 保活探测" 机制 > > socket API 中需**设置 `SO_KEEPALIVE` 选项**开启该机制。 > > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-3F40880038A0B2433289C256F678DCCB.png|612]] > [!NOTE] 许多 "应用层协议" 通常会在 "**应用层**" 自行实现 "**心跳机制**" 来检测连接状态。 > > 例如 HTTP 服务器可**自定义对长连接的超时时间**,超时无请求后,**服务器主动关闭连接**。 > > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-AF063C76B3461C9023FB964299D2AF32.png|338]] > > <br> ### 「半开连接」 > **半开连接**(half-open connection) 半开连接:指 **TCP 连接的一方因意外而断开连接**(未发送 FIN),但**另一方仍然认为连接有效,并==继续发送数据==** 的情况[^4]。 例如,下列场景中**意外端无法发送 FIN**,从而导致 "半开连接": - **主机宕机**(系统崩溃) - **网络中断**(拔网线、断 WiFi) - **设备断电** > [!NOTE] 「TCP 保活机制」正是用于检测 "**半开连接**"的手段。 <br> ## TCP 连接重置(RST) 下列情况下,将触发 TCP 一端发送 **RST 报文**(`RST` 标志位为 1),表示 "**中断当前连接**" 或 "**拒绝连接请求**"。 RST 报文的回发由 "**==内核==**" 自动进行。 | 情况 | 说明 | RST 报文含义 | | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | | **端口未监听** | 任一端收到 **SYN 请求**,但其**并没有监听该端口**时<br> | `RST` 指示拒绝 **无效请求** | | **无效报文** | 任一端收到的 TCP 报文在**序列号、确认号、标志位**或其他信息上**与当前连接状态不符**时 <br>- 例如,一端主机宕机重启,形成半开连接,而后收到对端的报文时; | `RST` 指示 "**终止异常连接**" | | **已关闭连接** | 任一端在 "**已关闭连接**" 上收到数据时 <br>- 例如,一端调用 `close()` 主动关闭连接,而后又收到对端发送的数据时 | `RST` 指示 "**连接已关闭**" | | **快速中断连接** | 应用层在**遇到错误**或者**需要快速中断连接**时, <br>可通过设置特定的 socket 选项(如将 SO_LINGER 设置为 0)来**立即终止连接**, <br>这种情况下系统会**发送 RST** 来快速释放连接占用的资源,而**不经过标准的四次挥手过程**。 | `RST` 指示 "**快速中断连接**" | <br> ## 「半连接队列」与「全连接队列」 TCP 三次握手过程中,**Linux 内核**会维护两个队列: - **半连接**队列,也称 **SYN 队列**(Incomplete Queue) - **存放收到 SYN 后,==尚未完成三次握手==的连接** - **全连接**队列,也称 **Accept 队列**,已连接队列(Completed Queue) - **存放==已完成三次握手==的连接**,等待调用 `accept()` 取出。 > [!NOTE] 当调用 `listen()` 后,TCP 协议栈会初始化这两个队列。 > > - **全连接队列大小**: `= min(backlog, somaxconn)`,其中 > - `backlog` :传给的 `listen(sockfd, backlog)` 的第二项参数。 > - `somaxconn` 是内核参数:`/proc/sys/net/core/somaxconn` > - **半连接队列大小**: > - 由内核参数 `/proc/net/ipv4/tcp_max_syn_backlog` 指定 > > [!caution] 当队列已满时的处理方式 > > - 对于「**半连接队列**」:若开启 SYN Cookie 则启用该机制,否则**直接丢弃 SYN 报文**。 > - 对于「**全连接队列**」:通过 Linux 内核参数 `tcp_abort_on_overflow` 控制: > - `0`(默认):**丢弃 ACK 报文**; > - `1`:回发 RST 报文,**中止连接**。 > [!NOTE] `ss -tl` 可查看 LISTEN socket 上的**全连接队列大小**——`Recv-Q` 字段为 "**==当前==全连接队列中连接数**",`Send-Q` 为全连接队列大小。 > [!info] Linux 内核协议栈中的实现[^7] > > - 半连接队列采用 "**哈希表**" 实现(链地址法,每个 bucket 挂一个链表),基于 IP&端口来计算哈希值,便于 $O(1)$ 查询 `SYN_RECV` 状态的连接; > - 全连接队列采用 "**链表**" 实现,FIFO 方式供 `accept()` 获取。 > > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-410F58AF027940C3808F3007EB1EF61F.png|537]] > [!note] Linux 内核协议栈的 "连接建立" 处理过程 > > 客户端调用 `connect()` 后**发送 SYN 报文**,进入 `SYN_SENT` 状态。当收到 **第二次握手报文 "SYN-ACK"** 时 **`connect()` 调用返回**; > > 服务器端**调用 `listen()` 进入==监听状态==** 后,**内核就会自动维护==两个队列==**: > > - 服务器端收到 **SYN 报文**时,内核将创建一个 "**半连接对象**",加入到「**==SYN 队列==**」,同时**回发 SYN-ACK 报文**给客户端; > - 服务器端收到 **ACK 报文**时,内核将从「SYN 队列」中取出相应连接对象,加入「**==Accept 队列==**」, > > `accpet()` 函数本身的作用仅仅是**从 " ==Accept 队列==" 中取出一个已通过三次握手建立连接的对象**,返回**一个新的==关联于该连接的套接字==**。 > > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-B7599C52B1B8C0B4E71467386679B2D1.png|881]] > > ^d9y5vw <br> ### SYN Cookie 机制 SYN Cookie 是用于**防御 SYN 泛洪攻击**的机制,其思想为: 1. 服务器**不在 ==SYN 队列==中存储半连接**,而是**根据==连接信息==(TCP 四元组)编码得到一个==特殊 Cookie==**,作为 **SYN-ACK 报文**中的 "**==初始序列号==**" (ISN) 。 2. 当客户端回复 ACK 时,服务器根据**确认序列号(ISN+1)** 进行验证,若成功则==**直接创建一个完整连接**==放入 ==**Accept 队列**==。 > [!NOTE] Linux 下通过 `net.ipv4.tcp_syncookies` 内核参数开启该功能。 > 参数值: > - `0`:关闭; > - `1`:仅当 「SYN 半连接队列」满时,自动启用。 > - `2`:无条件开启; > > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-69A12D6F98372E6BD0E06DC9D11B885B.png|387]] > > [!info] SYN Cookie 的计算: 根据 "**TCP 四元组** + **服务器密钥** + **timestamp**" 共同进行映射,得到一个 **32 位无符号整数**。 <br><br> ## 相关问题 #### ❓具有一个 IP 的服务端监听了一个端口,它的 **TCP 的最大连接数是多少**? 理论上,**服务器端通常固定在某个本地端口监听,等待客户端的连接请求**。 TCP 连接由 TCP 四元组唯一确认,**服务器端固定**,则其中可变的是**客户端 IP 与端口**。 故理论上最大 TCP 连接数 = **客户端 IP 数 * 客户端的端口数** ![image-20230911111227005|671](_attachment/02-开发笔记/07-计算机网络/传输层/TCP%20协议.assets/IMG-TCP%20协议-6E352D260A405DDF5EAF9B947C5558CA.png) 实际连接数受限: - 如果文件描述符被占满了,会发生 **Too many open files**。Linux 对可打开的文件描述符的数量分别作了三个方面的限制: - **系统级**:当前系统可打开的最大数量,通过 `cat /proc/sys/fs/file-max` 查看; - **用户级**:指定用户可打开的最大数量,通过 `cat /etc/security/limits.conf` 查看; - **进程级**:单个进程可打开的最大数量,通过 `cat /proc/sys/fs/nr_open` 查看; - **内存限制**,每个 TCP 连接都要占用一定内存,操作系统的内存是有限的,如果内存资源被占满后,会发生 OOM。 #### ❓ 对于已建立的 TCP 连接,其中一端 "**进程崩溃**" 或 "**主机宕机**"(如断电),两种情况**有什么区别**? 若此时 TCP 连接上无数据传输: - **进程崩溃**:OS **回收进程资源**时,**会发送 ==FIN 报文==给对端**,**由内核完成后续==挥手过程==**。 - **主机宕机**:无法通知对端。 - 若对端**开启了 ==`SO_KEEPALIVE` 选项==**,则可通过 **TCP 保活机制**进行探测,在**连续无响应时报告 TCP 连接死亡**。 - 若对端未开启 `SO_KEEPALIVE` 选项,则无法感知,将**始终处于 `ESTABLISHED` 状态**(形成 **==半开连接==**),直至服务器端**手动结束进程**。 若此时 TCP 连接上,还有 "**正常端 A**" 向 "**异常端 B**" 发送的**请求报文**, - 若 B **主机宕机**,且**迅速重启**,有收到 A 的**请求报文 or 重传报文**,则**回发 ==RST 报文==,重置该 TCP 连接**。 - 若 B **主机宕机**,且**久未重启**,A **==多次超时重传==** 后仍无响应则 **断开连接**。 <br><br><br> # TCP 滑动窗口机制(流量控制) > 滑动窗口机制(Sliding Window Mechanism) TCP 通信基于 "**请求-响应**" 的工作方式,在此基础上采用了 "**==滑动窗口==**" 机制(而非低效的 "**停-等**" 机制)。 > [!NOTE] 滑动窗口机制达到了两个目的:「**流水线高效传输**」与 「**流量控制**」 **滑动窗口机制**允许发送方**连续发送多个 TCP 数据报而无需逐条等待 ACK 应答**,是实现 **==高效数据传输==** 和 **==流量控制==** 的关键手段。 具体涉及三个窗口: - **发送窗口**(Send Window):逻辑上表现为,发送方**可以连续发送而无需等待 ACK 的最大数据范围**。 - 实现上为 "**无需等待 ACK 的情况下可发送的数据量(字节数)**" - 发送窗口大小根据 "**接收方的接收窗口大小**"和 "**网络拥塞情况**" 动态调整 - **接收窗口**(Receive Window):逻辑上表现为,接收方**可接收的最大数据范围**。 - 实现上为 "**还能接收的数据量(字节数)**" - 接收窗口大小 = 接收方 "**==接收缓冲区==**" 的**剩余容量**。 - **拥塞窗口**(Congestion Window):受**网络拥塞控制**的窗口,决定了**发送方实际能发送的最大数据量**。 > [!example] 滑动窗口的逻辑示意图 > > 下图中,接收方回传 ACK 报文时,"**接收窗口大小**" 将缩减为灰色部分。 > > ![image-20240104135714455|576](_attachment/02-开发笔记/07-计算机网络/传输层/TCP%20协议.assets/IMG-TCP%20协议-B2BC4560E0ED8A8E5C934C4D54C1CB53.png) > > <br> ### 高效传输 > [!example] "停-等"模式 vs "==滑动窗口=="模式 > > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-796A6DE5EC9A21AC6E4EF7ED2465CE93.png|495]] > <br><br> ## 流量控制——发送/接收窗口的动态调整 TPC 基于 "滑动窗口" 实现了**流量控制**,**防止发送方发送的数据超过接收方的处理能力**,即 **==防止接收方的缓冲区溢出==**。 - 接收方行为: - 接收方将其 "**接收窗口大小**"(即 **"==接收缓冲区==" 的剩余容量**) 通过 ACK 报文中 TCP 头部的 **"窗口大小"** 字段告知发送方——指示发送方**在接收到下一个 ACK 应答之前,还可以往线路上发送多少数据**。 - 发送方行为: - 发送方收到 ACK 报文后据此**动态调整**其 "**发送窗口大小**",控制其数据发送量,以**防止接收方的接收缓冲区溢出**。 - 当**接收方报告窗口大小为 0** 时,发送方停止发送数据,但会定期 **发送=="零窗口探测报文"==**,以检测窗口是否再次打开。 > [!note] 发送方的 "**==发送窗口==大小**" 由接收方的 "**==接收窗口==大小**" & "**==拥塞窗口==大小**" 共同决定 > > - 接收方通过 ACK 报文中的"窗口大小"字段提供接收窗口大小; > - 发送方根据**接收方提供的窗口大小**来调整发送窗口大小,同时受 "**拥塞窗口**" 影响。 > > $ > \text { 发送窗口大小 }=\min (\text { 接收窗口,拥塞窗口 }) > $ > > [!example] 基于 "滑动窗口" 实现流量控制 > > 下图中,客户端向服务器请求获取数据,**服务器端为 "发送方"**。 > > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-F542A5BB9BDA96C511502891BDE79256.png|642]] > [!example] 接收方报告其 "接收窗口" 大小 > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-C01690A7B5D1FF4955A0ED86C21F6080.png|732]] <br><br> ### 零窗口探测 当接收窗口大小变为 0 的时候,**发送方收到告知零窗口大小的 ACK 报文后将==停止发送数据==**。 **发送方需要知道何时能重新发送数据**: 其将会**启动一个定时器**,不断地向接收方发送 **==零窗口探测报文==**,以**检测接收方的窗口是否已经打开(即窗口大小是否变回非零值)**。一旦接收方的窗口大小增加,则通过 ACK 报文通知发送方,发送方随后恢复数据的传输。 > [!info] 零窗口探测报文(Zero Window Probe)只带有一个字节的数据 > [!example] 窗口探测示例 > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-680BCE05E3BEB3E0F7C1E5079A4B26D9.png|650]] > [!info] 接收方窗口为 0 时,还可接收来自发送方的特殊报文 > > 1. 发送方回发的 **ACK 报文**(**无数据载荷**) > 2. **零窗口探测报文**(单字节) > 3. 控制报文:**RST、FIN 等**。 > <br><br> ## 减少小报文传输的策略 为了提高**数据传输效率**,TCP 双端会在 **"接收窗口" 较小时** 就停止发送数据,**避免小数据传输** (TCP + IP 头至少 40 字节,若只传输几十甚至几字节数据很不划算)。 为此,TCP 的发送方、接收方采用如下**策略** 来减少 "**==小报文传输==**": - 发送方:采用 **Nagle 算法** - 接收方: - (1)**==不通告小窗口==**:当 $\text{窗口大小}< \min\left( \text{MSS}, \large\frac{\text{缓存空间}}{2} \right)$ 时,**直接报告==接收窗口为 0==**。 - (2)**==延迟确认==** 策略 ### Nagle 算法 **==Nagle 算法==**,思路是**延时发送**,**==积攒小包==**,只有满足下面两个条件中的任意一个条件,才可以发送数据: - 条件一:`接收窗口大小 >= MSS`  且  `数据大小 >= MSS`; - 条件二:收到**已发送数据的 ACK 应答** 若两个条件均不满足,则**将一直==囤积==数据**,直至满足条件。 ```c title:Nagle_Algorithm // Nagle算法伪代码: if (有数据要发送) { if (可用窗口大小 >= MSS && 可发送数据 >= MSS) { 立即发送MSS大小的数据 } else { if (有未确认数据) { 将数据放入缓存等待接收ACK } else { 立刻发送数据 } } } ``` > [!example] Nagle 算法示例 > > 如下图所示,收到 `"H"` 的 ACK 应答后,将 **囤积的 `"ELL"`** 发出。 > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-252F8C3606EA5D8DF93BC93679FB831A.png|467]] > [!info] Linux 协议栈中,Nagle 算法默认开启 > > 可设置 socket 属性,**通过 `TCP_NODELAY` 选项**关闭该算法: > `setsockopt(sock_fd, IPPROTO_TCP, TCP_NODELAY, (char *)&value, sizeof(int)); `。 > > 注:在一些**需要小数据包交互**的场景,例如 `telnet` 或 `ssh`,需开启该选项。 <br> ### TCP 延迟应答 延迟确认策略,旨在**应答 ACK 的同时携带数据**,从而提高传输效率: - 当**有**响应数据要发送时,响应数据随着 ACK 报文一起立即发送; - 当**无**响应数据要发送时,**ACK 延迟一段时间**(Linux **默认 200ms**),**以==等待==是否有响应数据** 可一起发送。 - 若延迟等待期间**第二个数据报文**到达,则**立刻发送 ACK**。 > [!example] TCP 延迟确认 > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-DB0AE0FA54C4D1BEC6DAD45AF8D4F81B.png|497]] > [!info] 延迟确认设置 > > 延迟确认**默认开启**,可在 Socket 设置中启用 `TCP_QUICKACK` 选项来关闭 > > ```c > // 关闭 TCP 延迟确认 > setsockopt(sock_fd, IPPROTO_TCP, TCP_QUICKACK, (char *)&value, sizeof(int)); > ``` > > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-FAFB2F2CC1FEAF2096A0EBEC9EB477AF.png|414]] > <br><br> ## 滑动窗口的具体实现 #### "发送窗口" 的实现 ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-977AFC12A896B923A8D7202BD0E890DB.png|690]] 通过**一个变量值** & **两个指针**实现: - `SND.WND`:发送窗口大小(由**接收方**指定) - `SND.UNA`:指针,指向**已发送但==未收到确认==的==最低字节==的序列号** - `SND.NXT`:指针,指向**未发送但==可发送==的首字节的序列号**(可发送是指位于 "发送窗口" 范围内,但还未发出) 当 `SND.UNA` 指向的**字节序列号**收到 ACK 后, `SND.UNA` 就会后移,从而实现 "**滑动**": ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-8BE07A4082FFB56FC54BD7FC0D0595B2.png|647]] #### "接收窗口" 的实现 > [!NOTE] "接收窗口" 说明 > > 逻辑示意: > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-76EBB9A6901AA037B25CB8D64B63CA78.png|652]] > > 实际实现: > > ![image-20240106222716262|558](_attachment/02-开发笔记/07-计算机网络/传输层/TCP%20协议.assets/IMG-TCP%20协议-1A03E1BD1F2456E05E8193F6CA59B779.png) > > 接收窗口的大小是 "**==接收缓冲区剩余容量==**",而不是 "**接收缓冲区总大小**"。 通过**一个变量值** & **一个指针**实现: - `RCV.WND`:接收窗口大小(应为 "**接收缓冲区**" 剩余容量的大小) - `RCV.NXT`:指向**期望接收的==下一个字节==的序列号**,也即回传的 ACK 报文中的 "**确认号**"。 <br> ## 滑动窗口的大小要求 **滑动窗口的长度必须 ≤ 序号空间大小的一半**,避免"**==序列号回绕==**" 导致的歧义: **"==接收窗口==" 后移后与 "==发送窗口=="(尚未后移) ==重叠==**, 故而 **接收方** 无法区分一个 TCP 报文是 "**发送方重传的旧报文**" 还是 "**接收方==预期的新报文==**"。 > [!example] > > 如下图所示。若滑动窗口长度满足 ≤ 序号空间大小的一半,则不会出现下列情况。 > > ![[Excalidraw/Excalidraw-Solution/TCP 协议.excalidraw|644]] > <br><br> # TCP 发送/接收缓冲区 TCP 是全双工通信,**每一端都既是发送方又是接收方,因此每端都有自己的==发送/接收缓冲区==**,各自维护一个接收窗口。 ![image-20240104114240978|526](_attachment/02-开发笔记/07-计算机网络/传输层/TCP%20协议.assets/IMG-TCP%20协议-2BDDD040014793F97E59CC26951177A7.png) - 发送缓冲区 `SendBuffer` - `LastByteSent`:发送方已发送的数据流的最后一个字节的序号 - `LastByteAcked`:发送方已发送但未被确认的最后一个字节的序号 - 发送方已发送但还未确认的数据量: `LastByteSent - LastByteAcked` - 接收缓冲区 `RcvBuffer` - `LastByteRead`:接收方的应用程序从缓冲区中读出的字节流的最后一个字节的序号 - `LastByteRcvd`:接收方缓冲区中的数据流的最后一个字节的序号 - **接收窗口 = 接收缓冲区剩余容量**,即`rwnd = RcvBuffer - [LastByteRcvd-LastByteRead]` > [!NOTE] TCP 接收缓冲区大小应当至少为 ==4 MSS==,从而保证 "快速重传&快速恢复" 机制可工作[^5](容纳 1 正常ACK + 3 冗余 ACK)[^5] > > [!NOTE] 查看系统默认的 TCP 缓冲区大小 > > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-426998A96040D83D65F8CC4424FC7F69.png|449]] > > 三个值分别为缓冲区大小的 "**最小值**"、"**默认值**"、"**最大值**"。 > > 每个 TCP 连接的**实际缓冲区大小==由内核动态调整==**,在连接尚未进行任何数据收发时,**实际分配的缓冲区内存或为 0**。 > 可通过 `ss -tm` 查看**已建立的 TCP 连接的缓冲区==实际被分配的内存大小==**。 > > 参见 `man 7 tcp`,可查看关于上述参数值的具体说明: > > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-C438BDE199270F3EC592F6960C87B28A.png|753]] > <br> ### 发送缓冲区(Send Buffer) 发送缓冲区:发送方为 **存储应用进程提交到 TCP 层的发送数据** 而设置的一块**实际内存空间**。 作用:「**暂存数据**」 & 「**实现==重传机制==**」 发送缓冲区中存储着两类数据: 1. **==尚未发送==** 的 => TCP 发送数据前还需要对数据进行打包封装处理,故存在延迟; 2. **==已发送但未收到 ACK==** 的 => 数据丢失情况下可以重传 ### 接收缓冲区(Receive buffer) 接收缓冲区:接收方为 **存储从网络接收的数据** 而设置的一块**实际内存空间**。 当 TCP 数据包到达接收方时,首先被放置在这个接收缓冲区中,等待 **==按序交付==** 给应用程序。 作用:「**暂存数据**」 & 「**保证==按序交付==**」 接收缓冲区中存储着 **已接收**、但**还未能交付**给目标应用程序的数据,包括 1. **按序到达,但应用程序==还未及时读取走==的数据**; 2. **到达顺序不连续,即==失序==的数据**(将缓存直至丢失的数据部分到达从而**补全得到完整的有序数据流**,再交付给应用程序) <br><br> ## "发送/接收缓冲区" 与 "发送/接收窗口" 的关联 区别说明: - "**发送/接收缓冲区**" 是 TCP 实现中为发送方/接收方 **实际分配的"内存空间"**,用于缓存数据。 - "**发送窗口/接收窗口**" 是实现**流量控制**的逻辑机制,可理解为在 "**缓冲区**" 上滑动的窗口; - "接收窗口": 即 "**接收缓冲区的剩余空间**"; - "发送窗口": 根据接收方 "接收窗口" 的大小而动态调整; - "**重传队列/ACK 队列**":(实现上并不是队列数据结构,而是 `sk_buff` 双向链表和 `RBTrees` 红黑树) - "重传队列":用于跟踪/记录发送方 "当前发送窗口" 内 "**已发送但未收到确认**" 的数据; - "ACK 队列":用于跟踪/记录接收方 "当前接收窗口" 内 "**已接收而等待发送 ACK 确认**"的数据。 层级关系: **"发送缓冲区"为发送数据提供缓存**,**"发送窗口"控制数据传输的流量**, **"重传队列" 管理可能需要重传的数据**: - "==**发送缓冲区**==" 中缓存应用进程提交给 TCP 层的数据,包括 "**所有等待发送**" 以及 "**已发送但未收到确认**" 的数据; - "==**发送窗口**==" 在发送缓冲区中滑动,窗口内只包含 "**当前允许发送" 和 "已发送但未收到确认**" 的数据; - "==**重传队列**==" 记录着滑动窗口内的那部分 "**已发送但未收到确认**" 的数据; <br><br><br> # TCP 确认应答机制 > 确认应答(Acknowledgment, ACK) ,该机制是 TCP 可靠数据传输的核心,**确保数据包能够被正确接收**。 TCP 通信基于 "**请求-响应**" 的工作方式,**接收方**需要为 "**收到的报文**" 回传 "**==应答报文==**"——向发送方指示接收方**已确认收到的数据**。 TCP 通过 "**累积确认**" 、 **"SACK"**、 **"D-SACK"** 三种机制来实现确认应答: | | 说明 | | --------- | ------------------------------------------------------------------------- | | 累积确认机制 | 接收方 ACK 报文中,TCP 头部的 "**==ACK 确认号==**" 指出该报文发送方 "**预期从对方收到的==下一个字节序号==**"; | | SACK 机制 | 接收方 ACK 报文中,TCP 头部的 "**==SACK 选项==**" 指出其 "**已接收的==非连续数据块==**"; | | D-SACK 机制 | 接收方 ACK 报文中,TCP 头部的 "**==SACK 选项==**" 指出其 "**已收到的==重复数据片段==**"; | <br> ## "累积确认" 机制 > 累积确认(Cumulative Acknowledgment) 累积确认机制下,**ACK 确认的是最后正确接收的字节**,而不是每个数据包。 累积确认的工作原理: 1. **ACK 确认号**的值是 "**预期从对方收到的==下一个字节序号==**",表明它 **已经成功接收到==字节序列号在该"确认号"之前==的所有数据**。 2. "**==冗余 ACK==**":当接收方收到 "**与==预期序列号==不符号的报文**" 时(意味着存在 "**丢包**" or "**乱序**"or "**冗余报文**"), 接收方将 **==重发上一个 ACK 报文==**(称为 "**==冗余 ACK==**"),表明对其已成功接收到的数据进行确认。 缺点:接收方只能确认其**最后一个==连续接收==的数据包**,而**发送方需要==重传丢失数据包之后的所有数据包==**。 > [!example] > > 例如,若**发送方发送了 `SEQ=1000`, `LEN=100` 的数据**(即数据范围 `[1000,1099]`), > 则接收方成功收到后,**返回报文中 `ACK=1100`**,表明**序号 1100 之前的字节数据均已成功接收**,而预期收到的下一个字节序号为 1100。 > > [!NOTE] 累计确认机制下,ACK 报文丢失不造成影响,不会导致数据重发。 > > 如下所示,上一条 `ACK=600` 报文丢失,但**通过下一条 `ACK=700` 报文可知接收方已收到 700 序号前的所有数据**。 > > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-A9FCA45BFC1F19D15BDDCAB7D0F15E68.png|585]] > > [!example] 冗余 ACK 示例 > ![image-20240105214824406|485](_attachment/02-开发笔记/07-计算机网络/传输层/TCP%20协议.assets/IMG-TCP%20协议-FD859B407261E1CBEB9DCE3C1D078B7D.png) > <br> ## "选择确认"机制(SACK) > 选择确认(Selective Acknowledgment,**SACK**),也称 **==选择重传==**。 SACK 机制下,接收方**更精确地指出==已接收的非连续数据块==**,发送方据此 **仅重传丢失的数据包**。 SACK 机制的工作原理: 1. **接收方** 回传的 ACK 报文中,在 **TCP 头部**携带一个「**==SACK 选项==**」,用以告知 "**发送方**" 其 **已成功接收的数据块的信息**。 - 每个 SACK 块指定了一个**已接收数据块的起始和结束序列号**。 2. **发送方** 根据接收方 ACK 报文中的 **SACK 信息**,当发生数据包丢失时,**==仅重传那些未被确认接收的数据包==**,而不是重传所有后续的数据包。 > [!example] > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-2563ED4FC25565BACDBA8EA858D7F45D.png|779]] > [!info] SACK 机制需要 TCP 通信双端 "**均开启**"。 > > Linux 下,可通过 `net.ipv4.tcp_sack` 参数打开(Linux 2.4 后默认打开) <br> ## D-SACK 机制 > Duplicate SACK,又称D-SACK D-SACK 即是在 SACK 选项中 "**报告已收到的==重复数据片段==**",用以**更明确地指出两种情况**,指示发送方无需再重传; 1. 丢失的是 "**==接收方的应答报文==**", 2. 未丢包,只是由于网络延迟,**接收方==较迟==收到中间数据包**。 > [!example] 两种情况 > > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-97F0CBCA265019B2A42883A9C8172345.png|749]] > > [!info] DSACK 机制需要 TCP 通信双端 "**均开启**"。 > Linux 下,可通过 `net.ipv4.tcp_dsack` 参数打开 D-SACK 机制(Linux 2.4 后默认打开) <br><br><br> # TCP 重传机制 **重传机制** 是 TCP 提供可靠数据传输的核心机制之一,确保在丢包或延迟情况下,**及时重传丢失的数据**,保障数据能最终被正确传递和接收。 TCP 具有两种具体的重传触发机制:「**超时重传**」与 「**快速重传**」 <br> ## 超时重传 超时重传的工作原理: 1. 当发送端发送一个 **TCP 报文段**时,启动一个计时器(默认 1s?),**等待对该报文段的 ACK 确认**。 2. 若**超时仍未收到 ACK**,则重新发送该数据包。 3. 若**连续发生超时**,TCP 实施**指数退避策略**——**每次超时后都将==超时间隔加倍==( `*2`)**,以避免在网络拥塞时频繁重传。 ==**发送方的超时重传**== 的触发原因有两种: 1. **发送方的==请求报文==丢失**; 2. **接收方回传的 ==ACK 应答报文==丢失**。 > [!example] 两种超时情况 > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-3644DF5C9F0EB70B0F25A577F49ECE1A.png|551]] > [!caution] ACK 报文没有重传机制 > 例如,三次握手与四次挥手过程中,仅 ==SYN==、==SYN-ACK==、==FIN== 报文会重传。 <br> ## 快速重传 > 快速重传(Fast Retransmit) 该机制下,当**发送方连续收到==三个或以上的重复 ACK 报文==时,即明确已发生了丢包,==发送方立即重传丢失的报文段==**,而**无需等待计时器超时**。 快速重传机制在 "**超时重传**" 的基础上,可以**更快响应丢包并进行重传**,减少等待时间。 > [!example] 快速重传触发示意 > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-10AA90A0DFF4B1810C69AF7D8BF0BEA9.png|429]] > [!NOTE] 拥塞控制中的 "==快速恢复==" 阶段就依赖于 "快速重传" 的触发,在触发快速重传之后,TCP 进入快速恢复阶段。 <br><br><br> # TCP "拥塞控制" 机制 > 拥塞控制(Congestion Control) > [!faq] ❓为什么需要拥塞控制? > 网络拥塞时可能出现**丢包或延迟**,若此时**发送方不断重传,只会导致网络拥塞更严重**,故需要 "**拥塞控制**"。 > 拥塞控制的目的:**避免「发送方」传输==过量==数据导致网络拥塞**。 拥塞控制通过引入一个额外的 「**==拥塞窗口==**」实现,其与**接收方的接收窗口 rwnd** 共同限制了**发送方的发送窗口大小**: $ \text{swnd}=\text{LastByteSent - LastByteAcked} \le \min(\text{cwnd}, \text{rwnd}) $ > [!info] Linux 下,每个 TCP 连接的 `cwnd` 默认值为 10 个 MSS > > 可通过 `ss -tani` 命令查看各 TCP 连接的初始 `cwnd` 值: > > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-1FF5B44C19CF85AC98BE6E0FDF19051B.png|970]] > > > [!NOTE] 拥塞控制机制的 效果 > > - 理想情况下**当多个 TCP 连接共享一段共同的瓶颈链路时,会收敛到趋近于多个连接之间平等地共享带宽**。 > - 实际情况下,多个 TCP 连接彼此间会获得非常不平等的链路带宽份额。 > - 具有**较小 RTT 的连接**能够在链路空闲时**更快地抢占到可用带宽**(能更快地扩展其拥塞窗口),因而比具有较大 RTT 的连接享有更高的吞吐量。 > <br> ## 拥塞控制的工作阶段 TCP 的拥塞控制主要包括三个过程/阶段: - **慢启动**(Slow Start) - **拥塞避免**(Congestion Avoidance) - **快速恢复**(Fast Recovery):cwnd **==减半后+3==**,此后每收到一个冗余 ACK 则增加 1 MSS。 > [!info] 拥塞控制引入了一个**状态变量 `ssthresh`** ,称为 "**==慢启动阈值==**",用于**控制慢启动过程**。 > > `ss -ti` 可查看已建立的 TCP 连接的 ssthresh 值: > > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-948531AB12763923E4BFC9C6FBA3DA4C.png|842]] > 各阶段的作用机制如下: | | 该阶段的工作方式 | 备注 | | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | | **慢启动**<br>(Slow Start) | 1. cwnd 从初始值开始; <br>2. 每收到一个ACK,则 **cwnd ==增加 1 MSS==**; <br>3. 当 `cwnd >= ssthresh` => 进入 "**拥塞避免**" 阶段 | cwnd 呈 **指数级增长**,**每经过一个 RTT 时间就会==翻倍==**; | | **拥塞避免** <br>(Congestion Avoidance) | - 每收到一个 ACK,**cwnd ==增加 1/cwnd==** | cwnd 呈 **线性增长**,**每经过一个 RTT 时间==增加 1 MSS==**; | | **快速恢复**<br>(Fast Recovery) | 1. 设置 `cwnd = ssthresh + 3*MSS`(3 个冗余 ACK); <br>2. 此后**每收到一个==冗余 ACK==**,**cwnd ==增加 1 MSS==**; <br>3. 当收 **==新的 ACK==** 时,重置 `cwnd = ssthresh` => 进入 "**拥塞避免**" 阶段。 | | > [!NOTE] 慢启动、拥塞避免过程示意 > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-C4899846967EDBB0394A236EB04A4A72.png|897]] <br> ## 拥塞处理办法 拥塞控制将 **两种重传机制** 的 **==触发==** 均视为指示 **==网络拥塞==** 的信号,在两种情况下采用不同处理方法: | 触发情况 | 处理方式 | 说明 | | -------- | --------------------------------------------------------------------------------------------------- | -------------------------------------------- | | **超时重传** | 1. 设置 **`ssthresh = cwnd/2`**(当前拥塞窗口大小的一半) <br>2. **重置 `cwnd`** 为初始值; <br>3. => 重新开始==**慢启动**==过程 | 超时,通常意味着**严重的网络拥塞**; | | **快速重传** | 1. 设置 **`ssthresh = cwnd/2`** <br>2. => 进入 ==**快速恢复**== 过程 | 意味着发生了丢包,但**网络拥塞较轻** <br>(接收方仍能接收后续的冗余 ACK) | > [!example] "==超时重传==" 时的拥塞处理 > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-3554C181B1131A3094EF516CC9FF577D.png|606]] > [!example] "**==快速重传==**" 时的拥塞处理 & "**==快速恢复==**" 过程 > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-C22D0C00FCBF778D63C57D4C1A603519.png|834]] <br> ### 三个过程的状态机 > ![image-20240107102832332|720](_attachment/02-开发笔记/07-计算机网络/传输层/TCP%20协议.assets/IMG-TCP%20协议-4D39D4DC1A72BE6C29048CED10A54AFB.png) <br><br><br> # TCP 粘包与拆包 TCP 是**面向字节流的无边界**协议,因此数据传输过程中可能遇见两种情况: - 「**粘包**」:发送方**一次性合并发送多个应用层消息**(例如受 Nagle 算法影响),接收方收到来自发送方的**多个数据包**后,难以区分边界。 - 「**拆包**」:发送方的**一个完整的应用层消息被拆分成多个 TCP 报文分别发送**(例如受限于 MSS),接收方需分多次接收并自行拼接。 解决办法:交由 **应用层** 明确消息边界,方式包括: 1. 在 "**消息头**" 中添加 "**长度**" 字段,例如 HTTP 中的 `Content-Length`; 2. 约定 "**消息分隔符**",从而明确消息边界。 3. 固定**单个 "应用层消息" 的长度**。 > [!NOTE] TCP 是无边界的字节流协议,不指定消息边界或分隔符,而由应用层自行确认。 > > 以 HTTP 为例: > > - GET 报文,通常没有消息体,只有请求行与请求头,以空行 `\r\n` 结尾,故读取到连续的 `\r\n\r\n` 时, 即可判断结束; > - POST 报文,消息体长度由头部的 `Content-Length` 字段指出,据此在空行之后读取对应数量的字节数即可。 > > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-54F2A56E459538661004673003EF4E9D.png|624]] > <br><br><br> # TCP FastOpen TCP FastOpen (**TFO**)是 TCP 协议的一个扩展,可实现 "**绕过三次握手而直接发送数据**",减少常规 TCP 三次握手的 1-RTT 时延[^6]。 TFO 利用了 cookie: - 客户端与服务器 "**首次建立 TCP 连接**" 时,服务器会在**第二次握手 SYN-ACK 报文**中携带一个 cookie。 - 客户端与服务器 "**==后续再次==建立 TCP 连接**" 时,客户端可在发送**第一次握手 SYN 报文**时 **==携带之前的有效 cookie==**。 - 若服务器验证 cookie 有效,则其收到 SYN 报文且确认接收数据后就可 **==立即回发 ACK 应答报文==**,而**不必等待三次握手结束**。 > [!NOTE] TFO 过程示意 > > 第一次连接时:**服务器的 SYN-ACK 携带 cookie** > > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-340804332D18DFA74A79902DECA2AFE7.png|454]] > > 第二次及后续连接时: > > - 客户端 SYN 报文可携带 data; > - 服务器收到数据后即可回发 ACK,**==不必等待三次握手完成==**。 > > ![[_attachment/02-开发笔记/07-计算机网络/传输层/TCP 协议.assets/IMG-TCP 协议-1709F8B950AAF7D66FED3A39AA3A94F7.png|448]] > <br><br><br> # ♾️参考资料 - 《计算机网络:自顶向下方法》 - 《Unix 网络编程》 - [4.1 TCP 三次握手与四次挥手面试题 | 小林coding](https://xiaolincoding.com/network/3_tcp/tcp_interview.html#%E6%97%A2%E7%84%B6-ip-%E5%B1%82%E4%BC%9A%E5%88%86%E7%89%87-%E4%B8%BA%E4%BB%80%E4%B9%88-tcp-%E5%B1%82%E8%BF%98%E9%9C%80%E8%A6%81-mss-%E5%91%A2) - [4.2 TCP 重传、滑动窗口、流量控制、拥塞控制 | 小林coding](https://xiaolincoding.com/network/3_tcp/tcp_feature.html#%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3) - [TCP/IP协议 (图解+秒懂+史上最全) - 疯狂创客圈 - 博客园](https://www.cnblogs.com/crazymakercircle/p/14499211.html) # Footnotes [^1]: 《Unix 网络编程》P136、P160 [^2]: [4.22 TCP 四次挥手,可以变成三次吗? \| 小林coding](https://xiaolincoding.com/network/3_tcp/tcp_three_fin.html#%E4%BB%80%E4%B9%88%E6%83%85%E5%86%B5%E4%BC%9A%E5%87%BA%E7%8E%B0%E4%B8%89%E6%AC%A1%E6%8C%A5%E6%89%8B) [^3]: [4.15 TCP Keepalive 和 HTTP Keep-Alive 是一个东西吗? | 小林coding](https://xiaolincoding.com/network/3_tcp/tcp_http_keepalive.html#tcp-%E7%9A%84-keepalive) [^4]: 《Unix 网络编程》P157 [^5]: 《Unix 网络编程》P163 [^6]: [4.14 HTTPS 中 TLS 和 TCP 能同时握手吗? | 小林coding](https://xiaolincoding.com/network/3_tcp/tcp_tls.html#tcp-fast-open) [^7]: [4.20 没有 accept,能建立 TCP 连接吗? \| 小林coding](https://xiaolincoding.com/network/3_tcp/tcp_no_accpet.html#%E5%8D%8A%E8%BF%9E%E6%8E%A5%E9%98%9F%E5%88%97%E3%80%81%E5%85%A8%E8%BF%9E%E6%8E%A5%E9%98%9F%E5%88%97%E6%98%AF%E4%BB%80%E4%B9%88)