TCP/IP 协议解析

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秒):

  1. 确保最后的 ACK 能到达服务器:如果丢了,服务器会重传 FIN,客户端还能处理
  2. 让旧连接的数据包在网络中消亡:防止迟到的残余数据包被新连接误收

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]       ← 拆包

解决方案

  1. 固定长度:每个消息固定 N 字节,不足补零
  2. 分隔符 :用特殊字符(如 \r\n)分隔消息
  3. 长度前缀:消息头部包含消息长度(最常用)
  4. 关闭 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 规则

  1. 如果发送缓冲区中已有 ≥ MSS 字节的数据,立即发送一个 MSS 的段
  2. 如果有数据待发且没有"在途"(未确认)的数据,立即发送
  3. 否则等待:要么缓冲区积累到 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)

两个用途

  1. RTTM(Round-Trip Time Measurement):精确测量 RTT

    复制代码
    发送方在 TSval 字段填入当前时间戳
    接收方在 TSecr 字段回显该值
    发送方收到后:RTT = 当前时间 - TSecr
  2. 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(无人应答)
服务器等啊等...资源耗尽

防御手段

  1. SYN Cookie:不在 SYN_RCVD 状态分配资源,把连接信息编码在 SYN+ACK 的序列号中,收到 ACK 时解码恢复
  2. SYN Cache:限制 SYN_RCVD 状态的连接数
  3. 防火墙/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        │  ← 默认路由
└─────────────────┴──────────────┴─────────────┘

路由过程

  1. 查找目的 IP 是否在本地子网 → 是:直接发送(ARP 获取 MAC)
  2. 否 → 发给默认网关
  3. 路由器收到 → 查路由表 → 找到下一跳 → 转发
  4. 逐跳转发,直到到达目的网络

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 协议栈的核心设计思想

  1. 分层解耦:每层只关心自己的职责,通过标准接口交互
  2. 端到端可靠性:网络层尽力而为,传输层(TCP)在端系统保证可靠性
  3. 统计复用:IP 包独立路由,不需要预先建立端到端的物理链路
  4. 鲁棒性:没有中心控制点,任意节点故障不影响全局(IP 的设计初衷)

TCP 保证可靠性的核心机制

  • 序列号 + 确认应答 → 不丢失、不重复
  • 超时重传 + 快速重传 → 丢了能恢复
  • 校验和 → 数据不被篡改
  • 滑动窗口 → 流量控制,防止淹没接收方
  • 拥塞控制 → 适应网络状况,防止撑垮网络
  • 三次握手 + 四次挥手 → 连接状态可靠管理

TCP vs UDP 的选择

  • 需要可靠性 → TCP(文件传输、Web、邮件)
  • 需要低延迟、可容忍丢包 → UDP(视频、游戏、DNS)
  • 既要可靠又要低延迟 → 应用层自己实现(如 QUIC/HTTP3 基于 UDP 实现可靠传输)
相关推荐
__Witheart__1 小时前
Android 驱动编译为模块或者built-in内核
android·linux·数据库
好名字更能让你们记住我1 小时前
通过docker在本地部署博客系统服务
linux·运维·服务器·ubuntu·docker·容器
feibaoqq1 小时前
深度解析TCP/UDP/HTTP/MQTT四大网络协议
网络·低空安防
Tim风声(网络工程师)1 小时前
双射频和三射频无线AP
运维·网络
阿拉金alakin1 小时前
数据链路层核心知识总结
网络
深念Y1 小时前
openwrt.ai:一款在线OpenWrt固件定制编译平台
网络·智能路由器·编译·openwrt·刷机·软路由·固件
hai3152475431 小时前
FiveOS V3.0 交付(微服务器操作系统版 · 物理合规修正
linux·人工智能·spring boot·后端·神经网络·机器学习
network_tester1 小时前
TSN台架系统测试:从实验室验证到智能驾驶落地的关键桥梁
网络·网络协议·5g·汽车·信息与通信·信号处理·tcpdump
文青小兵1 小时前
Linux云计算——docker 告警(六)
linux·运维·docker·云计算·prometheus