「Linux 网络编程」TCP 内部工作原理

1 TCP 套接字中的 I/O 缓冲

TCP 套接字(Socket)是应用程序与 TCP 协议交互的接口,而 I/O 缓冲(I/O Buffer) 是 TCP 实现 “异步传输” 和 “流量适配” 的核心机制,分为 发送缓冲接收缓冲,二者均由操作系统内核管理。


1.1 TCP 缓冲的核心作用

TCP 套接字的收发数据是无边界的。服务器端调用 1 次 write() 函数传输 100 字节的数据,客户端可能分几批接收数据(例如通过 4 次 read() 函数每次读取 25 字节的数据)。服务端一次性传输了 100 字节的数据,客户端一开始接收了 25 字节,那么剩下的 75 字节存放在哪里呢?

TCP 中调用 write() 函数后 并不是 就立即传输数据,调用 read() 函数后也并非马上接收数据。实际上,TCP 通过缓冲暂存数据,实现 “批量传输”。

TCP 套接字中的 I/O 缓冲

I/O 缓冲的核心作用

  • 避免应用程序频繁调用内核(减少上下文切换开销);
  • 适配应用程序与网络的速度差异(如应用写数据快,但网络传输慢);
  • 为 TCP 的重传、流量控制等机制提供数据暂存空间。

I/O 缓冲的特性

  • I/O 缓冲在每个套接字中 单独存在
  • I/O 缓冲在创建套接字时自动生成;
  • 关闭套接字后,依旧会继续传递 输出缓冲(发送缓冲) 中遗留的数据;
  • 关闭套接字后,输入缓冲(接收缓冲)中的数据会丢失。

1.2 发送缓冲的工作流程

  1. 应用程序调用 write()/send() 向 TCP 套接字写入数据时,数据并非直接发送到网络,而是 先被拷贝到发送缓冲

  2. TCP 协议从发送缓冲中提取数据,按照 “MSS(最大分段大小)” 拆分成长度合适的 TCP 段(Segment),并添加 TCP 头后交给 IP 层;

  3. 发送后,TCP 会 暂存已发送但未确认的数据(留在发送缓冲),直到收到接收方的 ACK(确认)后,才从缓冲中删除该部分数据;

  4. 若发送缓冲已满:

    • 阻塞式套接字:应用程序会阻塞,直到缓冲有空闲空间;
    • 非阻塞式套接字:会立即返回错误(如 EAGAIN),提示应用 “暂时无法写入”。

1.3 接收缓冲的工作流程

  1. 网络中的 TCP 段到达后,IP 层剥离 IP 头,将 TCP 段交给 TCP 协议;

  2. TCP 验证数据完整性(校验和)、排序(按序列号)、去重后,将数据存入 接收缓冲

  3. 应用程序调用 read()/recv() 时,从接收缓冲中读取数据;

  4. 若接收缓冲为空:

    • 阻塞式套接字:应用程序会阻塞,直到有数据到达;
    • 非阻塞式套接字:立即返回错误(如 EAGAIN);
  5. 关键机制:若接收缓冲已满(应用程序未及时读取),TCP 会通过 “窗口大小” 告知发送方 减缓发送速度(流量控制的核心——滑动窗口),避免数据丢失。



2 TCP 三次握手(建立连接)

TCP 是 “面向连接” 的协议,连接建立需通过 三次握手(Three-Way Handshake) 实现。

三次握手(Three-Way Handshake) 的核心目的:

  • 确保双方都具备 “发送” 和 “接收” 的能力;
  • 同步双方的 初始序列号(ISN,Initial Sequence Number)(TCP 通过序列号保证数据有序性和不丢失)。

2.1 三次握手的流程及状态变化

“客户端” 主动发起连接,“服务器” 被动监听(状态为LISTEN)。三次握手的流程如下:

步骤 发起方 报文类型 核心内容(TCP头关键字段) 双方状态变化
1 客户端 SYN(同步数据) 控制位SYN=1(请求同步)
序列号Seq = ISN_C(客户端初始序列号)
客户端CLOSEDSYN_SENT
2 服务器 SYN+ACK(同步确认数据) 控制位SYN=1(服务器同步)、ACK=1(确认客户端SYN)
序列号Seq = ISN_S(服务器初始序列号)
确认号Ack = ISN_C + 1(确认客户端的ISN,期望下一个字节是ISN_C+1)
服务器LISTENSYN_RCVD
3 客户端 ACK(确认数据) 控制位ACK=1(确认服务器SYN)
序列号Seq = ISN_C + 1(延续客户端序列号)
确认号Ack = ISN_S + 1(确认服务器的ISN,期望下一个字节是ISN_S+1)
客户端SYN_SENTESTABLISHED
服务器SYN_RCVDESTABLISHED

TCP 套接字的三次握手(建立连接)

TCP 套接字的三次握手(建立连接) 示例如上图所示:

  1. 客户端向服务器请求连接,发送消息 [SYN] Seq: 1000, Ack: -

    该消息初始序列号 Seq 为 1000,Ack 为空:

    当前客户端传递的数据包序号为 1000,如果服务端接收无误,请通知客户端后续传递 1001 号数据包。

    这是首次请求连接时使用的消息,又称为 SYN,表示收发数据前传输的同步消息。

  2. 服务端接下来向客户端传递消息 [SYN + ACK] Seq: 2000, Ack: 1001

    该消息初始序列号 Seq 为 2000

    当前服务端传递的数据包号为 2000,如果客户端接收无误,请通知服务端后续传递 2001 号数据包。

    Ack 为 1001(确认客户端的序列号,期望下一个字节为这个序列号 +1):

    刚才客户端传递的 1000 号数据包接收无误,后续请传递 1001 号数据包。

    简单来说,这个消息就是 服务端对客户端首次传输数据包的 确认消息Ack: 1001)和 为服务端传输数据做准备的 同步消息Seq: 2000)。

  3. 最后客户端向服务端传递消息 [ACK] Seq: 1001, Ack: 2001

    TCP 连接过程中每次发送数据包需要分配序列号,并向对方通报这个序列号(这都是为了防止数据丢失,并且可以在数据丢失时查看并重传丢失的数据包)

    第三次握手发送数据包的序列号为之前 1000 的基础上 +1,也就是分配的 1001(延续客户端序列号)。

    Ack 为 2001(确认服务器的序列号,期望下一个字节这个序列号 +1)

    刚才服务端传递的 2000 号数据包接收无误,后续可以传输 2001 号数据包。

    至此,客户端和服务端确认彼此通信准备就绪。


2.2 为什么需要三次握手

  1. 防止已失效的连接请求数据段

    考虑以下这种情况:

    1
    2
    客户端发送SYN包1 → 网络延迟 → 客户端重发SYN包2 → 建立连接 → 传输完成关闭
    然后延迟的SYN包1到达服务器

    如果只有两次握手:

    • 服务器收到延迟的SYN包1,直接进入ESTABLISHED状态
    • 服务器认为连接已建立,等待客户端发送数据
    • 但客户端早已关闭连接,不会发送任何数据
    • 结果:造成服务器资源浪费,出现 “半连接” 或 “僵尸连接”

    而三次握手可以让客户端 拒绝意外的 SYN+ACK 连接请求

    1
    客户端收到意外的SYN-ACK → 发送RST复位报文 → 服务器清除连接
  2. 同步双方的初始序列号(ISN)

    TCP 使用序列号保证数据包的顺序以及检测重复数据包,实现可靠的数据传输。

    1
    2
    3
    第一次握手: 客户端发送 ISN = x
    第二次握手: 服务器确认客户端的ISN(x),并发送自己的ISN = y
    第三次握手: 客户端确认服务器的ISN(y)

    如果只有两次握手:

    • 服务器知道客户端的 ISN,但客户端不知道服务器的 ISN
    • 服务器发送的数据可能被客户端当作无效数据丢弃

2.3 三次握手与 Linux TCP 实现对应关系

1
2
3
4
5
6
7
8
9
// 客户端
connect(sockfd, ...); // 触发SYN发送

// 服务器
listen(sockfd, ...); // 进入LISTEN状态
accept(sockfd, ...); // 等待内核完成三次握手

// 内核维护的连接状态
// SYN_SENT → SYN_RCVD → ESTABLISHED


3 TCP 数据交换(数据通信)

连接建立后(双方处于ESTABLISHED状态),进入数据通信阶段。TCP 通过一系列机制保证 “可靠、有序、无丢失” 传输,核心机制包括 序列号与确认号、重传机制、流量控制、拥塞控制


3.1 序列号(Seq)与确认号(Ack)

TCP 将传输的数据视为 “字节流”,每个字节都有唯一的序列号,核心规则:

  • 序列号(Seq):当前发送的数据段中,第一个字节的序号(而不是段的编号);
  • 确认号(Ack):接收方期望收到的“下一个字节的序号”,若 Ack=N,代表“序号 ≤N-1 的字节已全部收到”。

一个简化的示例如下:

  1. 客户端向服务器发送 100 字节数据,Seq=100(第一个字节序号 100),数据范围是100~199;
  2. 服务器收到后,回复 Ack=200(确认 100~199 已收到,期望下一个字节是 200),同时可携带服务器向客户端发送的数据(如 Seq=700,数据范围 700~799);
  3. 客户端收到服务器的 Ack=200 后,确认数据已送达,后续发送的数据 Seq 从 200 开始。

TCP 数据通信示例


3.2 重传机制

若发送方未收到接收方的 ACK,会认为数据丢失,触发 重传,主要有两种方式:

  • 超时重传(RTO)

    1. 发送方发送数据后,启动“超时计时器”(RTO,Retransmission Timeout,根据网络延迟动态调整);
    2. 若计时器到期前未收到 ACK,重传该数据,并将 RTO 翻倍(指数退避,避免频繁重传加剧网络负担)。
  • 快速重传(Fast Retransmit)

    1. 若接收方收到“失序数据”(如期望 Seq=200,却收到 Seq=300),会反复发送“重复ACK”(Ack=200);
    2. 发送方若收到 3个连续的重复ACK,无需等待 RTO 超时,立即重传丢失的数据(如 Seq=200~299),提升效率。

3.3 流量控制

流量控制是 端到端 的控制(仅客户端与服务器之间),目的是“让发送方的发送速度匹配接收方的接收速度”,核心是 滑动窗口(Sliding Window) 中的“接收窗口”:

  • 接收方在 ACK 报文中,通过 “窗口大小(Window Size)” 字段告知发送方:“我当前接收缓冲还能容纳多少字节的数据”;
  • 发送方的 “发送窗口” 最大不能超过接收方告知的 “接收窗口”,若接收窗口为 0,发送方会停止发送(仅定期发送“窗口探测报文”,等待接收方更新窗口)。

3.4 拥塞控制

拥塞控制是 全局 的控制(考虑整个网络的负载),目的是 避免发送方发送过快导致网络拥塞(如路由器缓存满、丢包),核心是通过 拥塞窗口(cwnd,Congestion Window) 动态调整发送速度,分为四个阶段:

  1. 慢启动(Slow Start):cwnd从1开始,每收到一个ACK就翻倍(指数增长),直到cwnd达到“慢启动阈值(ssthresh)”;
  2. 拥塞避免(Congestion Avoidance):cwnd达到ssthresh后,改为“每RTT增加1”(线性增长),避免快速拥塞;
  3. 快重传(Fast Retransmit):收到3个重复ACK后,立即重传,并将ssthresh设为当前cwnd的一半,cwnd设为ssthresh+3;
  4. 快恢复(Fast Recovery):重传后,cwnd按“线性增长”恢复,而非重新慢启动,提升效率。


4 TCP 四次挥手(关闭连接)

TCP是“全双工”协议(双方可同时发送数据),因此关闭连接需“双向分别关闭”,即 四次挥手(Four-Way Handshake)


4.1 四次挥手的流程及状态变化

“客户端” 主动发起关闭(数据已发送完毕),四次挥手流程如下:

步骤 发起方 报文类型 核心内容(TCP头关键字段) 双方状态变化
1 客户端 FIN(结束报文) 控制位FIN=1(请求关闭客户端→服务器的连接)
序列号Seq = N(客户端当前已发字节的最后一个序号+1)
客户端ESTABLISHEDFIN_WAIT_1
2 服务器 ACK(确认报文) 控制位ACK=1(确认客户端的FIN)
序列号Seq = M(服务器当前已发字节的最后一个序号+1)
确认号Ack = N + 1(确认客户端的FIN)
服务器ESTABLISHEDCLOSE_WAIT
客户端FIN_WAIT_1FIN_WAIT_2
3 服务器 FIN(结束报文) 控制位FIN=1(请求关闭服务器→客户端的连接,服务器需先发送完剩余数据)
序列号Seq = M(延续服务器序列号)
确认号Ack = N + 1(重复确认)
服务器CLOSE_WAITLAST_ACK
4 客户端 ACK(确认报文) 控制位ACK=1(确认服务器的FIN)
序列号Seq = N + 1(延续客户端序列号)
确认号Ack = M + 1(确认服务器的FIN)
客户端FIN_WAIT_2TIME_WAIT
服务器LAST_ACKCLOSED(收到ACK后)

TCP 套接字的四次挥手(关闭连接)

TCP 套接字的四次挥手(关闭连接) 示例如上图所示:

  1. 客户端发送 FIN 报文给服务器,请求关闭连接。消息为 Seq: 5000,进入 FIN_WAIT_1 状态。表示客户端没有数据要发送了,但是还可以接收数据。

  2. 服务器收到 FIN 后,发送 ACK 报文,其中确认号 Ack: 5001(即 5000 + 1),序列号为 8000。将其发送给客户端,进入CLOSE_WAIT 状态。

    此时,服务器 可能还有数据要发送给客户端,所以不会立即发送 FIN。

    客户端收到服务器的 ACK 后,进入 FIN_WAIT_2 状态,等待服务器发送 FIN 报文。

  3. 服务器 将剩余数据发送完毕后发送 FIN 报文(序列号为 Seq=8001,即 8000 + 1)给客户端,进入 LAST_ACK 状态。(如果之前还有数据发送给客户端,序列号可能是 > 8001 的其他数)

  4. 客户端收到 FIN 后,发送 ACK 报文(确认号 Ack=8002,即 8001 + 1)给服务器,进入 TIME_WAIT 状态,等待 2MSL(最大报文段生存时间)后关闭连接(最终 CLOSED)。


4.2 为什么需要四次挥手

如果尝试三次挥手:

1
2
3
客户端 → FIN → 服务器    (关闭客户端→服务器方向)
服务器 → FIN+ACK → 客户端 (关闭服务器→客户端方向 + 确认)
客户端 → ACK → 服务器 (确认)

但是,服务器可能还有数据要发送

1
2
3
4
客户端: "我没有数据要发了" (FIN)
服务器: "收到,但我还有最后一些数据要发给你" (先发数据,再发FIN)
服务器: "数据发完了,我也没有数据了" (FIN)
客户端: "收到,再见" (ACK)
  • ACK:确认收到对方的 FIN
  • FIN:表示本方向没有数据要发送了

4.3 四次挥手与 Linux TCP 实现对应关系

1
2
3
4
5
6
7
8
9
10
11
12
13
// 客户端 调用 close 发送 FIN
close(); // 变为 FIN_WAIT_1 状态

// 服务器接收到 FIN, recv() = 0
recv() == 0 // 服务器变为 CLOSE_WAIT 状态

// 接收到服务器的 ACK 后,内核自动处理,客户端变为 FIN_WAIT_2 状态

// 服务器 调用 close 发送 FIN
close(); // 变为 LAST_ACK 状态

// 客户端接收到 FIN 后,发送 ACK 报文,进入 TIME_WAIT 状态
// 等待 2MSL(最大报文段生存时间)后关闭连接(最终 `CLOSED`)。


5 TCP 其他补充说明

5.1 半关闭

TCP 支持“单向关闭”(半关闭,Half-Close),即 一方关闭发送通道后,仍可接收对方的数据,对应四次挥手的前两步:

  • 客户端发送 FIN(步骤1)后,进入 FIN_WAIT_1FIN_WAIT_2,此时客户端 无法再向服务器发送数据,但仍可接收服务器的数据;
  • 直到服务器发送 FIN(步骤3),客户端确认后进入TIME_WAIT,才完全关闭接收通道。

5.2 滑动窗口

滑动窗口(Sliding Window) 是TCP实现“批量传输”和“流量控制”的核心,本质是“允许发送方在未收到确认的情况下,连续发送多个数据段”,分为 发送窗口接收窗口

  1. 发送窗口结构(发送方视角)

发送窗口是“已发送但未确认”+“可发送但未发送”的字节范围,分为4个区域:

  • 已发送且已确认:无需关注;
  • 已发送但未确认:需等待ACK,超时则重传;
  • 可发送但未发送:未超过接收方窗口大小,可立即发送;
  • 不可发送:超过接收方窗口大小,需等待接收方更新窗口。
  1. 窗口滑动规则
  • 当发送方收到某部分数据的ACK后,发送窗口 向右滑动(如收到 ACK=200,窗口起点从100移到200);
  • 接收方的接收窗口也会随“应用程序读取数据”向右滑动(如应用读取了100字节,接收窗口大小增加100)。

核心价值

相比“停止-等待协议”(发一个等确认再发下一个),滑动窗口能大幅提升吞吐量(如窗口大小为 10,可连续发 10 个段再等确认)。


5.3 TCP 数据包格式

TCP 段由 TCP头数据部分 组成,TCP 头长度可变(20~60 字节,取决于“选项”字段),关键字段如下(按顺序):

字段 长度(字节) 核心作用
源端口号 2 标识发送方的应用程序(如HTTP默认80,HTTPS默认443)
目的端口号 2 标识接收方的应用程序
序列号(Seq) 4 当前段第一个字节的序号(数据通信时);初始序列号(三次握手时)
确认号(Ack) 4 期望接收的下一个字节的序号(仅ACK=1时有效)
数据偏移 4位 TCP头的长度(单位:4字节),如值为5表示TCP头长20字节(5×4)
保留位 6位 预留,均为0
控制位(Flags) 6位 关键控制标志:
- SYN=1:同步序列号(三次握手)
- ACK=1:确认号有效
- FIN=1:请求关闭连接(四次挥手)
- RST=1:重置连接(如连接异常)
- PSH=1:提示应用程序立即读取数据
- URG=1:紧急数据(配合紧急指针)
窗口大小 2 接收方当前的接收窗口大小(流量控制核心)
校验和 2 校验TCP头+数据,检测数据是否损坏
紧急指针 2 URG=1时,标识紧急数据的末尾位置
选项(Options) 可变(0~40字节) 可选扩展:
- MSS:最大分段大小(双方协商,避免IP分片)
- SACK:选择性确认(只确认丢失的数据,提升重传效率)
- TS:时间戳(计算RTT、避免序列号回绕)
填充(Padding) 可变 确保TCP头长度为4字节的整数倍(对齐)
数据部分 可变 应用程序发送的数据(如HTTP请求、文件数据)

「Linux 网络编程」TCP 内部工作原理
https://marisamagic.github.io/2025/10/07/20251007/
作者
MarisaMagic
发布于
2025年10月7日
许可协议