1. TCP/IP 协议栈总览
TCP/IP 不是一个协议,而是一组协议的集合,采用分层架构:
┌─────────────────────────────────────────┐
│ 应用层 (Application) │ HTTP / FTP / DNS / SMTP / SSH
├─────────────────────────────────────────┤
│ 传输层 (Transport) │ TCP / UDP / SCTP
├─────────────────────────────────────────┤
│ 网络层 (Network) │ IP / ICMP / ARP / IGMP
├─────────────────────────────────────────┤
│ 数据链路层 (Data Link) │ 以太网 / Wi-Fi / PPP
├─────────────────────────────────────────┤
│ 物理层 (Physical) │ 光纤 / 双绞线 / 无线电波
└─────────────────────────────────────────┘
核心设计哲学:端到端原则(End-to-End Principle)------网络层只负责"尽力而为"地转发数据包,可靠性由传输层(TCP)在端系统之间实现。这种设计让网络核心保持简单,复杂性推向边缘。
2. 如何知道对方的 IP 地址
2.1 DNS 域名解析
用户输入的是域名(如 www.baidu.com),不是 IP。DNS 负责将域名翻译为 IP 地址。
浏览器输入 www.baidu.com
│
▼
┌──────────┐ 缓存命中? ┌──────────────┐
│ 浏览器DNS │──────────────→│ 直接返回IP │
│ 缓存 │ 是 └──────────────┘
└─────┬────┘
│ 否
▼
┌──────────┐ 缓存命中? ┌──────────────┐
│ OS DNS │──────────────→│ 直接返回IP │
│ 缓存 │ 是 └──────────────┘
└─────┬────┘
│ 否
▼
┌──────────┐ 有记录? ┌──────────────┐
│hosts文件 │──────────────→│ 直接返回IP │
└─────┬────┘ 是 └──────────────┘
│ 否
▼
┌──────────┐
│本地DNS │ ← 递归查询:本地DNS服务器代替客户端去查
│ 服务器 │
└─────┬────┘
│
▼ 迭代查询过程
┌──────────────────────────────────────────────┐
│ 1. 问根DNS(.) → 返回 .com 顶级域DNS地址 │
│ 2. 问 .com DNS → 返回 baidu.com 权威DNS地址 │
│ 3. 问 baidu.com DNS → 返回 www.baidu.com 的IP │
└──────────────────────────────────────────────┘
关键细节:
- 递归查询:客户端问本地DNS,本地DNS"全权代理"去查,客户端只等最终结果
- 迭代查询:本地DNS依次问根→顶级域→权威DNS,每一步都可能返回"去问别人"
- DNS 缓存:各级缓存有 TTL(Time To Live),过期才重新查询
- DNS 传输:默认用 UDP(端口53),响应超过512字节时切换到 TCP
- DNS 负载均衡:同一域名可返回多个 IP,轮询或按地理位置分配
2.2 ARP 地址解析(IP → MAC)
知道 IP 后,在局域网内还需要知道对方的 MAC 地址才能真正通信:
主机A (192.168.1.10) 要发数据给 主机B (192.168.1.20)
│
▼
A 的 ARP 缓存中有 B 的 MAC?
│
├─ 是 → 直接用
│
└─ 否 → 广播 ARP 请求:
"谁的 IP 是 192.168.1.20?请告诉 192.168.1.10"
│
▼
B 收到后单播回复:
"我是 192.168.1.20,我的 MAC 是 AA:BB:CC:DD:EE:FF"
│
▼
A 将映射存入 ARP 缓存,开始通信
跨网段通信 :如果目标 IP 不在同一子网,ARP 解析的是网关(路由器)的 MAC,数据包先发给网关,由网关逐跳转发。
2.3 DHCP 自动获取 IP
设备接入网络时,通过 DHCP 自动获取 IP 配置:
客户端 DHCP 服务器
│ │
│──── DHCP Discover(广播)──────────→│
│ │
│←──── DHCP Offer(提供IP)───────────│
│ │
│──── DHCP Request(接受提议)───────→│
│ │
│←──── DHCP ACK(确认分配)───────────│
│
└→ 获得:IP地址、子网掩码、网关、DNS服务器
3. TCP 如何保证传输的可靠性
TCP 是面向连接的可靠传输协议,通过多种机制协同工作来保证数据不丢失、不重复、按序到达。
3.1 序列号与确认应答(ACK)
这是 TCP 可靠性的基石。
发送方 接收方
│ │
│── Seq=1000, Len=500, Data=前500字节 ─────→│
│ │ 收到,期望下一个从1500开始
│←── Ack=1500 ──────────────────────────────│
│ │
│── Seq=1500, Len=500, Data=后500字节 ─────→│
│ │ 收到,期望下一个从2000开始
│←── Ack=2000 ──────────────────────────────│
核心机制:
- 序列号(Seq):每个字节都有编号,不是每个包一个编号。Seq=1000, Len=500 表示第1000~1499字节
- 确认号(Ack):表示"我期望收到的下一个字节的序号"。Ack=1500 表示"1500之前的数据我都收到了"
- 累积确认:Ack=2000 隐含确认了1000~1999所有数据,即使中间某个ACK丢了也没关系
- 初始序列号(ISN):不是从0开始,而是随机生成,防止历史连接的残余数据干扰
3.2 校验和(Checksum)
TCP 首部 + 数据 + 伪首部
│
▼
计算 16-bit 校验和 → 填入 TCP 首部的 checksum 字段
│
▼
接收端用同样算法计算 → 对比不一致则丢弃
伪首部包含源IP、目的IP、协议号、TCP长度------确保IP层信息没被篡改,防止数据包被错误投递到别的连接。
3.3 超时重传(RTO)
发送方每发出一个数据段就启动一个定时器。如果在 RTO(Retransmission Timeout)内没有收到 ACK,就重传该数据段。
发送方 接收方
│ │
│── Seq=1000, 数据 ───────────────────────→│
│ │
│ 等待 ACK... │ (数据丢失,接收方没收到)
│ │
│ RTO 超时! │
│ │
│── Seq=1000, 重传数据 ───────────────────→│
│ │ 收到
│←── Ack=1500 ──────────────────────────────│
RTO 的计算(RFC 6298):
RTT(Round-Trip Time)= 数据包往返时间
SRTT = (1-α) × SRTT + α × RTT_sample (平滑RTT,α=1/8)
RTTVAR = (1-β) × RTTVAR + β × |SRTT - RTT_sample| (RTT偏差,β=1/4)
RTO = SRTT + 4 × RTTVAR
RTO 指数退避:每次重传失败,RTO 翻倍(1s → 2s → 4s → 8s...),避免网络拥塞时疯狂重传加剧问题。
3.4 连接管理:三次握手与四次挥手
三次握手(建立连接)
客户端 服务器
│ │
│──── SYN, Seq=x ───────────────────────→│ ① 客户端:我要连接
│ │
│←── SYN+ACK, Seq=y, Ack=x+1 ───────────│ ② 服务器:收到,我也确认
│ │
│──── ACK, Ack=y+1 ─────────────────────→│ ③ 客户端:收到,连接建立
│ │
└──── 双方进入 ESTABLISHED 状态 ──────────┘
为什么是三次而不是两次?
核心问题是防止历史重复连接的初始化:
- 如果只有两次握手,网络中滞留的旧 SYN 包到达服务器后,服务器会以为是新连接并分配资源,但客户端根本不知道,造成资源浪费
- 三次握手让客户端在第三步确认"服务器的初始序列号我收到了",双方都确认了对方的收发能力
每次握手确认了什么:
| 步骤 | 确认内容 |
|---|---|
| 第一次 | 客户端发送能力正常,客户端初始序列号=x |
| 第二次 | 服务器接收+发送能力正常,服务器初始序列号=y,确认客户端序列号 |
| 第三次 | 客户端接收能力正常,确认服务器序列号 |
四次挥手(关闭连接)
客户端 服务器
│ │
│──── FIN, Seq=u ───────────────────────→│ ① 客户端:我没有数据要发了
│ │
│←── ACK, Ack=u+1 ───────────────────────│ ② 服务器:知道了(但我可能还有数据要发)
│ │
│ ... 服务器可能继续发送剩余数据 ... │
│ │
│←── FIN, Seq=w ─────────────────────────│ ③ 服务器:我也没有数据要发了
│ │
│──── ACK, Ack=w+1 ─────────────────────→│ ④ 客户端:知道了
│ │
│ TIME_WAIT (2MSL) │
│ 等待 2×最大段生存时间后关闭 │
为什么是四次而不是三次?
- TCP 是全双工的,每个方向需要独立关闭
- 收到 FIN 只表示对方没有数据要发了,但本方可能还有数据要发
- 第二步和第三步不能合并(除非本方恰好也没有数据了)
TIME_WAIT 的作用(等待 2MSL,通常60秒):
- 确保最后的 ACK 能到达服务器:如果丢了,服务器会重传 FIN,客户端还能处理
- 让旧连接的数据包在网络中消亡:防止迟到的残余数据包被新连接误收
3.5 流量控制(滑动窗口)
防止发送方发得太快,把接收方的缓冲区撑爆。
接收方通告窗口 = 4000 字节
发送方窗口(大小=4000):
┌────────────┬────────────┬────────────┬────────────┐
│ 已发送已确认 │ 已发送未确认 │ 可以发送 │ 不能发送 │
│ 1-1000 │ 1001-2000 │ 2001-4000 │ 4001+ │
└────────────┴────────────┴────────────┴────────────┘
↑ ↑
last_ack last_ack + wnd
窗口工作流程:
发送方 接收方
│ │
│── 发送 [1-1000] [1001-2000] [2001-3000] ─→│ 窗口内全发
│ │
│←── ACK=2001, Win=3000 ────────────────────│ 确认前两个,窗口缩小
│ │
│── 发送 [3001-4000] ──────────────────────→│ 窗口右移
│ │
│←── ACK=4001, Win=2000 ────────────────────│ 应用消费慢,窗口再缩
│ │
│ 暂停发送,等待窗口更新 │
零窗口与窗口探测:
- 接收方窗口变为 0 时,发送方停止发送
- 但发送方不会永远等下去,会定期发送窗口探测包(1字节数据),询问"你现在有空间了吗?"
- 防止死锁:如果窗口更新的 ACK 丢了,双方会永远等下去
3.6 拥塞控制
流量控制解决的是"发送方 vs 接收方"的速度匹配,拥塞控制解决的是"发送方 vs 网络"的关系------防止把网络撑垮。
TCP 拥塞控制有四个核心算法:
慢启动(Slow Start)
cwnd (拥塞窗口)
│
│ ssthresh
│ ↓
8MSS ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
│ ╱
4MSS ─ ─ ─ ─ ─ ─ ─ ╱─ ─ ─ ─ ─ ─ ─
│ ╱
2MSS ─ ─ ─ ─ ╱─ ─ ─ ─ ─ ─ ─ ─ ─ ─
│ ╱
1MSS ──╱─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
└────────────────────────────────→ 时间(RTT)
每收到一个ACK,cwnd += 1 MSS
效果:每 RTT 翻倍(指数增长)
起点 :cwnd = 1 MSS(或更小,RFC 6928 允许10 MSS)
增长方式 :每收到一个 ACK,cwnd += 1 MSS → 每 RTT 翻倍
何时结束:cwnd 达到 ssthresh(慢启动阈值)
拥塞避免(Congestion Avoidance)
cwnd
│
│ ╱╱╱╱╱╱ ← 拥塞避免:线性增长
│ ╱
│ ╱
│ ╱ ← 慢启动:指数增长
│ ╱
│ ╱
│╱
└────────────────────→ 时间
- cwnd ≥ ssthresh 时进入拥塞避免
- 每 RTT 增加 1 MSS(线性增长,不再是指数)
- AIMD(Additive Increase, Multiplicative Decrease):加法增大,乘法减小
快速重传(Fast Retransmit)
发送方 接收方
│ │
│── [Seq=1] ──────────────────────────────→│ 收到,ACK=2
│←── ACK=2 ─────────────────────────────────│
│ │
│── [Seq=2] ────× 丢失 ───────────────────→│
│ │
│── [Seq=3] ──────────────────────────────→│ 收到,但期望Seq=2
│←── ACK=2 (重复ACK #1) ──────────────────│
│ │
│── [Seq=4] ──────────────────────────────→│ 收到,但期望Seq=2
│←── ACK=2 (重复ACK #2) ──────────────────│
│ │
│── [Seq=5] ──────────────────────────────→│ 收到,但期望Seq=2
│←── ACK=2 (重复ACK #3) ──────────────────│ ← 第3个重复ACK!
│ │
│ 【快速重传 Seq=2】不等超时! │
│── [Seq=2] ──────────────────────────────→│
│←── ACK=6 ─────────────────────────────────│ 一次性确认2-5
核心思想:收到 3 个重复 ACK 就认为该包丢了,立即重传,不用等 RTO 超时。RTO 超时太慢(通常几百毫秒到几秒),快速重传可以在几十毫秒内恢复。
快速恢复(Fast Recovery)
快速重传后不是回到慢启动,而是进入快速恢复:
ssthresh = cwnd / 2
cwnd = ssthresh + 3 MSS (3个重复ACK说明有3个包到达了网络)
然后进入拥塞避免阶段,而不是从 1 MSS 重新开始。
BBR 拥塞控制(Google 提出,Linux 4.9+)
传统拥塞控制基于"丢包"判断网络状态,BBR 基于"带宽和延迟"的测量:
传统算法: BBR:
丢包 → 减小窗口 测量最大带宽(BtlBw)和最小RTT(RTprop)
没丢 → 增大窗口 发送速率 = BtlBw × (RTprop / RTT)
不依赖丢包信号
BBR 特别适合高带宽高延迟的网络(如跨国传输),在有少量丢包的环境下仍能保持高吞吐。
4. TCP vs UDP:两种传输方式
4.1 对比总览
┌─────────────────┬──────────────────────┬──────────────────────┐
│ 特性 │ TCP │ UDP │
├─────────────────┼──────────────────────┼──────────────────────┤
│ 连接方式 │ 面向连接(三次握手) │ 无连接 │
│ 可靠性 │ 可靠(确认+重传) │ 不可靠(尽力交付) │
│ 数据顺序 │ 保证按序到达 │ 不保证 │
│ 流量控制 │ 有(滑动窗口) │ 无 │
│ 拥塞控制 │ 有(慢启动等) │ 无 │
│ 传输方式 │ 字节流(无消息边界) │ 数据报(有消息边界) │
│ 首部开销 │ 20字节(最小) │ 8字节 │
│ 传输效率 │ 较低(控制开销大) │ 高(开销小) │
│ 适用场景 │ 文件传输/网页/邮件 │ 视频/游戏/DNS/直播 │
│ 连接状态 │ 有(TCB/PCB) │ 无 │
└─────────────────┴──────────────────────┴──────────────────────┘
4.2 TCP 首部结构
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤
│ Source Port (16) │ Destination Port (16) │
├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤
│ Sequence Number (32) │
├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤
│ Acknowledgment Number (32) │
├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤
│ Data │ │U│A│P│R│S│F│ │
│ Offset│ Resrv │R│C│S│S│Y│I│ Window Size (16) │
│ (4) │ (3) │G│K│H│T│N│N│ │
├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤
│ Checksum (16) │ Urgent Pointer (16) │
├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤
│ Options (variable) + Padding │
├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤
│ Data (variable) │
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
各字段含义:
| 字段 | 大小 | 说明 |
|---|---|---|
| Source Port | 16 bit | 源端口号(0-65535) |
| Destination Port | 16 bit | 目的端口号 |
| Sequence Number | 32 bit | 本报文段数据的第一个字节的序号 |
| Acknowledgment Number | 32 bit | 期望收到的下一个字节的序号 |
| Data Offset | 4 bit | TCP首部长度(单位:4字节),最小5(20字节) |
| Flags | 6 bit | URG/ACK/PSH/RST/SYN/FIN |
| Window Size | 16 bit | 本方接收窗口大小 |
| Checksum | 16 bit | 校验和 |
| Urgent Pointer | 16 bit | 紧急指针(URG=1时有效) |
4.3 UDP 首部结构
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤
│ Source Port (16) │ Destination Port (16) │
├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤
│ Length (16) │ Checksum (16) │
├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤
│ Data (variable) │
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
仅 8 字节首部,没有序列号、确认号、窗口等字段,开销极小。
4.4 TCP 的"粘包"问题
TCP 是字节流协议,没有消息边界。多次 send() 的数据可能被合并成一个 TCP 段发送:
应用层 send("Hello") + send("World")
│
▼
TCP 层可能合并为一个段:[HelloWorld] ← 粘包
或者拆成多个段:[He] [lloWor] [ld] ← 拆包
解决方案:
- 固定长度:每个消息固定 N 字节,不足补零
- 分隔符 :用特殊字符(如
\r\n)分隔消息 - 长度前缀:消息头部包含消息长度(最常用)
- 关闭 Nagle 算法 :
TCP_NODELAY选项减少合并概率
UDP 没有这个问题------每次 sendto() 对应一次 recvfrom(),消息边界天然保留。
5. TCP 高级特性与能力
5.1 Nagle 算法
目的:减少网络中小包的数量,提高带宽利用率。
未启用 Nagle:
send("H") → 立即发送 [H] ← 1字节的小包,效率低
send("ello") → 立即发送 [ello]
启用 Nagle:
send("H") → 缓存,不发
send("ello") → 合并发送 [Hello] ← 等到凑够 MSS 或收到前一个 ACK
Nagle 规则:
- 如果发送缓冲区中已有 ≥ MSS 字节的数据,立即发送一个 MSS 的段
- 如果有数据待发且没有"在途"(未确认)的数据,立即发送
- 否则等待:要么缓冲区积累到 MSS,要么收到前一个段的 ACK
何时关闭 Nagle :对延迟敏感的应用(游戏、SSH、远程桌面)设置 TCP_NODELAY。
5.2 延迟确认(Delayed ACK)
目的:减少纯 ACK 包的数量。
正常情况:
接收方收到数据 → 立即回 ACK
延迟确认:
接收方收到数据 → 等最多 200ms/40ms → 回 ACK
→ 如果这期间有数据要发,把 ACK 捎带在数据包里(piggyback)
问题:Nagle + Delayed ACK 可能导致额外延迟。发送方等 ACK 才发下一个小包,接收方延迟发 ACK,互相等待。
5.3 SACK(选择性确认)
传统 TCP 只能确认"最后一个连续收到的字节",SACK 允许接收方告诉发送方"哪些段收到了,哪些没收到":
发送方发了 [1-1000] [1001-2000] [2001-3000] [3001-4000]
其中 [1001-2000] 丢失
传统 ACK:
Ack=1001(只确认了1-1000)
发送方不知道 2001-4000 是否收到,可能全部重传
SACK:
Ack=1001, SACK=[2001-4000]
发送方知道只有 [1001-2000] 丢了,只重传这一个段
SACK 信息放在 TCP 首部的 Options 字段中。
5.4 窗口缩放(Window Scale)
TCP 首部的 Window Size 只有 16 bit,最大 65535 字节。对于高带宽延迟积(BDP)的网络远远不够。
BDP = 带宽 × RTT
例如:1 Gbps × 100ms = 100 Mbit = 12.5 MB
65535 字节的窗口根本填不满 1Gbps 的管道
窗口缩放选项(握手时协商):
- 在 SYN 包中携带 Window Scale 因子(0-14)
- 实际窗口 = Window Size × 2^Scale
- Scale=7 时,最大窗口 = 65535 × 2^7 = 8,388,480 字节 ≈ 8 MB
5.5 时间戳选项(Timestamps)
两个用途:
-
RTTM(Round-Trip Time Measurement):精确测量 RTT
发送方在 TSval 字段填入当前时间戳 接收方在 TSecr 字段回显该值 发送方收到后:RTT = 当前时间 - TSecr -
PAWS(Protection Against Wrapped Sequences):防止序列号回绕
- 高速网络上 32 bit 序列号可能回绕(在同一个连接中重复)
- 用时间戳区分"新的"和"旧的"序列号
5.6 TCP Keep-Alive
目的:检测死连接(对方崩溃或网络断开)。
正常连接:应用有数据发 → 自然检测到连接状态
空闲连接:长时间没有数据 → 可能对方已经死了
Keep-Alive:
空闲 2 小时后,发送探测包
每 75 秒发一次,连续 9 次无响应 → 认为连接已死,关闭
注意:Keep-Alive 不是 TCP 标准的一部分,是操作系统的可选实现。应用层通常自己实现心跳机制。
5.7 TCP Fast Open(TFO)
目的:减少 TCP 连接建立的 RTT 延迟。
普通 TCP:
客户端 ──SYN──→ 服务器 } 1 RTT
客户端 ←─SYN+ACK─ 服务器 } (只是建立连接,还不能传数据)
客户端 ──ACK──→ 服务器
客户端 ──HTTP GET──→ 服务器 ← 数据要等到下一个 RTT
TFO(第二次连接起):
客户端 ──SYN + TFO Cookie + HTTP GET──→ 服务器 } 1 RTT
客户端 ←─SYN+ACK + HTTP Response── 服务器 } 连接建立+数据传输同时完成
首次连接时服务器生成一个加密 Cookie 给客户端,后续连接客户端携带 Cookie,服务器验证后直接在 SYN 阶段处理请求。
5.8 SYN Flood 防护
攻击方式:攻击者伪造大量 SYN 包,服务器为每个 SYN 分配资源等待 ACK,耗尽资源。
攻击者 ──SYN(src_ip=伪造)──→ 服务器
服务器分配资源,回 SYN+ACK → 发往伪造的IP(无人应答)
服务器等啊等...资源耗尽
防御手段:
- SYN Cookie:不在 SYN_RCVD 状态分配资源,把连接信息编码在 SYN+ACK 的序列号中,收到 ACK 时解码恢复
- SYN Cache:限制 SYN_RCVD 状态的连接数
- 防火墙/IDS:检测异常 SYN 速率,过滤伪造源 IP
5.9 TCP 拥塞控制算法演进
Tahoe (1988) Reno (1990) NewReno (1996) CUBIC (2006) BBR (2016)
│ │ │ │ │
│ │ │ │ │
慢启动+拥塞避免 +快速重传 改进快速恢复 三次函数增长 基于带宽延迟
超时→回到慢启动 +快速恢复 处理多个丢包 Linux默认 模型驱动
超时→回到慢启动 高BDP友好
| 算法 | 特点 | 适用场景 |
|---|---|---|
| Tahoe | 基础版:慢启动+拥塞避免+快速重传 | 早期网络 |
| Reno | 加入快速恢复,3个重复ACK不回慢启动 | 通用 |
| NewReno | Reno 的改进,处理同一窗口多个丢包 | 通用 |
| CUBIC | 三次函数增长,恢复更快,Linux默认 | 高BDP网络 |
| BBR | 不依赖丢包,基于带宽延迟模型 | 高BDP/有丢包网络 |
| DCTCP | 数据中心TCP,基于ECN标记 | 数据中心内部 |
5.10 TCP 与 NAT
NAT(Network Address Translation)让内网多个设备共享一个公网 IP:
内网设备 A (192.168.1.10:5000) ──→ NAT ──→ 公网服务器 (8.8.8.8:443)
NAT 转换表:
┌──────────────────────┬──────────────────────┐
│ 内部地址:端口 │ 外部地址:端口 │
│ 192.168.1.10:5000 │ 203.0.113.1:40001 │
└──────────────────────┴──────────────────────┘
NAT 对 TCP 的影响:
- 端到端连接被 NAT 打断,P2P 通信需要 NAT 穿透技术
- NAT 通过跟踪 TCP 状态(SYN/ACK/FIN)来管理映射的生命周期
- 破损的 NAT 可能不正确处理 RST 或 TIME_WAIT
5.11 TCP 性能优化
Nagle + Delayed ACK 问题
问题场景:
发送方 Nagle 等 ACK → 接收方 Delayed ACK 等数据 → 互相等待
解决方案:
1. 发送方设置 TCP_NODELAY(关闭 Nagle)
2. 应用层批量发送(减少小包)
带宽延迟积(BDP)
BDP = 带宽 × RTT
100 Mbps × 50ms = 5 Mbit = 625 KB
→ 需要至少 625 KB 的窗口才能充分利用带宽
→ 窗口缩放 + 足够的缓冲区
TCP 调优参数(Linux)
bash
# 窗口大小
net.ipv4.tcp_rmem = 4096 131072 6291456 # 接收缓冲 min default max
net.ipv4.tcp_wmem = 4096 16384 4194304 # 发送缓冲 min default max
net.core.rmem_max = 6291456 # 系统最大接收缓冲
net.core.wmem_max = 4194304 # 系统最大发送缓冲
# 拥塞控制
net.ipv4.tcp_congestion_control = bbr # 使用 BBR
net.core.default_qdisc = fq # BBR 推荐的队列调度
# 连接相关
net.ipv4.tcp_fastopen = 3 # 启用 TFO(客户端+服务器)
net.ipv4.tcp_tw_reuse = 1 # 重用 TIME_WAIT 连接
net.ipv4.tcp_fin_timeout = 30 # FIN_WAIT_2 超时时间
net.ipv4.tcp_keepalive_time = 600 # Keep-Alive 探测间隔
6. IP 层:数据包如何到达目的地
6.1 IP 地址与子网
IPv4 地址:32 bit,点分十进制
192.168.1.100 = 11000000.10101000.00000001.01100100
子网掩码:区分网络部分和主机部分
255.255.255.0 (/24) → 前24位是网络号,后8位是主机号
同子网判断:
192.168.1.100 & 255.255.255.0 = 192.168.1.0 (网络地址)
192.168.1.200 & 255.255.255.0 = 192.168.1.0 (同子网)
6.2 路由:数据包如何跨网络
源主机 ──→ 默认网关 ──→ 路由器A ──→ 路由器B ──→ ... ──→ 目的主机
每个路由器的路由表:
┌─────────────────┬──────────────┬─────────────┐
│ 目的网络 │ 下一跳 │ 出接口 │
├─────────────────┼──────────────┼─────────────┤
│ 192.168.1.0/24 │ 直接相连 │ eth0 │
│ 10.0.0.0/8 │ 192.168.1.1 │ eth0 │
│ 0.0.0.0/0 │ 192.168.1.1 │ eth0 │ ← 默认路由
└─────────────────┴──────────────┴─────────────┘
路由过程:
- 查找目的 IP 是否在本地子网 → 是:直接发送(ARP 获取 MAC)
- 否 → 发给默认网关
- 路由器收到 → 查路由表 → 找到下一跳 → 转发
- 逐跳转发,直到到达目的网络
TTL(Time To Live):每经过一个路由器减 1,减到 0 则丢弃并返回 ICMP "Time Exceeded"。防止数据包在网络中无限循环。
6.3 ICMP 协议
IP 的"伴生协议",用于传递控制消息和错误报告:
| 类型 | 代码 | 用途 |
|---|---|---|
| Echo Request/Reply | 0/8 | ping |
| Destination Unreachable | 0-15 | 目标不可达(网络/主机/端口/需要分片等) |
| Time Exceeded | 0/1 | TTL 超时 / 分片重组超时 |
| Redirect | 0-3 | 重定向到更好的路由 |
7. 数据包的完整旅程
以 curl http://example.com 为例:
1. DNS 解析
浏览器 → DNS服务器:example.com 的 IP 是什么?
DNS服务器 → 浏览器:93.184.216.34
2. TCP 三次握手
客户端 → SYN → 服务器
客户端 ← SYN+ACK ← 服务器
客户端 → ACK → 服务器
3. HTTP 请求
客户端 → [PSH,ACK] GET / HTTP/1.1\r\nHost: example.com\r\n... → 服务器
4. 服务器响应
客户端 ← [PSH,ACK] HTTP/1.1 200 OK\r\n... ← 服务器
5. TCP 四次挥手
客户端 → FIN → 服务器
客户端 ← ACK ← 服务器
客户端 ← FIN ← 服务器
客户端 → ACK → 服务器
客户端进入 TIME_WAIT,等待 2MSL
每一步涉及的协议层:
应用层:HTTP 请求/响应
↓
传输层:TCP 段(加端口号、序列号、确认号等)
↓
网络层:IP 包(加源/目的 IP、TTL 等)
↓
数据链路层:以太网帧(加源/目的 MAC、CRC 等)
↓
物理层:电信号/光信号
每一层都加上自己的头部(封装),到达对端后逐层剥离(解封装)。
8. 抓包分析要点(Wireshark)
8.1 过滤器
# TCP 相关
tcp # 所有 TCP 包
tcp.port == 80 # 端口 80
tcp.flags.syn == 1 # SYN 包
tcp.flags.fin == 1 # FIN 包
tcp.flags.reset == 1 # RST 包
tcp.analysis.retransmission # 重传包
tcp.analysis.duplicate_ack # 重复 ACK
tcp.analysis.zero_window # 零窗口
# DNS 相关
dns # 所有 DNS 包
dns.qry.name == "www.baidu.com" # 查询特定域名
# HTTP 相关
http.request.method == "GET" # HTTP GET 请求
http.response.code == 200 # HTTP 200 响应
8.2 常见问题排查
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 大量重传 | 网络丢包、缓冲区太小 | 看 tcp.analysis.retransmission |
| 连接建立慢 | DNS 慢、SYN 重传 | 看 DNS 响应时间和 SYN→SYN+ACK 间隔 |
| 传输速度慢 | 窗口太小、拥塞 | 看 Window Size 和 cwnd 变化 |
| 连接被重置 | 服务器拒绝、防火墙 | 看 RST 包的来源 |
| 粘包问题 | Nagle + Delayed ACK | 看是否有小包合并现象 |
9. 总结
TCP/IP 协议栈的核心设计思想:
- 分层解耦:每层只关心自己的职责,通过标准接口交互
- 端到端可靠性:网络层尽力而为,传输层(TCP)在端系统保证可靠性
- 统计复用:IP 包独立路由,不需要预先建立端到端的物理链路
- 鲁棒性:没有中心控制点,任意节点故障不影响全局(IP 的设计初衷)
TCP 保证可靠性的核心机制:
- 序列号 + 确认应答 → 不丢失、不重复
- 超时重传 + 快速重传 → 丢了能恢复
- 校验和 → 数据不被篡改
- 滑动窗口 → 流量控制,防止淹没接收方
- 拥塞控制 → 适应网络状况,防止撑垮网络
- 三次握手 + 四次挥手 → 连接状态可靠管理
TCP vs UDP 的选择:
- 需要可靠性 → TCP(文件传输、Web、邮件)
- 需要低延迟、可容忍丢包 → UDP(视频、游戏、DNS)
- 既要可靠又要低延迟 → 应用层自己实现(如 QUIC/HTTP3 基于 UDP 实现可靠传输)