「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 通过缓冲暂存数据,实现 “批量传输”。
I/O 缓冲的核心作用:
- 避免应用程序频繁调用内核(减少上下文切换开销);
- 适配应用程序与网络的速度差异(如应用写数据快,但网络传输慢);
- 为 TCP 的重传、流量控制等机制提供数据暂存空间。
I/O 缓冲的特性:
- I/O 缓冲在每个套接字中 单独存在;
- I/O 缓冲在创建套接字时自动生成;
- 关闭套接字后,依旧会继续传递 输出缓冲(发送缓冲) 中遗留的数据;
- 关闭套接字后,输入缓冲(接收缓冲)中的数据会丢失。
1.2 发送缓冲的工作流程
-
应用程序调用
write()
/send()
向 TCP 套接字写入数据时,数据并非直接发送到网络,而是 先被拷贝到发送缓冲; -
TCP 协议从发送缓冲中提取数据,按照 “MSS(最大分段大小)” 拆分成长度合适的 TCP 段(Segment),并添加 TCP 头后交给 IP 层;
-
发送后,TCP 会 暂存已发送但未确认的数据(留在发送缓冲),直到收到接收方的 ACK(确认)后,才从缓冲中删除该部分数据;
-
若发送缓冲已满:
- 阻塞式套接字:应用程序会阻塞,直到缓冲有空闲空间;
- 非阻塞式套接字:会立即返回错误(如
EAGAIN
),提示应用 “暂时无法写入”。
1.3 接收缓冲的工作流程
-
网络中的 TCP 段到达后,IP 层剥离 IP 头,将 TCP 段交给 TCP 协议;
-
TCP 验证数据完整性(校验和)、排序(按序列号)、去重后,将数据存入 接收缓冲;
-
应用程序调用
read()
/recv()
时,从接收缓冲中读取数据; -
若接收缓冲为空:
- 阻塞式套接字:应用程序会阻塞,直到有数据到达;
- 非阻塞式套接字:立即返回错误(如
EAGAIN
);
-
关键机制:若接收缓冲已满(应用程序未及时读取),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 (客户端初始序列号) |
客户端:CLOSED → SYN_SENT |
2 | 服务器 | SYN+ACK(同步确认数据) | 控制位:SYN=1 (服务器同步)、ACK=1 (确认客户端SYN)序列号: Seq = ISN_S (服务器初始序列号)确认号: Ack = ISN_C + 1 (确认客户端的ISN,期望下一个字节是ISN_C+1) |
服务器:LISTEN → SYN_RCVD |
3 | 客户端 | ACK(确认数据) | 控制位:ACK=1 (确认服务器SYN)序列号: Seq = ISN_C + 1 (延续客户端序列号)确认号: Ack = ISN_S + 1 (确认服务器的ISN,期望下一个字节是ISN_S+1) |
客户端:SYN_SENT → ESTABLISHED ;服务器: SYN_RCVD → ESTABLISHED |
TCP 套接字的三次握手(建立连接) 示例如上图所示:
-
客户端向服务器请求连接,发送消息
[SYN] Seq: 1000, Ack: -
。该消息初始序列号 Seq 为
1000
,Ack 为空:当前客户端传递的数据包序号为 1000,如果服务端接收无误,请通知客户端后续传递 1001 号数据包。
这是首次请求连接时使用的消息,又称为 SYN,表示收发数据前传输的同步消息。
-
服务端接下来向客户端传递消息
[SYN + ACK] Seq: 2000, Ack: 1001
。该消息初始序列号 Seq 为
2000
:当前服务端传递的数据包号为 2000,如果客户端接收无误,请通知服务端后续传递 2001 号数据包。
Ack 为
1001
(确认客户端的序列号,期望下一个字节为这个序列号 +1):刚才客户端传递的 1000 号数据包接收无误,后续请传递 1001 号数据包。
简单来说,这个消息就是 服务端对客户端首次传输数据包的 确认消息(
Ack: 1001
)和 为服务端传输数据做准备的 同步消息(Seq: 2000
)。 -
最后客户端向服务端传递消息
[ACK] Seq: 1001, Ack: 2001
。TCP 连接过程中每次发送数据包需要分配序列号,并向对方通报这个序列号(这都是为了防止数据丢失,并且可以在数据丢失时查看并重传丢失的数据包)
第三次握手发送数据包的序列号为之前
1000
的基础上 +1,也就是分配的1001
(延续客户端序列号)。Ack 为
2001
(确认服务器的序列号,期望下一个字节这个序列号 +1)刚才服务端传递的 2000 号数据包接收无误,后续可以传输 2001 号数据包。
至此,客户端和服务端确认彼此通信准备就绪。
2.2 为什么需要三次握手
-
防止已失效的连接请求数据段
考虑以下这种情况:
1
2客户端发送SYN包1 → 网络延迟 → 客户端重发SYN包2 → 建立连接 → 传输完成关闭
然后延迟的SYN包1到达服务器如果只有两次握手:
- 服务器收到延迟的SYN包1,直接进入ESTABLISHED状态
- 服务器认为连接已建立,等待客户端发送数据
- 但客户端早已关闭连接,不会发送任何数据
- 结果:造成服务器资源浪费,出现 “半连接” 或 “僵尸连接”
而三次握手可以让客户端 拒绝意外的 SYN+ACK 连接请求:
1
客户端收到意外的SYN-ACK → 发送RST复位报文 → 服务器清除连接
-
同步双方的初始序列号(ISN)
TCP 使用序列号保证数据包的顺序以及检测重复数据包,实现可靠的数据传输。
1
2
3第一次握手: 客户端发送 ISN = x
第二次握手: 服务器确认客户端的ISN(x),并发送自己的ISN = y
第三次握手: 客户端确认服务器的ISN(y)如果只有两次握手:
- 服务器知道客户端的 ISN,但客户端不知道服务器的 ISN
- 服务器发送的数据可能被客户端当作无效数据丢弃
2.3 三次握手与 Linux TCP 实现对应关系
1 |
|
3 TCP 数据交换(数据通信)
连接建立后(双方处于ESTABLISHED状态),进入数据通信阶段。TCP 通过一系列机制保证 “可靠、有序、无丢失” 传输,核心机制包括 序列号与确认号、重传机制、流量控制、拥塞控制。
3.1 序列号(Seq)与确认号(Ack)
TCP 将传输的数据视为 “字节流”,每个字节都有唯一的序列号,核心规则:
- 序列号(Seq):当前发送的数据段中,第一个字节的序号(而不是段的编号);
- 确认号(Ack):接收方期望收到的“下一个字节的序号”,若
Ack=N
,代表“序号≤N-1
的字节已全部收到”。
一个简化的示例如下:
- 客户端向服务器发送 100 字节数据,
Seq=100
(第一个字节序号 100),数据范围是100~199; - 服务器收到后,回复
Ack=200
(确认 100~199 已收到,期望下一个字节是 200),同时可携带服务器向客户端发送的数据(如Seq=700
,数据范围 700~799); - 客户端收到服务器的
Ack=200
后,确认数据已送达,后续发送的数据 Seq 从 200 开始。
3.2 重传机制
若发送方未收到接收方的 ACK,会认为数据丢失,触发 重传,主要有两种方式:
-
超时重传(RTO):
- 发送方发送数据后,启动“超时计时器”(RTO,Retransmission Timeout,根据网络延迟动态调整);
- 若计时器到期前未收到 ACK,重传该数据,并将 RTO 翻倍(指数退避,避免频繁重传加剧网络负担)。
-
快速重传(Fast Retransmit):
- 若接收方收到“失序数据”(如期望
Seq=200
,却收到Seq=300
),会反复发送“重复ACK”(Ack=200
); - 发送方若收到 3个连续的重复ACK,无需等待 RTO 超时,立即重传丢失的数据(如
Seq=200~299
),提升效率。
- 若接收方收到“失序数据”(如期望
3.3 流量控制
流量控制是 端到端 的控制(仅客户端与服务器之间),目的是“让发送方的发送速度匹配接收方的接收速度”,核心是 滑动窗口(Sliding Window) 中的“接收窗口”:
- 接收方在 ACK 报文中,通过 “窗口大小(Window Size)” 字段告知发送方:“我当前接收缓冲还能容纳多少字节的数据”;
- 发送方的 “发送窗口” 最大不能超过接收方告知的 “接收窗口”,若接收窗口为 0,发送方会停止发送(仅定期发送“窗口探测报文”,等待接收方更新窗口)。
3.4 拥塞控制
拥塞控制是 全局 的控制(考虑整个网络的负载),目的是 避免发送方发送过快导致网络拥塞(如路由器缓存满、丢包),核心是通过 拥塞窗口(cwnd,Congestion Window) 动态调整发送速度,分为四个阶段:
- 慢启动(Slow Start):cwnd从1开始,每收到一个ACK就翻倍(指数增长),直到cwnd达到“慢启动阈值(ssthresh)”;
- 拥塞避免(Congestion Avoidance):cwnd达到ssthresh后,改为“每RTT增加1”(线性增长),避免快速拥塞;
- 快重传(Fast Retransmit):收到3个重复ACK后,立即重传,并将ssthresh设为当前cwnd的一半,cwnd设为ssthresh+3;
- 快恢复(Fast Recovery):重传后,cwnd按“线性增长”恢复,而非重新慢启动,提升效率。
4 TCP 四次挥手(关闭连接)
TCP是“全双工”协议(双方可同时发送数据),因此关闭连接需“双向分别关闭”,即 四次挥手(Four-Way Handshake)。
4.1 四次挥手的流程及状态变化
“客户端” 主动发起关闭(数据已发送完毕),四次挥手流程如下:
步骤 | 发起方 | 报文类型 | 核心内容(TCP头关键字段) | 双方状态变化 |
---|---|---|---|---|
1 | 客户端 | FIN(结束报文) | 控制位:FIN=1 (请求关闭客户端→服务器的连接)序列号: Seq = N (客户端当前已发字节的最后一个序号+1) |
客户端:ESTABLISHED → FIN_WAIT_1 |
2 | 服务器 | ACK(确认报文) | 控制位:ACK=1 (确认客户端的FIN)序列号: Seq = M (服务器当前已发字节的最后一个序号+1)确认号: Ack = N + 1 (确认客户端的FIN) |
服务器:ESTABLISHED → CLOSE_WAIT ;客户端: FIN_WAIT_1 → FIN_WAIT_2 |
3 | 服务器 | FIN(结束报文) | 控制位:FIN=1 (请求关闭服务器→客户端的连接,服务器需先发送完剩余数据)序列号: Seq = M (延续服务器序列号)确认号: Ack = N + 1 (重复确认) |
服务器:CLOSE_WAIT → LAST_ACK |
4 | 客户端 | ACK(确认报文) | 控制位:ACK=1 (确认服务器的FIN)序列号: Seq = N + 1 (延续客户端序列号)确认号: Ack = M + 1 (确认服务器的FIN) |
客户端:FIN_WAIT_2 → TIME_WAIT ;服务器: LAST_ACK → CLOSED (收到ACK后) |
TCP 套接字的四次挥手(关闭连接) 示例如上图所示:
-
客户端发送 FIN 报文给服务器,请求关闭连接。消息为
Seq: 5000
,进入FIN_WAIT_1
状态。表示客户端没有数据要发送了,但是还可以接收数据。 -
服务器收到 FIN 后,发送 ACK 报文,其中确认号
Ack: 5001
(即5000 + 1
),序列号为8000
。将其发送给客户端,进入CLOSE_WAIT
状态。此时,服务器 可能还有数据要发送给客户端,所以不会立即发送 FIN。
客户端收到服务器的 ACK 后,进入
FIN_WAIT_2
状态,等待服务器发送 FIN 报文。 -
服务器 将剩余数据发送完毕后,发送 FIN 报文(序列号为
Seq=8001
,即8000 + 1
)给客户端,进入LAST_ACK
状态。(如果之前还有数据发送给客户端,序列号可能是> 8001
的其他数) -
客户端收到 FIN 后,发送 ACK 报文(确认号
Ack=8002
,即8001 + 1
)给服务器,进入TIME_WAIT
状态,等待 2MSL(最大报文段生存时间)后关闭连接(最终CLOSED
)。
4.2 为什么需要四次挥手
如果尝试三次挥手:
1 |
|
但是,服务器可能还有数据要发送:
1 |
|
- ACK:确认收到对方的 FIN
- FIN:表示本方向没有数据要发送了
4.3 四次挥手与 Linux TCP 实现对应关系
1 |
|
5 TCP 其他补充说明
5.1 半关闭
TCP 支持“单向关闭”(半关闭,Half-Close),即 一方关闭发送通道后,仍可接收对方的数据,对应四次挥手的前两步:
- 客户端发送 FIN(步骤1)后,进入
FIN_WAIT_1
→FIN_WAIT_2
,此时客户端 无法再向服务器发送数据,但仍可接收服务器的数据; - 直到服务器发送 FIN(步骤3),客户端确认后进入TIME_WAIT,才完全关闭接收通道。
5.2 滑动窗口
滑动窗口(Sliding Window) 是TCP实现“批量传输”和“流量控制”的核心,本质是“允许发送方在未收到确认的情况下,连续发送多个数据段”,分为 发送窗口 和 接收窗口。
- 发送窗口结构(发送方视角)
发送窗口是“已发送但未确认”+“可发送但未发送”的字节范围,分为4个区域:
- 已发送且已确认:无需关注;
- 已发送但未确认:需等待ACK,超时则重传;
- 可发送但未发送:未超过接收方窗口大小,可立即发送;
- 不可发送:超过接收方窗口大小,需等待接收方更新窗口。
- 窗口滑动规则
- 当发送方收到某部分数据的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请求、文件数据) |