目录
机制是指为了解决一个特定问题,而设计的一套有明确步骤、规则和触发条件的做事方法
1.TCP可靠性保证机制
可靠性的核心定义可总结为:确保数据从发送端A到接收端B的传输过程中,实现不丢(完整)、不乱(按序)、不差(无差错)、不重(无重复)
TCP通过序号+确认应答 实现字节级别的可靠性,滑动窗口 支持批量发送与累积确认以提高效率,超时重传 弥补未确认部分的不可靠性,校验和 确保数据包在传输过程中未发生损坏。但理论上最后一条已发送数据永远无法被确认,因此TCP保证的是已确认数据的可靠,而不是整个会话的100%无丢包
1.1确认应答机制

- **原理:**发送方每发一个报文,接收方必须回复一个ACK(确认序号)
- 作用 :发送方通过ACK 明确知道对方收到了哪些字节
- 单向性:数据从A→B可靠由B回复的ACK保证;B→A可靠由A回复的ACK保证,两者独立
已确认的数据:可靠;发送但未确认的数据:暂时不可靠
1.2超时重传机制

- 超时重传是TCP保障可靠性的兜底机制:当发送方在动态计算的超时时间(默认500ms整数倍) 内未收到ACK,则判定数据丢失(可能原因:数据丢失、网络阻塞、ACK丢失)
- 超时时间与网络状况强相关 ,且存在特定重传次数限制(超过则断开连接)
- 接收方可能收到因重传导致的重复报文 ,依靠序号机制去重,保证数据唯一性
- 整个过程在传输层自动完成,应用层无感知,属于TCP内部可靠性细节
TCP通过动态测量网络RTT(往返时间)计算自适应超时时间(RTO),并默认以500ms为单位进行控制;若超时未收到ACK则触发重传,每次重传后将超时时间翻倍 (1×RTO、2×RTO、4×RTO...),避免网络拥堵;当重传累计达上限(如15次)或总超时超阈值,则判定连接异常并强制关闭。这一机制在传输效率与网络稳定性之间取得动态平衡,既避免因超时过短导致频繁误重传,又防止超时过长降低丢包恢复速度
1.3流量控制机制

(1)核心目的: 保证发送方的发送数据量不超过接收方的接收能力,避免因接收方缓冲区满而导致丢包;既提升可靠性(防止正常丢包 ),也提升效率(减少不必要的重传 )
(2)建立连接时的协商(三次握手):在三次握手过程中 ,双方通过SYN报文和SYN+ACK报文中的窗口字段,告知并协商彼此的接收能力 ,为后续发送数据量设定合理基础;第三次握手可以携带数据(捎带应答),因为此时客户端认为连接已建立,发送数据量大小也可预测
(3)传输过程中的动态调整:当接收方窗口变为0时,发送方会发送窗口探测报文 ,主动获取窗口是否已恢复;接收方一旦有可用缓冲区,会发送窗口更新报文通知发送方,避免死锁
(4)窗口扩大因子: TCP选项中可以包含窗口扩大因子(M)
实际窗口大小 = 声明窗口值 << M(左移,相当于乘以2的M次方);扩大因子受操作系统和硬件限制,用于支持高带宽延迟网络中的大窗口
1.4连接管理机制

TCP通信基于连接,通过三次握手 建立连接,通过四次挥手 断开连接。连接管理由双方操作系统自动完成,上层应用(如
connect、accept)只负责触发和获取结果
| 对比项 | 三次握手 | 四次挥手 |
|---|---|---|
| 目的 | 建立连接,同步状态 | 断开连接,释放资源 |
| 主动性 | 客户端主动 | 双方均可主动 |
| 次数原因 | 最小可靠验证全双工 | 可靠保证双方无发送意愿 |
| 状态 | SYN_SENT、SYN_RCVD、ESTABLISHED |
FIN_WAIT、TIME_WAIT、CLOSE_WAIT等 |
| 关键机制 | 队列管理(半连接/全连接) | 半关闭、TIME_WAIT |
| 特性 | 三次握手 | 四次挥手 |
|---|---|---|
| 保障的重点 | 协商起始序号,确认双方通信能力正常 | 优雅地关闭,防止数据丢失,支持半关闭 |
1.4.1三次握手

(1)握手过程
| 步骤 | 发送方 | 报文 | 状态变化 |
|---|---|---|---|
| 1 | 客户端 → 服务器 | SYN | 客户端进入 SYN_SENT |
| 2 | 服务器 → 客户端 | SYN + ACK | 服务器进入 SYN_RCVD |
| 3 | 客户端 → 服务器 | ACK | 双方进入 ESTABLISHED |
(2)连接建立完成的判定
- 客户端:发送完ACK后即认为连接建立完成
- 服务器:收到ACK后认为连接建立完成
- 本质:客户端在"赌"ACK能被服务器收到;若服务器未收到ACK,后续收到客户端数据时会回复RST报文
(3)相关函数
- connect:发起建立连接请求,阻塞等待,不参与握手细节
- accept:不参与 连接建立,只是从已完成连接队列中提取结果(阻塞等待)
(4)三次握手的原因
- 最小可靠握手次数: 验证全双工通路通畅,将连接失败的成本转移到客户端
第1次 (客户端→服务端):服务端收到SYN → 证明 客户端→服务端 发送通 + 服务端接收通
第2次 (服务端→客户端):客户端收到SYN+ACK → 证明 服务端→客户端 发送通 + 客户端接收通 ,同时确认了第1次ACK的回复
第3次 (客户端→服务端):服务端收到ACK → 证明 客户端→服务端 发送通(再次确认) + 更关键的是:让服务端确信客户端活着的、能收、能回
- 资源消耗对比
| 阶段 | 客户端资源 | 服务端资源 |
|---|---|---|
| 收到SYN后 | 无状态 | 分配TCB(传输控制块),进入SYN_RCVD |
| 收到ACK后 | 进入ESTABLISHED |
进入ESTABLISHED |
关键 :服务端在收到第三个ACK之前 就已经分配了资源(TCB)。如果只做2次握手,服务端在收到第一个SYN后就要分配资源并进入ESTABLISHED,恶意客户端可以发送大量伪造IP的SYN,让服务端为不存在的连接消耗海量内存
-
成本转移
正常流程:
SYN → 服务端分配资源(SYN_RCVD)→ 等待ACK → 若ACK不到,超时释放SYN洪水攻击(2次握手):
攻击者发送海量SYN(伪造源IP)→ 服务端为每个SYN分配资源 → 永远等不到ACK → 资源耗尽SYN洪水攻击(3次握手,有SYN Cookie保护):
攻击者发送海量SYN → 服务端不分配资源,只计算Cookie → 只有收到合法ACK(携带正确的Cookie)才分配资源
2次握手:服务端在收到SYN时就承担了资源成本(容易被攻击)
3次握手:服务端把验证客户端的责任推迟到收到ACK之后,客户端必须证明自己能收到服务端的SYN+ACK(即能正确回复ACK),服务端才分配完整资源
结果 :攻击者要消耗服务端资源,必须先耗费自己的资源去接收并回复SYN+ACK(即必须有一个真实的IP和网络通路)。成本从服务端转移到了攻击者
-
两次握手:无法验证服务器→客户端的发送能力,连接失败的成本在服务端
客户端 ---SYN---> 服务端
客户端 <---ACK--- 服务端(不发送SYN)
两次握手无法验证服务端→客户端的发送能力 :两次握手只包含客户端发送SYN和服务端回复ACK,其中ACK仅是被动响应,无法证明服务端具备主动向客户端发送数据的能力(考验完整的本地发送路径:应用 → 协议栈 → 网卡 → 网络)。如果服务端→客户端的发送通路存在问题,客户端会误以为连接已建立,后续服务端发送的数据客户端完全收不到,双方却都认为连接正常,导致单向断连且无法及时察觉
连接失败成本在服务端(假设有两次握手):两次握手中,服务端收到SYN后立即分配资源并进入ESTABLISHED状态(随后才发送ACK)。如果客户端没有收到ACK,客户端会超时重传并最终放弃,认为连接未建立;而服务端已经白白占用了资源,并向上层应用误报连接成功,这就是"连接失败的成本完全压在服务端"
-
一次握手:无法验证全双工通路通畅
客户端 ---SYN---> 服务端(直接认为连接建立)
客户端无法知道服务端是否存活,服务端无法知道客户端是否收到任何东西
这根本不是握手,只是单向通知。UDP就是这样工作的,但TCP要求可靠性,所以不可接受
- 细节补充
三次握手本质可以看作四次:服务端的 SYN 和 ACK 是合并发送的(SYN+ACK 一个包)
握手期间会协商双方的接收能力
1.4.2四次挥手
建立连接是一方主动,断开连接需要双方都要同意
断开连接通过四次挥手完成
(1)挥手过程

| 步骤 | 发送方向 | 说明 |
|---|---|---|
| 初始 | --- | 正常数据传输 |
| 1 | 主动方 → 被动方 | 主动方调用close,发送FIN |
| 2 | 被动方 → 主动方 | 被动方收到FIN,回复ACK;应用层收到EOF |
| 3 | 被动方 → 主动方 | 被动方调用close,发送FIN |
| 4 | 主动方 → 被动方 | 主动方回复ACK;被动方收到后关闭 |
四次挥手本质是两个独立的"FIN-ACK"可靠传输闭环
分别关闭两个方向的数据流,确保双方都明确知道对方已无数据发送
(2)核心原则
- 断开连接的本质:双方都没有数据要发送了
- 四次挥手可靠保证:双方都没有发送消息的意愿
- 允许半关闭:一方发FIN后仍可接收数据,发送管理报文
(3)挥手次数的变化
- 四次挥手可能变成三次(当被动方的FIN和ACK合并发送时)
1.4.3队列机制与listen参数
(1)连接队列
| 队列 | 存放内容 | 管理方式 |
|---|---|---|
| 全连接队列 | 已完成三次握手的连接 | listen的第二个参数 + 1 = 队列最大长度 |
| 半连接队列 | 处于SYN_RCVD状态的连接 |
由操作系统决定长度,超时自动清除 |
accept的本质是从 TCP 全连接队列中取出第一个节点
并返回一个新的 socket 文件描述符供应用层使用
全连接队列满后,Linux 默认 直接丢弃客户端发来的第三次握手 ACK,导致连接无法从半连接队列移入全连接队列,仍停留在半连接队列的SYN_RCVD状态;服务端会超时重传 SYN+ACK,若客户端无响应,该半连接约 60-120 秒后超时释放(自动消失)
(2)listen的第二个参数backlog

- 底层全连接队列最大长度 = min(backlog, somaxconn(系统级上限数量)) + 1
- **为0:**为0可能导致服务器空闲,效率低(实际长度为 1)
- **太大:**浪费内核内存,应根据硬件和场景调整
(3)SYN洪水攻击
- 攻击者大量发送SYN,填满半连接队列
- 导致正常用户的半连接请求被丢弃,无法正常通信
1.4.4TIME_WAIT状态
- 产生条件及特点
主动断开连接的一方在四次挥手完成后进入TIME_WAIT状态
等待2倍MSL(最大报文存活时间)后自动释放
(源IP, 源端口, 目标IP, 目标端口) 在 TIME_WAIT 期间(通常为 2MSL,约 60 秒)保持唯一
| 属性 | 值 |
|---|---|
| 全称 | Maximum Segment Lifetime |
| 含义 | TCP报文段在网络中存在的最长时间 |
| 本质 | 工程假设值,并非物理上限 |
| 作用 | 作为 TIME_WAIT 状态时长的计算基准 |
- 存在原因
让历史报文消散: 防止后续新连接的报文与旧连接的残留报文冲突(起始序号随机可辅助规避)
**容错性:**如果最后一个ACK丢了,被动方会重发FIN,主动方在TIME_WAIT期间可以重发ACK
- TIME_WAIT 会导致"无法立即重启"
例如:服务端程序崩溃或正常重启。它尝试再次 bind() 到 (0.0.0.0:8080)时,内核会发现上一个连接的四元组 (客户端IP, 客户端端口, 服务端IP, 8080) 仍然处于 TIME_WAIT 状态。结果bind() 失败,报错 Address already in use。服务无法启动
这是为了防止旧连接延迟的包误入新连接,造成数据错乱
- 客户端"不易处于 TIME_WAIT"
主动关闭方:谁先发起 FIN,谁进入 TIME_WAIT
服务端:通常主动关闭(如停止服务、重启),因此成为 TIME_WAIT 的受害者
客户端:通常由服务端先关闭连接,客户端被动关闭,进入 CLOSE_WAIT 后快速释放。即使客户端主动关闭,它使用的是随机端口,下次启动时源端口变了,四元组自然不同
重启解决方案:setsockopt
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname,
const void *optval, socklen_t optlen);
| 参数 | 说明 |
|---|---|
sockfd |
socket 文件描述符 |
level |
选项所在的协议层 |
optname |
选项名称 |
optval |
指向选项值的指针 |
optlen |
选项值的长度 |
| 返回值 | 成功返回 0,失败返回 -1 |
(1)常用level级别
| level | 说明 |
|---|---|
SOL_SOCKET |
Socket 通用层(与协议无关) |
IPPROTO_TCP |
TCP 协议层 |
IPPROTO_IP |
IP 协议层 |
IPPROTO_IPV6 |
IPv6 协议层 |
(2)SOL_SOCKET 层常用选项
1. SO_REUSEADDR ------ 重用地址
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
- 作用 :允许重用处于
TIME_WAIT状态的地址和端口 - 典型场景 :服务器崩溃/重启后,立即绑定同一个端口;解决
Address already in use错误
| 场景 | 无 SO_REUSEADDR |
有 SO_REUSEADDR |
|---|---|---|
TIME_WAIT 存在时 bind() |
失败 | 成功 |
| 安全性 | 防止旧包混入新连接 | 需自行确保无旧包干扰 |
2. SO_REUSEPORT ------ 重用端口
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
作用 :允许多个 socket 绑定到完全相同的 IP 和端口。
典型场景:多进程/多线程服务器,每个进程独立监听同一端口;内核级负载均衡,新连接分发到不同进程
| 特性 | SO_REUSEADDR |
SO_REUSEPORT |
|---|---|---|
忽略 TIME_WAIT |
✅ | ✅ |
| 多进程绑定同端口 | ❌ | ✅ |
| 内核负载均衡 | ❌ | ✅ |
| 兼容性 | 所有平台 | Linux 3.9+ |
3.快速参考表
| 选项 | 级别 | 作用 | 典型值 |
|---|---|---|---|
SO_REUSEADDR |
SOL_SOCKET |
快速重用 TIME_WAIT 端口 |
1 |
SO_REUSEPORT |
SOL_SOCKET |
多进程绑定同端口 | 1 |
SO_KEEPALIVE |
SOL_SOCKET |
TCP 保活探测 | 1 |
SO_RCVBUF |
SOL_SOCKET |
接收缓冲区大小 | 262144 |
SO_SNDBUF |
SOL_SOCKET |
发送缓冲区大小 | 262144 |
SO_LINGER |
SOL_SOCKET |
控制 close() 行为 |
{1,0} |
TCP_NODELAY |
IPPROTO_TCP |
禁用 Nagle 算法 | 1 |
TCP_QUICKACK |
IPPROTO_TCP |
立即发送 ACK | 1 |
1.5TCP拥塞控制机制
(1)为什么需要拥塞控制
- 之前讨论的机制都假设**问题出在对端主机:**对方处理慢(接收窗口小);对方ACK回得慢
- 但网络是共享的公共资源 。真正的丢包往往不是对方主机问题,而是网络路径上的某个路由器缓存满了 (网络拥塞)。如果发送方在拥塞时仍然超时重传 ,只会往已经满的管道里再塞一份数据,让拥塞更严重
核心思想 :TCP不仅是两个主机的事,更是网络公共资源的使用者。检测到拥塞时,必须自我限速
TCP将网络视为公共资源,通过拥塞控制机制(如慢启动、拥塞避免、快速重传/恢复)让各主机自主感知拥塞(如检测丢包、RTT增大)并主动限速 ,从而在不依赖中央协调的情况下实现分布式共识:集体牺牲发送速率以消散网络中的历史积压数据,再逐步恢复,以此保障整个网络的稳定与公平。但这种协作是**"自适应"**的,不同主机可能因探测时机、算法实现或RTT差异而对拥塞做出不同强度的反应
(2)拥塞控制的关键:引入一个新窗口
之前的发送窗口只考虑对方接收能力:发送窗口 = 接收方通告窗口 (rwnd),现在引入拥塞窗口 (cwnd, congestion window) ,反映网络路径的承载能力
最终发送窗口大小 = min(rwnd, 拥塞窗口 cwnd)
- rwnd:对方能收多少(接收端能力)
- cwnd:网络能撑多少(网络路径能力)
真正的瓶颈取最小值:
- 对方处理慢 → 被 rwnd 限制
- 网络拥塞 → 被 cwnd 限制
(3)三个"窗口"概念对比
| 窗口名称 | 位置/归属 | 作用 | 动态性 |
|---|---|---|---|
| 16位窗口大小 | TCP报头中的一个字段 | 接收方告诉发送方"我还能收多少"(即 rwnd) | 由接收方应用层读取数据的速度决定 |
| 滑动窗口大小 | 发送方维护 | 实际能发送的数据量 = min(rwnd, cwnd) | 随 rwnd 和 cwnd 变化 |
| 拥塞窗口 cwnd | 发送方维护 | 判断网络是否健康的指标,超过会引发拥塞 | 根据网络拥塞情况动态调整 |
接收方通过 16位窗口字段 告诉发送方自己的接收能力(rwnd)(可结合选项扩展),发送方结合 拥塞窗口 cwnd (网络能力),取最小值作为 滑动窗口 的实际大小
| 角度 | 含义 |
|---|---|
| 最终发送窗口 | 是一个数值 (大小)= min(rwnd, cwnd) |
| 滑动窗口 | 是一个区间 (范围)= [snd_una, snd_una + 最终发送窗口] |
(4)拥塞控制的两个阶段

RTT全称Round-Trip Time,即往返时间
MSS 全称 Maximum Segment Size,即最大报文段长度
阶段1:慢启动
特点:初始慢,但增长速度快(指数增长)
- 初始 cwnd = 1~10 个 MSS(现代TCP通常初始为10)
- 每收到 1 个 ACK,cwnd 加 1;经过 1 个 RTT,cwnd 翻倍
设当前 cwnd = n(单位是 MSS)
- 一个 RTT 内,发送方能发 n 个包
- n 个包会触发接收方发回 n 个 ACK(假设无延迟应答影响)
- 每个 ACK 让 cwnd +1
- 一个 RTT 结束后,cwnd_new = n + n = 2n
为什么叫"慢启动"?
- 相比直接发一个大窗口,初始值确实很慢(从1开始)
- 但增长速度是快的(指数级),这个名字确实容易误解
谨慎启动,快速爬升
| 错误理解 | 正确理解 |
|---|---|
| 收到 1 个 ACK,cwnd 就 ×2 | 收到 1 个 ACK,cwnd 只 +1 |
| 翻倍是瞬间发生的 | 翻倍是一个 RTT 周期结束后的结果 |
| 所有阶段都翻倍 | 只有慢启动阶段,且达到慢启动阈值后停止翻倍 |
阶段2:拥塞避免
触发条件: cwnd 达到 慢启动阈值 (ssthresh)
-
ssthresh初始值:初始 ssthresh 通常设为一个较大值
-
ssthresh更新规则:每次检测到网络拥塞(丢包)时, ssthresh = 当前cwnd 的一半
-
作用:当 cwnd 达到 ssthresh 后,从指数增长切换为线性增长(每个 RTT 拥塞窗口增加 1 MSS)
超时丢包(严重拥塞)
ssthresh = cwnd / 2
cwnd = 1
重新进入慢启动收到3个重复ACK(轻度拥塞,快重传+快恢复)
ssthresh = cwnd / 2
cwnd = ssthresh + 3
直接进入拥塞避免(不重新慢启动) -
超时 → 网络可能完全不通 → 严重 → 从1重新慢慢爬
-
3个重复ACK → 网络还能传后续包 → 轻度 → 窗口减半直接线性增长
-
+3 是为了利用已被确认、离开网络的 3 个报文段所腾出的空间
为什么需要线性增长?
- 指数增长太快,很容易一下就撞破网络承载极限
- 线性增长更温和,逐步探测网络的上限
2.TCP效率提升机制
TCP的可靠性依赖确认应答,但一发一确认效率太低。于是,
- 滑动窗口:允许批量发送,不需要等ACK就能发多个报文
- **累积确认:**用一个ACK确认多个报文,减少ACK数量
- 捎带应答:将确认信息附着在数据报文中,避免发送纯ACK
三者结合,在保证可靠性的前提下,实现了"高效传输"
2.1滑动窗口机制
(1)什么是滑动窗口

- 滑动窗口是发送缓冲区的一部分
- 用于保存已经发出去但暂时没有收到应答的报文
- 窗口大小不能超过对方接收缓冲区剩余空间(即应答报文中的窗口大小)
(2)为什么需要滑动窗口
- 有了滑动窗口区域,发送方可以一次性发送多个报文,无需等待每个ACK
- 既保证可靠性(保存未确认报文以便重传 ),又提高效率(批量发送)
- 滑动窗口是流量控制、超时重传、快重传的基础
| 机制 | 与滑动窗口的关系 |
|---|---|
| 流量控制 | 发送方根据接收方报文中的16位窗口大小调整滑动窗口大小进而直接实现流量控制 |
| 超时重传 | 基于滑动窗口的已发送未确认区间判断丢包,超时后重传窗口内未被确认的报文 |
| 快重传 | 基于滑动窗口的重复 ACK 检测------收到 3 个重复 ACK 说明窗口内某个报文可能丢失,立即重传 |
(3)发送缓冲区的分区
发送缓冲区(环形)
|<-- 已确认 -->|<-- 已发未确认(窗口内) -->|<-- 待发送 -->|<-- 空 -->|
0 a b c size
↑ ↑
ack指针 nxt指针
- 已确认:[0, a) - 可覆盖,内存可回收
- 已发未确认:[a, b) - 滑动窗口区域,需暂存等待ACK
- 待发送:[b, c) - 应用层已写入但尚未发送
- 空闲:[c, size) - 可用缓冲区空间
发送缓冲区按发送与确认的性质划分为以下区域:
| 区域 | 说明 |
|---|---|
| 已确认 | 已收到ACK,可被覆盖 |
| 已发送但未收到应答 | 滑动窗口区域,需暂存 |
| 待发送 | 尚未发送的数据 |
| 空 | 空闲缓冲区 |
- 区域划分通过首尾字节下标作为双指针实现
- 窗口滑动的本质就是指针右移
- TCP采用环状算法(模运算),保证滑动窗口不越界
(4)窗口的滑动机制
- 窗口不会向左移动(已确认的数据不会回退),只会向右移动(窗口整体前移)
- 窗口大小可以动态变化:变大、变小、不变,甚至可以变为0
| 变化类型 | 左边界 | 右边界 | 窗口大小 | 触发条件 |
|---|---|---|---|---|
| 右移(滑动) | 右移 | 右移 | 不变 | 收到 ACK,已确认数据被移除 |
| 扩大 | 不变 | 右移 | 变大 | 接收方通告更大的 rwnd |
| 缩小 | 不变 | 左移 | 变小 | 接收方通告更小的 rwnd |
| 窗口关闭 | 不变 | 左移至边界 | 变为 0 | 接收方缓冲区满,通告 rwnd=0 |
(5)窗口边界的计算
窗口边界
start = SND.UNA(已确认序号)
window_size = min(rwnd, cwnd)
end = start + window_size
实际发送
//可用窗口 表示 当前这一瞬间还能发多少
可用窗口 = end - SND.NXT(下一个要发送的序号)
//end 会在收到 ACK 或 rwnd/cwnd 变化时重新计算
待发数据量 = 有效数据量
本次可发 = min(可用窗口, 待发数据量)
发送方的行为就是在这个动态滑动的窗口内,尽最大努力发送待发数据
(6)窗口滑动的依据
- 确认序号的定义: 确认序号为x,表示x之前的所有报文都已收到(累积确认)
- 如果前面的报文丢了,即使后面的收到了,ACK确认序号只能是前面的那个
- 这保证了确认序号是线性的,不会跳跃,进而使滑动窗口连续滑动
(7)窗口大小影响因素
窗口大小 = min(接收方扛得住, 网络撑得住, 硬件发得出)
滑动窗口的实际大小受多个因素共同限制:
- 对方接收窗口大小(流量控制)
- 拥塞窗口大小(拥塞控制)
- 硬件限制(窗口内部还会分块发送)
原则:窗口越大,网络吞吐量越高
rwnd = cwnd = 1000时,窗口大小固定为 1000;有效数据只有 100,则实际只发送 100,但窗口能力仍是 1000,不因数据量而缩小
(8)序号与滑动窗口的关系
新连接双方各自独立随机选择起始序号;随机目的是防止旧报文干扰新连接
初始序号(ISN) = 随机值
发送序号(Seq) = 初始序号(ISN) + 已发送字节数
确认序号(ACK) = 期望收到的下一个字节 = 收到的最后一个连续字节 + 1
滑动窗口用序号标记左右边界,确认序号使左边界单调递增,从而驱动窗口线性、连续地向右滑动
2.2快重传机制
(1)为什么需要快重传
在TCP协议中,超时重传是基础机制,但存在明显缺陷:
- RTO(重传超时时间)通常较长(至少200ms,常见200ms~1s以上)
- 如果丢包后被动等待超时,网络利用率低,延迟大
- 尤其对于短连接或实时性要求高的场景,影响显著
快重传就是为了提前发现丢包、立即重传,不依赖超时
(2)快重传的核心触发条件
收到 3 个重复的 ACK**(注意** :必须是 3 个 )
- 1 个重复 ACK → 可能是网络乱序(包可能晚到)
- 2 个重复 ACK → 也可能是乱序
- 3 个重复 ACK → 大概率是丢包
这个阈值(3)是一个工程权衡的结果,兼顾避免误判乱序与快速恢复
(3)快重传VS超时重传
| 特性 | 快重传 | 超时重传 |
|---|---|---|
| 触发条件 | 收到 3 个重复 ACK | RTO 超时 |
| 响应速度 | 快(几十ms内) | 慢(通常 ≥ 200ms) |
| 是否依赖 RTO 估计 | 否 | 是 |
| 是否判定为丢包 | 高置信度 | 最终兜底 |
| 是否会引发拥塞控制 | 是(进入快恢复) | 是(进入慢启动) |
| 作用 | 主要丢包检测手段 | 兜底机制 |
- 快重传收到3个重复ACK时判定丢包但网络尚可,将拥塞窗口减半并进入快恢复以温和调整;
- 超时重传判定网络可能严重阻塞,将拥塞窗口降为1并进入慢启动,代价更大、恢复更慢
2.3延迟应答机制

(1)为什么需要延迟应答
假设:
- 接收方内核缓冲区剩余空间 = 1KB
- 收到1KB数据后立即ACK → 通告窗口 = 0KB(满)
- 发送方被迫停止发送
- 上层应用还没来得及read数据
结果:网络空闲,效率低下
(2)延迟应答的核心思想
收到数据后不立即ACK,等一会儿,给上层应用时间把数据读走,这样ACK时就能通告一个更大的窗口, 本质是博概率
- 延迟一小段时间 → 上层可能正好把数据读走 → 缓冲区多出空间 → 通告窗口变大
- 发送方看到大窗口 → 一次发更多数据 → 网络吞吐量提升
(3) 延迟应答的两个限制(防止死等)
| 限制类型 | 典型值 | 目的 |
|---|---|---|
| 时间限制 | 最大200ms | 避免ACK拖太久触发发送方超时重传 |
| 数量限制 | 每2个报文应答一次 | 确保接收方不会无限延迟 |
- 200ms:这是一个经验值,小于TCP的RTO(重传超时时间,通常200ms以上),防止因ACK延迟导致发送方误判丢包而重传
- 2个报文:如果每次都等2个报文才ACK,相当于把ACK频率减半,减少了网络中的确认包数量,也给了应用两次数据的间隔时间来读取
实际策略(常见实现):
- 收到2个报文 → 必须ACK
- 或等待200ms → 必须ACK
- 哪个条件先满足就触发
(4)开发者的影响
推荐做法:
- 尽快调用 read() / recv() 把数据从内核缓冲区取走
- 不要在内核层积压数据
- 这样可以保证下次ACK时通告的窗口是真实可用的
不推荐:
- 应用层处理慢,数据在内核堆积 → 窗口变小 → 发送方降速
2.4Nagle算法
| 组成部分 | 在Nagle算法里的体现 |
|---|---|
| 要解决的问题 | 网络中被大量小数据包(如1字节)浪费带宽,效率极低。 |
| 核心思想 | "合并发送" ------ 将多个小数据积攒成一个大数据块再发送。 |
| 规则和步骤 | 1. 判断发送缓冲区是否有"未确认"的数据。 2. 有未确认数据 → 新数据放入缓冲区,暂不发送。 3. 无未确认数据 → 新数据立即发送。 |
| 触发条件 | 1. 累积到一个MSS 。 2. 或发送缓冲区已满。 3. 或收到上次数据的ACK。 |
| 执行动作 | 发送一个"合并后"的大数据包。 |
(2)Nagle算法VS延迟应答
带宽 = 网络链路的最大传输速率,单位时间能传输的最大数据量
| 对比维度 | Nagle算法 | 延迟应答 |
|---|---|---|
| 谁执行 | 发送方 | 接收方 |
| 核心目的 | 减少小数据包发送,节省带宽 | 通告更大窗口,提升吞吐量 |
| 核心操作 | 攒小包,合并发 | 延迟ACK,等应用读数据 |
| 副作用 | 可能增加延迟(因为要等ACK) | 可能让发送方误以为丢包 |
| 冲突点 | 两者都引入额外延迟,叠加效果可能导致明显卡顿 |
(3)经典问题:Nagle + 延迟应答 = 死等?
场景:
- 发送方发了一个小包,进入"未确认"状态(Nagle把它卡住)。
- 接收方收到包,但因为延迟应答,不立即回ACK。
- 发送方等ACK才能发下一个包,接收方等时间才回ACK。
- 双方都在等,造成几十毫秒的延迟。
解决方案:
- 对实时性要求高的应用(如游戏、远程桌面),用 TCP_NODELAY 选项禁用Nagle算法。
- Linux下:setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));
2.5捎带应答机制

捎带应答 是指,接收方在向发送方返回确认(ACK)时,如果自己恰好也有数据要发送给对方,就将ACK确认信息 与数据放在同一个TCP报文段中发送,而不是单独发送一个只包含ACK的空报文
(1)为什么需要捎带应答
- 在TCP协议中,一个ACK报文段通常只包含一个20字节的IP头和20字节的TCP头,共40字节,而数据部分为0
- 没有捎带应答: 为了确认一个数据包,接收方需要专门发送一个40字节的"空头"ACK包。在网络中,这些小包会挤占带宽 ,并增加路由器/交换机处理大量微小数据包的负担,降低整体效率
- 有捎带应答: 接收方把自己要发送的数据(比如一个HTTP响应)和ACK合并成一个更大的报文。发送方收到后,既拿到了数据,又完成了确认。这减少了网络中数据包的总数,提高了传输效率