目录
[一、传输控制协议 (TCP) 简述与本质](#一、传输控制协议 (TCP) 简述与本质)
[1. TCP简要介绍](#1. TCP简要介绍)
[2. 网络 I/O 函数的本质](#2. 网络 I/O 函数的本质)
[3. TCP 的"控制"体现在哪里?](#3. TCP 的“控制”体现在哪里?)
[二、 TCP 协议段格式深度解析](#二、 TCP 协议段格式深度解析)
[1. 基本介绍(⭐⭐⭐)](#1. 基本介绍(⭐⭐⭐))
[2. 报头与有效载荷的分离](#2. 报头与有效载荷的分离)
[3. 序号和确认序号](#3. 序号和确认序号)
[4. 六个控制标记位](#4. 六个控制标记位)
[1. 判断数据是否丢失](#1. 判断数据是否丢失)
[2. 超时重传的等待时间 (RTO)](#2. 超时重传的等待时间 (RTO))
[1. 操作系统对连接的管理(⭐⭐⭐)](#1. 操作系统对连接的管理(⭐⭐⭐))
[2. 三次握手](#2. 三次握手)
[2.1 过程](#2.1 过程)
[2.2 为什么必须是三次?(⭐⭐⭐)](#2.2 为什么必须是三次?(⭐⭐⭐))
[3. 四次挥手](#3. 四次挥手)
[3.1 过程](#3.1 过程)
[3.2 为什么是四次?](#3.2 为什么是四次?)
[4.状态流转与 TIME_WAIT 的深度解析(⭐⭐⭐)](#4.状态流转与 TIME_WAIT 的深度解析(⭐⭐⭐))
[4.2 为什么需要 TIME_WAIT 状态?(⭐⭐⭐)](#4.2 为什么需要 TIME_WAIT 状态?(⭐⭐⭐))
[六、 全连接队列与 Listen 的 Backlog](#六、 全连接队列与 Listen 的 Backlog)
[1. 半连接队列与全连接队列](#1. 半连接队列与全连接队列)
[2. Listen 的第二个参数](#2. Listen 的第二个参数)
[3. SYN 洪水攻击 (SYN Flood)](#3. SYN 洪水攻击 (SYN Flood))
[4. backlog的长度控制(⭐⭐⭐)](#4. backlog的长度控制(⭐⭐⭐))
[1. 核心机制](#1. 核心机制)
[2. 窗口探测与更新](#2. 窗口探测与更新)
[3. 窗口扩大因子](#3. 窗口扩大因子)
[1. 窗口在哪里?区域是如何划分的?](#1. 窗口在哪里?区域是如何划分的?)
[2. 丢包了怎么理解滑动窗口?](#2. 丢包了怎么理解滑动窗口?)
[3. 窗口的移动与越界(⭐⭐⭐)](#3. 窗口的移动与越界(⭐⭐⭐))
[1. 延迟应答 (Delayed ACK)](#1. 延迟应答 (Delayed ACK))
[2. 捎带应答](#2. 捎带应答)
[1. 核心共识](#1. 核心共识)
[2. 慢启动与动态调整(⭐⭐⭐)](#2. 慢启动与动态调整(⭐⭐⭐))
[1. 面向字节流的本质](#1. 面向字节流的本质)
[2. 粘包问题](#2. 粘包问题)
[十二、TCP 异常问题剖析](#十二、TCP 异常问题剖析)
[十三、如何用 UDP 实现可靠传输?(⭐⭐⭐)](#十三、如何用 UDP 实现可靠传输?(⭐⭐⭐))
[1. 可靠性与性能(⭐⭐⭐)](#1. 可靠性与性能(⭐⭐⭐))
[2. 六种最核心的定时器](#2. 六种最核心的定时器)
[2.1 超时重传定时器](#2.1 超时重传定时器)
[2.2. 坚持定时器 (零窗口探测定时器)](#2.2. 坚持定时器 (零窗口探测定时器))
[2.3 时间等待定时器 (2MSL 定时器)](#2.3 时间等待定时器 (2MSL 定时器))
[2.4 保活定时器](#2.4 保活定时器)
[2.5 延迟应答定时器](#2.5 延迟应答定时器)
[2.6 FIN_WAIT_2 定时器 (孤儿连接定时器)](#2.6 FIN_WAIT_2 定时器 (孤儿连接定时器))
[3. 快重传VS超时重传](#3. 快重传VS超时重传)
一、传输控制协议 (TCP) 简述与本质
1. TCP简要介绍
TCP(Transmission Control Protocol)是一种面向连接的协议:通信双方在交换数据之前必须先建立连接(三次握手),数据传输结束后要释放连接(四次挥手)。它提供了:
-
可靠性:通过确认应答、超时重传、校验和等机制确保数据正确到达。
-
流量控制:让发送方根据接收方的处理能力调整发送速度,避免接收方缓冲区溢出。
-
拥塞控制:当网络出现拥塞时,动态降低发送速率,避免网络瘫痪。
-
全双工通信:连接双方可以同时发送和接收数据。
2. 网络 I/O 函数的本质
在 C++/Linux 网络编程中,应用层调用的 write、read、recv、send,本质上都是拷贝函数。
当你调用
send时,你并没有把数据直接发到网络上,而是将用户态缓冲区的数据拷贝到了操作系统的 TCP 发送缓冲区中。当你调用
recv时,你只是将操作系统的 TCP 接收缓冲区中的数据拷贝到了用户态缓冲区。一旦数据进入内核缓冲区,应用程序的工作就结束了,剩下的所有事情(包括封装、发送、重传、分片等)都交由 OS 内核中的 TCP 协议栈去完全接管。
3. TCP 的"控制"体现在哪里?
因为 OS 接管了缓冲区,TCP 实现了真正的自主决定:
什么时候发送? TCP 会根据网络拥塞情况、对方的接收能力以及 Nagle 算法等自行决定是立即发送还是攒一波再发。
发送多少? 受制于对方的窗口大小(流量控制)和网络的拥塞窗口(拥塞控制)。
出错了怎么办? TCP 会自动进行超时重传、快重传。 这一切对应用层都是透明的。
二、 TCP 协议段格式深度解析
1. 基本介绍(⭐⭐⭐)

- 源/目的端口号:发送端端口号;接收端端口号。
- 序号:报文段中第一个数据字节的序号(初始值随机)
- 确认序号:期望收到的下一个报文段的首字节序号,表示该序号之前的数据都已收到。
- 报头长度:TCP头部长度,单位4字节,取值范围5~15(即20~60字节)
- 保留字段:保留未用。
- 标志位:URG、ACK、PSH、RST、SYN、FIN。
- 检验和:覆盖TCP头部和数据的校验和,计算时加上伪首部。
- 窗口大小:接收方当前剩余接收缓冲区大小,用于流量控制
- 紧急指针:当URG=1时有效,指向紧急数据末尾的偏移量(紧急数据从序号开始到该偏移)
- 选项字段:如MSS、窗口扩大因子、时间戳、SACK等。
2. 报头与有效载荷的分离
TCP 采用 "固定长度 + 自描述字段" 的方式进行分离。
标准报头:前 20 个字节是固定的。
首部长度 (Data Offset):占 4 个比特位。它表示 TCP 报头共有多少个 32 bit(即 4 字节)。
因为 4 个比特位最大能表示的数字是 15,所以 TCP 报头的最大长度是 15 * 4 = 60 字节。
因此,选项(Options)的长度最多是 60 - 20 = 40 字节。
分离过程:内核读取前 20 字节,提取"首部长度"字段,乘以 4 得出报头总长度。跳过这个长度后的所有数据,就是有效载荷。
3. 序号和确认序号

-
序号:TCP 将发送的每一个字节都进行了编号。报头中的序号是该报文段有效载荷的第一个字节的编号。用于解决网络包乱序到达的问题。
-
确认序号:告诉发送端,"这个序号之前的数据我已经全部按序收到了,下一次请从这个序号开始发"。
为什么初始序号 (ISN) 是随机的?(⭐⭐⭐)
安全性:防止黑客伪造 TCP 报文注入到当前连接中(TCP 序列号预测攻击)。
避免历史报文干扰:如果前后两次连接使用了相同的 IP 和端口组,且序列号总是从 0 开始,那么上一次连接中在网络中迷路的"延迟报文"到达后,可能会被误认为是新连接的数据,造成数据错乱。
4. 六个控制标记位
TCP 报文是"多态"的,标记位决定了当前报文的核心意图:
ACK :确认号是否有效。除了最初建立连接的 SYN 包外,正常通信时该位必须为 1。
SYN:同步序列号,用于发起一个连接。
FIN:发送端完成发送任务,请求断开连接。
PSH:催促接收端。提示接收端的 OS 尽快将接收缓冲区的数据推送到应用层(不要再等缓冲区满了才通知)。
RST :复位连接。在三次握手中,Client 发出最后一个 ACK 就认为连接建立并分配资源。但如果这个 ACK 丢了,Server 没有收到。此时 Client 强行发送数据,Server 收到后会发现这是一个尚未建立的连接,于是 Server 会回复一个带有 RST 标志的报文,要求 Client 重新建立连接。
URG :紧急指针有效。TCP 是字节流,正常数据必须排队读取。但如果报文携带 URG 标志,内核会提取紧急指针指向的那个字节(带外数据 OOB)。同时,内核会向应用层进程发送一个
SIGURG信号 。应用程序可以通过注册信号处理函数,并使用recv(fd, buf, len, MSG_OOB)标志位,直接越过排队的数据,立刻把这个紧急字节读出来。
三、确认应答机制
这是 TCP 保证可靠性的最核心机制。 在 TCP 中,没有任何数据是发出去就完事的。只要发送端发出了数据,接收端收到后必须回复一个 ACK 报文(或者在捎带应答中回复)。只有收到了 ACK,发送端才确信数据安全到达;否则,就认为数据丢失了。
在理解TCP的可靠性时,一个常见的误区是认为"确认应答机制保证了双方通信的所有消息都可靠到达"。实际上,TCP的确认应答(ACK)的核心作用只有一个:让发送方知道,它之前发送的某一段数据已经被接收方正确收到了。换句话说,ACK是发送方对自己发送消息的"收据",它并不能直接保证对方发来的消息可靠------那是对方发送方通过自己的ACK来保证的。
这种"单向保证"的设计体现了协议的分治思想:将全双工通信拆分成两个独立的单工通道,分别处理各自的可靠性。每个方向都有自己的序号空间、窗口和ACK机制,互不干扰。这也使得TCP的流量控制和拥塞控制可以针对每个方向独立进行。
四、超时重传机制
1. 判断数据是否丢失
发送端发出数据后,会启动一个定时器。如果等待了很长时间(RTO,Retransmission Timeout)还没有收到确认,就会认为丢包了。 丢包分为两种情况,但对发送端来说表现是一样的(都是没收到 ACK):
数据包丢了:数据根本没到接收端。
ACK 丢了:接收端收到了数据并回复了 ACK,但 ACK 在网络中丢了。如果是这种情况,发送端重传后,接收端会收到重复数据,内核会根据"序号"自动去重,保证应用层不会读到两份一样的数据。
2. 超时重传的等待时间 (RTO)
RTO 不能是固定的。如果网络很好,RTO 太长会导致丢包后恢复极慢;如果网络很差,RTO 太短会导致疯狂重发,加剧网络拥塞。
动态调整 :Linux 内核会持续采样报文的往返时间 (RTT, Round Trip Time),即从发出数据到收到 ACK 的时间差。
内核利用一种平滑加权移动平均算法(如 Jacobson/Karels 算法)根据 RTT 的波动动态计算 RTO。通常情况下,RTO 略大于 RTT ,并且如果连续发生超时重传,RTO 的值会呈指数级增长(例如 2倍、4倍、8倍),避免给已经拥堵的网络火上浇油。
五、连接管理机制
1. 操作系统对连接的管理(⭐⭐⭐)
在 Linux 内核中,所谓的"连接"并不是一条物理存在的线,而是一个内存中的结构体 (如 struct tcp_sock)。
建立连接的本质:通信双方的操作系统各自在内存中开辟了一块空间,创建了维护该连接状态、缓冲区、序号等信息的结构体。
断开连接的本质:释放这些结构体占用的系统资源。
这也意味着,维护连接是需要付出 CPU 和内存成本的。
2. 三次握手

2.1 过程
SYN :客户端随机生成初始序号
seq=x,向服务端发送 SYN 报文,进入SYN_SENT状态。SYN+ACK :服务端收到后,由内核自动回复 SYN+ACK 报文(确认号
ack=x+1,自己的随机序号seq=y),进入SYN_RCVD状态。ACK :客户端收到后,回复 ACK 报文(
ack=y+1),进入ESTABLISHED状态。服务端收到此 ACK 后,也进入ESTABLISHED状态。
2.2 为什么必须是三次?(⭐⭐⭐)
(1) 验证全双工通信: TCP 是双向的。第一次握手证明客户端能发;第二次证明服务端能收也能发;第三次证明客户端能收。三次交互是验证双方"收发能力"都正常的最小次数。
(2) 奇数次连接的"失败成本嫁接": 网络通信中,最后一次发出的报文是不保证一定到达的。如果是三次握手,最后一次发出 ACK 的是客户端 。客户端在发出 ACK 后就认为连接建立,分配了资源;如果这个 ACK 丢了,服务端没有收到,就不会分配资源。这极其重要,因为服务端是"一对多"的,如果把失败的风险和资源浪费(分配了结构体但没真正连上)强加给服务端,服务端的内存很快就会被耗尽。
3. 四次挥手

3.1 过程
FIN :主动断开方(假设是客户端)发送 FIN,进入**
FIN_WAIT_1** 状态。ACK :被动断开方(服务端)收到后回复 ACK,进入
CLOSE_WAIT状态。此时客户端进入FIN_WAIT_2状态。FIN :服务端将底层未发完的数据发完后,也调用
close(),发送 FIN,进入**LAST_ACK** 状态**。**ACK :客户端收到 FIN,回复 ACK,进入
TIME_WAIT状态。服务端收到 ACK 后彻底关闭(CLOSED)。
3.2 为什么是四次?
因为 TCP 的全双工特性决定了发送通道和接收通道是独立关闭的。
客户端发 FIN 仅仅代表"我没有应用层数据要发给你了",但这不代表它不能接收数据。服务端收到 FIN 后,可能还有自己的数据没处理完,所以先回一个 ACK 稳住对方,等自己真正处理完了,再发自己的 FIN。
4.状态流转与 TIME_WAIT 的深度解析(⭐⭐⭐)
4.1状态转化的全部过程

4.2 为什么需要 TIME_WAIT 状态?(⭐⭐⭐)
主动断开连接的一方,在发送完最后一个 ACK 后,必须进入 TIME_WAIT 状态,停留时间通常为 2MSL(MSL = Maximum Segment Lifetime,报文最大生存时间,Linux 下通常 2MSL 为 60 秒)。
(1) 保证最后一个 ACK 送达: 如果客户端最后发的 ACK 丢了,服务端会触发超时重传,再次发送 FIN。如果客户端不等待直接关闭,收到服务端的重传 FIN 后会回复 RST,导致服务端非正常关闭。停留在
TIME_WAIT可以保证有足够的时间重发 ACK。(2) 让历史网络迷游报文消散: 等待 2MSL 可以更大几率的确保当前连接在网络中产生的所有残留报文都彻底死亡,防止它们干扰后续在相同 IP 和端口上建立的新连接。
setsockopt解决端口占用: 如果 Server 是主动断开方,重启时会发现端口处于TIME_WAIT状态,提示 "Address already in use" 导致绑定失败。在服务器代码中,必须在socket()和bind()之间使用setsockopt设置SO_REUSEADDR,允许立刻复用处于TIME_WAIT状态的端口。
六、 全连接队列与 Listen 的 Backlog
1. 半连接队列与全连接队列
在 TCP 建立连接的过程中,Linux 内核为每个 Listen 状态的 Socket 维护了两个队列:
半连接队列 (SYN Queue): 存放收到 SYN 但还未收到最后一次 ACK 的连接(处于
SYN_RCVD状态)。全连接队列 (Accept Queue): 存放已经完成三次握手(处于
ESTABLISHED状态),等待应用层调用accept()取走的连接。
2. Listen 的第二个参数
backlog 在 Linux 的实现中,listen(fd, backlog) 的 backlog 参数,主要决定了全连接队列的最大长度 (通常是 backlog + 1)。
3. SYN 洪水攻击 (SYN Flood)
攻击者伪造大量不同 IP 的 SYN 包发给 Server,但不回复第三次 ACK。这会导致 Server 的半连接队列被迅速填满,无法处理正常的连接请求。
4. backlog的长度控制(⭐⭐⭐)
(1) 为什么 backlog 不能太长?
如果全连接队列设置得极大,当高并发到来而应用层
accept()处理不过来时,大量连接会在队列中堆积。对客户端而言,连接虽然在底层建立成功了,但迟迟得不到业务响应,最终客户端会超时断开。这些堆积在 Server 队列里的连接就成了无用的"死连接",白白浪费大量内存。(2) 为什么不能没有?
队列起到了一个缓冲 (Buffer) 的作用。如果完全没有队列,只有当应用层恰好在调用
accept()阻塞等待时才能建立连接,一旦并发量稍微起伏,多余的连接请求就会立刻被操作系统丢弃,导致服务器吞吐量极低。
七、流量控制
1. 核心机制
滑动窗口 流量控制是基于接收方的"接收能力"来控制发送方的"发送速度"。
在三次握手期间(前两次握手不携带数据,但第三次握手是可以携带数据的),双方就已经通过 TCP 报头中的"窗口大小"字段协商了彼此的初始接收能力。
发送方发送数据时,绝不能超过对方通告的窗口剩余大小。
2. 窗口探测与更新
窗口更新通知: 当接收方的应用程序从缓冲区读走了数据,剩余空间变大,接收方会主动发一个"窗口更新"报文给发送方。
窗口探测: 如果这个更新报文在网络中丢了,发送方就一直以为对方窗口是 0。为了打破这种死锁,TCP 发送方会启动一个定时器,定期发送零窗口探测报文(Zero Window Probe),强制要求接收方回复当前的窗口大小。
3. 窗口扩大因子
TCP 报头中的"窗口大小"只有 16 位,最大只能表示 65535 字节。在千兆/万兆网络中,这个大小太小了,会严重限制传输速度。 因此,TCP 在选项字段中引入了"窗口扩大因子"。它通过移位操作(Shift),可以将实际的窗口大小成倍扩大,从而支持现代高速网络的需求。
八、滑动窗口的深度剖析
前面提到的确认应答机制,如果每发一个段就等一个 ACK,效率会低得令人发指。TCP 引入滑动窗口,就是为了在保证可靠性的前提下,实现批量数据的并发发送。
1. 窗口在哪里?区域是如何划分的?
滑动窗口本质上是发送方操作系统内核中发送缓冲区的一部分。我们可以将其理解为一个巨大的字符数组,内核通过维护几个指针(数组下标)将缓冲区划分为四个区域:

区域 1:已发送且已收到 ACK 确认的数据。(这部分数据可以从缓冲区清除了)
区域 2 :已发送但尚未收到 ACK 的数据。(这就是滑动窗口所在的位置,大小受对方通告窗口和网络拥塞窗口限制)
区域 3:尚未发送,但允许发送的数据。(在窗口范围内)
区域 4:尚未发送,且不允许发送的数据。(超出了窗口范围)
2. 丢包了怎么理解滑动窗口?
滑动窗口极大地优化了丢包处理机制:
(1) ACK 丢失(累积确认机制): 假设发送方发了 1, 2, 3 三个包。接收方收到了,回复了 ACK_2, ACK_3, ACK_4。如果前两个 ACK 丢了,只收到了 ACK_4。 没关系 TCP 的确认序号代表"该序号之前的数据我已经全部按序收到了"。收到 ACK_4 就意味着 1、2、3 都收到了。这种允许少量 ACK 丢失的机制,保证了窗口连续向右滑动。
(2) 数据包丢失(快重传机制 Fast Retransmit): 如果发送方发了 1, 2, 3, 4, 5。第 2 个包在网络中丢了。 接收方收到 1 回 ACK_2;收到 3 还是回 ACK_2;收到 4 依然回 ACK_2。 当发送方连续收到 3 个相同的确认应答(ACK_2)时,它立刻意识到第 2 个包丢了,会在定时器超时之前立刻重发第 2 个包。这就叫"快重传"。
3. 窗口的移动与越界(⭐⭐⭐)
向右移动: 随着不断收到新的 ACK,窗口的左边缘向右推移;随着对方通告的窗口变化,右边缘也向右推移。
动态变化: 窗口大小 =
min(接收端剩余缓冲区大小, 网络拥塞窗口)。它会随着接收方的处理速度变大、变小,甚至变为 0(引发上文提到的零窗口探测)。越界问题: 缓冲区的内存是有限的,一直向右滑动一定会越界。因此,TCP 底层将这个数组组织成了一个环形队列 (Ring Buffer)。
九、延迟应答与捎带应答
1. 延迟应答 (Delayed ACK)
如果接收方一收到数据就立刻回复 ACK,此时它的缓冲区刚被填入数据,上层应用还没来得及读走,它能通告给对方的"窗口大小"就会变小。
策略: 收到报文后不着急应答,稍微等一等(例如等 200ms)。这期间如果应用层把数据读走了,接收缓冲区就腾出了空间,回复 ACK 时就能通告一个更大的窗口,从而让发送方发得更快,提高整体吞吐量。
触发限制: 并不是所有包都延迟。通常有数量限制 (每隔 N 个包应答一次)和时间限制(最大延迟时间不能超过超时重传时间)。
2. 捎带应答
ACK 报文本身往往不携带有效载荷,只为了发一个头部,开销很大。
策略: 如果接收方刚好也有数据要发给对方,就把这个 ACK 标记位和确认序号"顺风车"一样搭载在数据报文中一起发过去。
体现: 三次握手本该是四次(SYN -> ACK -> SYN -> ACK),但服务端的 ACK 和 SYN 合并成了一个报文,这就是典型的捎带应答。
十、拥塞控制
滑动窗口解决了通信双方 的收发能力问题,但没有考虑中间网络的承受能力。如果网络本身已经堵车了,两边还拼命发数据,只会导致网络瘫痪。
1. 核心共识
少量丢包: 认为是正常情况(可能路由器瞬时拥挤),触发快重传或超时重传。
大量丢包: 发送方发出大量数据均超时,判断为网络拥塞。用tcp协议实现了网络中多主机面对拥塞时的共识。
2. 慢启动与动态调整(⭐⭐⭐)

TCP 引入了拥塞窗口 (cwnd) 的概念。
真正的发送窗口 = min(通告窗口 rwnd, 拥塞窗口 cwnd)。
慢启动: 刚开始通信时,不清楚网络状况,cwnd 初始化为 1(或较小值)。每收到一个 ACK,cwnd 加 1。这意味着每一个 RTT(往返时间),发送数量会翻倍(1 -> 2 -> 4 -> 8),呈指数级增长。
拥塞避免: 指数增长不能无限进行。当 cwnd 达到慢启动阈值 (ssthresh) 时,改为线性增长(每个 RTT 加 1),缓慢试探网络底线。
网络恢复: 一旦发生超时重传(网络拥塞),ssthresh 瞬间减半,cwnd 重新回到 1,再次进入慢启动。这表现为 TCP 极强的"自律性"和网络自我恢复能力。
十一、面向字节流与粘包问题
1. 面向字节流的本质
TCP 不关心应用层发的是什么协议、什么格式。你调用 10 次 write 发送 100 字节,底层 TCP 可能把它们拼成 1 个包发出去;你调用 1 次 write 发送 10000 字节,底层 TCP 可能把它拆成 10 个包发出去。读写次数毫无对应关系。它的任务就是将这些数据准确无误的发送到对方的接收缓冲区当中就行了,而至于如何解释这些数据完全由上层应用来决定,这就叫做面向字节流。
2. 粘包问题
正因为是"流",接收方的应用层在缓冲区里看到的是连成一片的字节,无法区分哪里是一个完整报文的结束,哪里是下一个报文的开始。
解决策略(必须在应用层处理):
定长报文: 规定每个包严格 1024 字节,不够就补空格。
特殊字符界定: 用特定字符隔开,例如 HTTP 协议 header 结尾的
\r\n\r\n。自描述字段(长度信息): 在报文最前面加上 4 个字节,专门表示后面有效载荷的长度(TLV 格式:Type-Length-Value)。
组合拳: 长度字段 + 特殊字符校验。
十二、TCP 异常问题剖析
在实际服务器运行中,各种意外随时发生,TCP 有一些应对策略。
(1) 进程终止: 无论是正常退出还是被
kill掉,操作系统内核都会介入,自动关闭该进程打开的所有文件描述符(FD)。这会触发正常的四次挥手,与正常关闭无异。(2) 机器重启 (Reboot): 操作系统在重启前会尝试优雅关闭所有进程,同样会触发四次挥手。如果时间不够,可能会发送 RST 报文。
(3) 机器掉电 / 网线物理拔出: 瞬间死亡,根本没机会发 FIN 或 RST。
此时接收方毫不知情,如果接收方不主动发数据,它会永远挂在那里。
解决: TCP 内置了保活机制 (Keep-Alive) ,长时间没数据通信时会发送探测包。但由于其周期太长(默认 2 小时),实际开发中,通常由应用层自己实现心跳包 (Heartbeat) 来检测掉电死连接。
十三、如何用 UDP 实现可靠传输?(⭐⭐⭐)
这个问题本质上是考察对 TCP 原理的理解。既然 UDP 是不可靠的,就要在应用层把 TCP 走过的路重新走一遍:
引入序列号: 给每个 UDP 包在应用层加上编号,解决乱序问题。
引入确认应答: 接收方收到包后,回复一个带有 ACK 确认号的 UDP 包。
引入超时重传: 发送方维护定时器,没收到 ACK 就重发。
引入滑动窗口: 在应用层维护发送/接收缓冲区和窗口大小,实现流量控制和拥塞控制。 (现代著名的 QUIC 协议、游戏开发中常用的 KCP 协议,底层逻辑正是如此。)
十四、零散知识点
1. 可靠性与性能(⭐⭐⭐)
可靠性的保证
(1)检验和;(2)序列号;(3)确认应答;(4)超时重传;(5)连接管理;(6)流量控制;(7)拥塞控制。
性能保证
(1)滑动窗口;(2)快速重传;(3)延迟应答;(4)捎带应答。
只有 在极端的局域网环境,或者对丢包容忍度极高、对绝对延迟极其敏感的场景(如视频会议、实时竞技游戏),纯粹的 UDP 才 会展现出绝对的速度优势。但在大多数存在丢包、抖动的广域网(公网)传输中,一个经过 Linux 内核几十年极致优化的 TCP 协议栈,其真实吞吐量和效率,往往会无情碾压很多开发者自己用 UDP 随手捏造的"粗糙版可靠传输"方案。这也是为什么像 HTTP/1.1 和 HTTP/2 这样要求极致性能的应用层协议,依然长期坚定地建立在 TCP 之上。
2. 六种最核心的定时器
2.1 超时重传定时器
这是 TCP 最核心、最重要的定时器,是保证可靠性的基石。
何时启动: 当发送端发出一个数据报文(或 SYN、FIN 等控制报文)时,就会启动该定时器。
它的作用: 等待接收端的 ACK 确认报文。
超时发生什么: 如果在规定的时间(RTO,Retransmission Timeout)内没有收到 ACK,定时器到期,TCP 就会认为报文丢失,并重新发送该报文。
核心细节:动态 RTO。 这个时间绝对不能是写死的。TCP 内核会不断采样报文的往返时间(RTT),并通过复杂的平滑算法(如 Jacobson/Karels 算法)动态计算出 RTO。如果连续发生超时,RTO 会呈指数级退避(例如 1s -> 2s -> 4s -> 8s),防止给拥塞的网络火上浇油。
2.2. 坚持定时器 (零窗口探测定时器)
这是为了打破流量控制死锁而设计的。
何时启动: 当接收端因为缓冲区满了,向发送端发送了一个 "窗口大小为 0" 的 ACK 报文时,发送端会停止发送数据,并启动坚持定时器。
它的作用: 假设接收端后来缓冲区有空间了,发了一个"窗口更新"报文(比如窗口变成 2000 字节),但这个更新报文在网络中丢失了。此时接收端在等数据,发送端在等窗口更新,双方陷入死锁。
超时发生什么: 坚持定时器到期后,发送端会主动发送一个 零窗口探测报文(Zero Window Probe,通常只含 1 字节数据)。强制要求接收端回复当前的窗口大小,从而打破死锁局面。
2.3 时间等待定时器 (2MSL 定时器)
这是为了安全释放连接而设计的。
何时启动: 主动断开连接的一方,在发送完第四次挥手的最后一个 ACK 报文后启动。
它的作用: 停留在一个安全的状态,时间固定为 2MSL(Maximum Segment Lifetime,报文最大生存时间的 2 倍,Linux 下通常是 60 秒)。
超时发生什么: 确保最后一个 ACK 如果丢失可以被重传,并且让网络中属于该连接的所有历史迷游报文彻底消散。定时器到期后,连接彻底销毁,进入
CLOSED状态,端口释放。
2.4 保活定时器
这是为了检测死连接(半打开连接)而设计的。
何时启动: 当通信双方长时间没有数据交互时(即连接处于空闲状态)。注意:默认情况下它是关闭的,需要在应用层通过
setsockopt开启SO_KEEPALIVE。它的作用: 防止因为客户端突然断电、网线拔出等物理故障,导致服务端永远维持着一个毫无意义的 TCP 连接,白白浪费结构体和文件描述符(FD)资源。
超时发生什么: 默认空闲时间通常长达 2 小时。2 小时后,保活定时器到期,服务端会发送探测报文。如果连续发送多次(默认 9 次,每次间隔 75 秒)都没有响应,服务端就认为客户端已经挂了,内核会强行关闭该连接。
实战经验: 2 小时太长了,所以实际的高并发服务器开发中,我们极少依赖这个内核层的定时器,而是在应用层自己实现心跳包(Heartbeat)。
2.5 延迟应答定时器
这是为了提升传输效率而设计的。
何时启动: 接收端收到数据报文,但此时自己没有数据要发给对方(无法捎带应答),且没有超过规定的包数限制时启动。
它的作用: 不立刻发送纯 ACK 报文,而是"等一等"。
超时发生什么: 定时器时间很短(通常在 40ms ~ 200ms 之间)。如果在定时器到期前,应用层有数据要发送,ACK 就顺风车捎带过去;如果应用层读走了数据,还可以通告更大的窗口。如果定时器到期了还是没等到,只能单独把这个纯 ACK 发出去。
2.6 FIN_WAIT_2 定时器 (孤儿连接定时器)
这是一个兜底的安全定时器。
何时启动: 主动断开方发送 FIN 并收到 ACK 后,进入
FIN_WAIT_2状态,等待对方发送 FIN 时启动。它的作用: 如果被动断开方(通常是服务端代码有 BUG)迟迟不调用
close()发送 FIN,让自己过一端时间销毁。超时发生什么: 在 Linux 中,如果这个连接已经由应用层调用了
close()(被称为孤儿连接),内核会启动这个定时器(可通过tcp_fin_timeout配置,默认 60 秒)。一旦超时还没收到对方的 FIN,内核会直接暴力销毁该连接,防止资源永久枯竭。
3. 快重传VS超时重传
超时重传是 TCP 可靠性的"最后防线(兜底机制)",而快重传只是为了提升效率的"锦上添花"。 两者是互补的关系,快重传提高了 TCP 的下限体验,而超时重传守住了 TCP 可靠性的底线。
| 特性 | 超时重传 (Timeout Retransmit) | 快重传 (Fast Retransmit) |
|---|---|---|
| 触发机制 | 内部定时器:等待 RTO (重传超时时间) 耗尽。 | 外部反馈:连续收到 3 个相同的重复 ACK。 |
| 核心定位 | 基石与兜底(保证绝对可靠性)。 | 效率优化(避免无效等待,提速)。 |
| 响应速度 | 慢(为了防网络抖动,RTO 通常略大于往返时间 RTT)。 | 极快(只要收到 3 个重复 ACK 立刻补发,不理会定时器)。 |
| 网络状态假设 | 认为网络可能出现了严重拥塞。 | 认为网络畅通,只是偶尔轻微丢包 或乱序。 |
快重传的致命弱点在于其触发条件极为苛刻:必须有后续的数据包成功到达接收端,接收端才会一直回复重复的 ACK。
假设发送方一共只发 5 个包(1, 2, 3, 4, 5)。如果前面都正常,但第 5 个包(最后一个)在网络中丢了。 因为后面已经没有第 6、第 7 个包发过去了,接收端根本没有机会被触发去回复"重复的 ACK_4"。发送方永远凑不够 3 个重复的 ACK,如果不依靠超时重传定时器,这个第 5 个包将永远石沉大海,应用层数据就不完整。