TCP 协议复习

一、TCP 在协议栈中的定位

  • TCP 是传输层协议,负责端到端的可靠传输(不是安全,加密是 TLS 的事)
  • TCP 底层跑在 IP(网络层)之上,IP 是不可靠的(尽力而为),TCP 在不可靠的 IP 之上实现可靠
  • HTTP / WebSocket 是应用层协议,跑在 TCP 之上
    • HTTP:请求→响应→断开,一次性的
    • WebSocket:长连接,双向实时通信

二、TCP 连接管理

三次握手

Client                           Server
  |  ── SYN (seq=x) ───────→    |  "我来了,你能收到吗?"
  |  ←── SYN+ACK (seq=y, ack=x+1) |  "收到了,你也能收到我吗?"
  |  ── ACK (ack=y+1) ────→    |  "收到了,咱俩都能收发"
  |  连接建立(ESTABLISHED)       |

目的: 确保双方收发能力都正常(client 发→server 收、server 发→client 收)。

三次握手避免了什么?(经典面试题)

核心答案:防止已失效的旧 SYN 报文段突然到达服务端,导致服务端建立无效连接。

具体场景:

1. Client 发出 SYN1,但网络拥堵,SYN1 在路上卡住了
2. Client 等不到回复,以为丢了,重发 SYN2
3. SYN2 先到达,Server 响应 SYN+ACK,正常建立连接
4. 双方通信完毕,连接关闭
5. 此时 SYN1 才姗姗来迟,到达 Server

如果是两次握手:
   Server 收到旧 SYN1 → 回复 SYN+ACK → 直接进入 ESTABLISHED
   → Server 傻等数据,Client 压根不知道这个连接
   → 服务端资源白白被占用

三次握手:
   Server 收到旧 SYN1 → 回复 SYN+ACK → 等待 ACK
   → Client 收到 SYN+ACK,发现"我没请求连接啊"
   → Client 回复 RST 重置,Server 不会建立无意义的连接
为什么不是两次握手?

综上,两次握手无法处理网络延迟导致的历史 SYN。服务端会为不存在需求的连接分配资源。

为什么不是四次握手?

效率问题。三次已经足够验证双方收发能力——Client 第二次就能验证 Server 的收发,Server 第三次能验证 Client 的收发。多加一次纯属浪费。

ISN(初始序列号)为什么要随机?
  1. 防止历史连接的残留数据干扰新连接 — 如果 ISN 固定从 0 开始,旧连接因网络延迟迟到的数据包被新连接误接收(新连接的 seq 范围刚好覆盖到)
  2. 防止序列号预测攻击(如 TCP 劫持) — 攻击者如果知道下一次连接的 ISN,可以伪造 TCP 报文注入恶意数据。随机 ISN 让攻击者无法预测
  3. 现代系统用半随机算法: ISN = 初始偏移 + 基于时间的增量,Linux 内核通过 MD5 哈希时钟等随机因子生成

四次挥手

Client                           Server
  |  ── FIN ───────────────→    |   "我说完了"
  |  ←── ACK ───────────────    |   "知道了"(但可能还有数据要发)
  |  ←── FIN ───────────────    |   "我也说完了"
  |  ── ACK ───────────────→    |   "知道了"
  |  等2MSL(TIME_WAIT)          |   直接 CLOSED

为什么四次不是三次: TCP 全双工,每个方向独立关闭。服务端收到 FIN 后可能还有数据在缓冲区,必须先发完数据才能发自己的 FIN,所以 ACK 和 FIN 不能合并。

TCP 状态迁移(完整链路)

Client 视角:                    Server 视角:
  CLOSED                          CLOSED
    | SYN-SENT                      | LISTEN
    |                               | SYN-RECEIVED
    |                               |
    └──────→  ESTABLISHED  ←────────┘
               |
    FIN-WAIT-1 ──FIN──→ CLOSE-WAIT (对方数据可能还在发)
    FIN-WAIT-2 ←──ACK── CLOSE-WAIT
    TIME-WAIT  ←──FIN── LAST-ACK
    (2MSL后)   ──ACK──→ CLOSED
    CLOSED
两个易出问题的状态

CLOSE_WAIT(服务端卡住):

  • 服务端收到 FIN 后进入 CLOSE_WAIT,必须由应用层主动调用 close() 才能发 FIN 进入 LAST-ACK
  • 如果程序忘了 close()(比如代码 bug 没释放 socket),连接永远卡在 CLOSE_WAIT
  • 线上服务大量 CLOSE_WAIT → 文件描述符耗尽 → 无法接受新连接

TIME_WAIT(客户端堆积):

  • 高并发短连接场景(如 HTTP/1.0 每次请求都关连接),主动关闭方堆积大量 TIME_WAIT
  • 每个 TIME_WAIT 占用一个本地端口对,65535 个端口很快耗尽
  • 解决方法:SO_REUSEADDR、长连接(HTTP Keep-Alive)、调小 2MSL(一般不推荐)

TIME_WAIT(2MSL)

  • 谁发最后一个 ACK,谁承担 TIME_WAIT(通常是主动关闭方,即客户端)
  • MSL 是什么:Maximum Segment Lifetime,一个 IP 报文在网络中存活的最长时间
  • 为什么等 2MSL:
    1. 防止最后一个 ACK 丢失 — 丢了的话服务端会重发 FIN,客户端在 TIME_WAIT 期间能重发 ACK
    2. 让旧连接的数据包"死透" — 2MSL = 一个方向上 MSL,再加上反向一个 MSL,确保所有方向上的残留包都过期,避免干扰复用同端口的新连接
  • 2MSL 是固定值(Linux 内核写死 60s),不受网络状况影响
  • 注意区分:2MSL 是 TIME_WAIT 的时长,RTO 是超时重传的时长,两个不同的东西

SYN Flood 攻击 + SYN Cookie

攻击原理
攻击者                                Server(受害者)
  |  ── SYN1(伪造IP)──────→       |  分配TCB+半连接队列 → SYN+ACK
  |  ── SYN2(伪造IP)──────→       |  分配TCB+半连接队列 → SYN+ACK
  |  ── SYN3(伪造IP)──────→       |  分配TCB+半连接队列 → SYN+ACK
  |  ...大量伪造SYN...              |  半连接队列满!
                                     |  正常用户的SYN被丢弃 → 拒绝服务

攻击者只发 SYN 不回 ACK,服务端的半连接队列被撑爆。

SYN Cookie 防御
正常流程:
  Server收到SYN → 分配TCB结构体 → 存入半连接队列 → 回复SYN+ACK

SYN Cookie流程:
  Server收到SYN → 不分配TCB!
  → 把 (源IP, 源端口, 时间戳) 哈希成一个"Cookie"
  → 把Cookie当作seq填入SYN+ACK发回去
  → 收到ACK时验证Cookie,通过才分配TCB

核心思想: 资源不在握手第二步分配,延迟到第三步验证通过后再分配。这就把你之前学的三次握手第二步(存半连接队列)的存储压力变成了纯计算压力。攻击者伪造再多 SYN 也无法耗尽服务器内存。


三、TCP 六大可靠性机制

① 确认应答(ACK)

  • TCP 面向字节流,ACK 确认的是字节编号而非"第几个包"
  • seq(序列号)= 本端发送数据的起始字节编号
  • ack(确认号)= 期望对方下次发送的起始字节编号,隐含表示之前的全部收到
  • 如果重复收到已确认的包,接收方丢弃但要重发 ACK
延迟 ACK(Delayed ACK)

接收方不会每个包都立刻回 ACK,而是等一会儿:

  • 如果后续收到连续的包,只回复最后一个(累计确认),减少 ACK 包数量
  • 如果在这期间有反向数据要发,就把 ACK 捎带(piggyback) 在数据包上
  • 通常延迟 200ms,如果在这期间又有新包到达,合并确认
延迟 ACK × Nagle 算法:互相拖慢的经典问题
  • Nagle 算法: 发送方想攒小数据,等前面的 ACK 回来再发下一个包
  • 延迟 ACK: 接收方想等会儿再发 ACK
  • 两个碰到一起 → 发送方等 ACK,接收方等更多数据 → 短暂死锁(200ms 级延迟)
  • 解决:对延迟敏感的应用开启 TCP_NODELAY 关闭 Nagle

② 超时重传(RTO)

  • RTO 是动态计算的,不是固定值
  • 计算公式:
    • SRTT = 0.875 × 旧SRTT + 0.125 × 新RTT(平滑加权)
    • RTTVAR = 0.75 × 旧RTTVAR + 0.25 × |SRTT - 新RTT|(波动量)
    • RTO = SRTT + 4 × RTTVAR
  • 重传二义性: 重传包返回的 ACK 分不清是确认原包还是重发包 → 重传期间不采样 RTT,RTO 翻倍(Karn 算法)
  • RTO vs 2MSL: RTO 用于数据发送超时重传(动态),2MSL 用于连接关闭后等待(固定)

③ 滑动窗口

  • 解决"发一个等一个"的低效问题,实现批量发送批量确认
  • 窗口大小 = 接收方告知的剩余缓冲区大小(ACK 里的 window 字段)
  • 收到 ACK → 窗口右滑,新的数据纳入窗口
  • 发送方数据分三区:已发已确认 | 已发待确认 | 可发未发
发送方视角:
  [1-3 已确认] | [4-7 已发待确认] | [8-10 可发送] | [11+ 不允许发]
                ←──── 滑动窗口(即"飞行中的数据")────→
  • window=0 死锁问题: ACK(window>0) 丢失 → 发送方永远等 → 死锁
    • 解决:持续计时器(persist timer),发送方定期发 1 字节探测包询问窗口是否恢复
  • 糊涂窗口综合征(Silly Window Syndrome): 接收方只释放一点点窗口空间,发送方立刻塞入很小的包 → 浪费大量头部开销
    • David D. Clark 方案:接收方窗口 < 1 MSS 或 < 缓冲区一半时,谎报 window=0,等攒够了再告诉对方

④ 拥塞控制

  • 滑动窗口管接收方容量,拥塞窗口管网络容量
  • 实际发送量 = min(滑动窗口, 拥塞窗口)
  • ssthresh(慢启动阈值): 分界线,决定用哪种增长策略
流量控制 vs 拥塞控制 — 本质区别

流量控制(滑动窗口) 拥塞控制
关心的对象 接收方能不能处理 网络中间设备能不能处理
范围 端到端 全局(所有连接共享网络)
反馈来源 接收方主动告知 window 自行探测(丢包 = 信号)
打个比方 桶能不能装得下 水管能不能流得过去
两个阶段
阶段 条件 增长方式
慢启动 cwnd < ssthresh 指数增长(每轮翻倍)
拥塞避免 cwnd >= ssthresh 线性增长(每轮 +1)

"慢启动"名字容易误导——实际是指数级快速增长,"慢"只是指起点小(从 1 开始)。

两种丢包检测 + 两种应对

超时重传(RTO超时) 3 个重复 ACK
严重程度 重刑(网络堵死了) 轻刑(偶尔丢几个包)
cwnd 归 1,重新慢启动 砍半,继续跑
ssthresh 设为丢包时 cwnd 的一半 设为丢包时 cwnd 的一半
后续 慢启动 → 拥塞避免 快速恢复 → 拥塞避免

为什么 3 个重复 ACK 不用归 1: 说明后续包还是到了,网络还活着,只是个别丢包。能收到后续包就意味着数据仍在传输,否则后面那些包到不了。

快重传 + 快恢复
  • 快重传: 收到 3 个重复 ACK → 不等超时,立刻重传丢失的包
  • 快恢复: cwnd 减半(不是归 1),ssthresh = cwnd,接着用拥塞避免线性增长,不从头慢启动

⑤ 序号 + 按序到达

  • TCP 给每个字节编号(seq),接收方按序号排序、去重、拼接
  • 保证上层拿到的数据是完整有序的字节流
  • 前提:ISN 随机生成,避免不同连接间的数据混淆

⑥ 校验和

  • TCP 头部 + 数据按 16 位一组求和取反,填入 checksum 字段
  • 接收方同样算法验证,对不上 → 数据损坏 → 丢弃,不回复 ACK → 发送方超时重传

四、知识点对比速查表

概念 作用 固定/动态 所在阶段
2MSL TIME_WAIT 等待时长 固定(60s) 四次挥手最后
RTO 超时重传判定时间 动态(基于 RTT) 数据传输
滑动窗口 接收方容量控制(流量控制) 动态(接收方告知) 数据传输
拥塞窗口 网络容量控制(拥塞控制) 动态(自行探测) 数据传输
ssthresh 慢启动/拥塞避免分界线 动态(丢包后重设) 拥塞控制
状态 常见问题 原因
TIME_WAIT 大量堆积,端口耗尽 高并发短连接,主动关闭方
CLOSE_WAIT 大量堆积,fd 耗尽 服务端程序忘了 close()

五、常见面试追问(自测用)

  1. 三次握手可以携带数据吗?为什么?
  2. 四次挥手可以变为三次吗?
  3. TIME_WAIT 过多怎么解决?
  4. TCP Keep-Alive 和 HTTP Keep-Alive 有什么区别?
  5. 如果网络中有大量重复的 ACK 包,TCP 怎么处理?
  6. 客户端和服务端同时发送 SYN(同时打开)会发生什么?
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐