第15天:TCP协议基础
今日目标
- 理解TCP协议的核心特性
- 掌握TCP报文格式
- 深入理解TCP三次握手过程
- 深入理解TCP四次挥手过程
- 掌握TCP状态机转换
- 实现TCP连接分析工具
1. TCP协议概述
1.1 什么是TCP?
TCP(Transmission Control Protocol,传输控制协议) 是互联网协议套件中最重要的协议之一。
生活类比:
TCP就像打电话:
1. 拨号等待对方接听(建立连接)
2. 双方确认能听清楚(三次握手)
3. 开始对话(数据传输)
4. 说"再见"挂断电话(四次挥手)
特点:
✅ 可靠:说的每句话对方都能听到
✅ 有序:对话内容按顺序传递
✅ 面向连接:必须先建立连接才能通话
1.2 TCP的核心特性
TCP = 可靠的、面向连接的、字节流协议
1. 面向连接(Connection-Oriented)
├── 通信前必须建立连接
├── 通信后必须释放连接
└── 类似打电话
2. 可靠传输(Reliable)
├── 数据不丢失
├── 数据不重复
├── 数据按序到达
└── 通过确认和重传机制实现
3. 全双工通信(Full-Duplex)
├── 双方可同时发送和接收
└── 类似电话可以双向对话
4. 字节流服务(Byte Stream)
├── 面向字节流,不是消息流
├── 应用层交给TCP的数据可能被拆分或合并
└── 没有消息边界
5. 流量控制(Flow Control)
├── 防止发送方发送过快
└── 通过滑动窗口实现
6. 拥塞控制(Congestion Control)
├── 防止网络过载
└── 慢启动、拥塞避免等算法
1.3 TCP vs UDP
| 特性 | TCP | UDP |
|---|---|---|
| 连接 | 面向连接 | 无连接 |
| 可靠性 | 可靠(确认+重传) | 不可靠(尽力而为) |
| 顺序 | 保证顺序 | 不保证顺序 |
| 速度 | 较慢(开销大) | 快速(开销小) |
| 头部开销 | 20-60字节 | 8字节 |
| 流量控制 | 有 | 无 |
| 拥塞控制 | 有 | 无 |
| 数据边界 | 字节流(无边界) | 数据报(有边界) |
| 应用场景 | HTTP、FTP、邮件 | DNS、视频、游戏 |
使用场景对比:
选择TCP:
✅ 需要可靠传输(文件下载、网页浏览)
✅ 数据完整性重要(银行转账、邮件)
✅ 顺序很重要(聊天消息)
选择UDP:
✅ 实时性要求高(视频通话、游戏)
✅ 可以容忍少量丢包(直播、语音)
✅ 数据量小且频繁(DNS查询)
✅ 广播/组播通信
2. TCP报文格式
2.1 TCP段结构
TCP传输的数据单元称为段(Segment):
TCP段 = TCP头部 + 数据
┌─────────────────────────────────────────────────────┐
│ TCP 头部 │
│ (20-60 字节) │
├─────────────────────────────────────────────────────┤
│ 应用数据 │
│ (可变长度) │
└─────────────────────────────────────────────────────┘
2.2 TCP头部格式(20字节最小)
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 源端口 (16位) | 目标端口 (16位) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 序列号 (32位) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 确认号 (32位) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 头部 | |C|E|U|A|P|R|S|F| |
| 长度 | 保留 |W|C|R|C|S|S|Y|I| 窗口大小 (16位) |
| (4位) | (3位) |R|E|G|K|H|T|N|N| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 校验和 (16位) | 紧急指针 (16位) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 选项 (可变长度,最多40字节) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 数据 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
2.3 字段详解
1. 源端口和目标端口(各16位)
用途:标识发送方和接收方的应用进程
示例:
源端口: 54321 (客户端随机端口)
目标端口: 80 (HTTP服务器端口)
2. 序列号(Sequence Number,32位)
作用:
- 标识发送的数据字节流中的位置
- 确保数据按序到达
- 范围:0 ~ 4,294,967,295
示例:
第1个TCP段:SEQ = 1000,数据长度100字节
第2个TCP段:SEQ = 1100 (1000 + 100)
第3个TCP段:SEQ = 1200
3. 确认号(Acknowledgment Number,32位)
作用:
- 期望收到的下一个字节的序列号
- ACK=1000 表示"我已收到999及之前的所有数据,期望收到1000"
特点:
- 累积确认(确认号之前的所有数据都已收到)
4. 头部长度(4位)
单位:32位字(4字节)
范围:5-15(表示20-60字节)
最小值:5 × 4 = 20字节(无选项)
最大值:15 × 4 = 60字节(40字节选项)
5. 保留位(3位)
保留未来使用,必须置0
6. 控制标志位(9位)
最重要的6个标志:
1. URG (Urgent,紧急)
- URG=1 表示紧急指针有效
- 很少使用
2. ACK (Acknowledgment,确认)
- ACK=1 表示确认号有效
- 除了第一个SYN包,其他都是1
3. PSH (Push,推送)
- PSH=1 要求立即推送数据给应用层
- 不要缓冲
4. RST (Reset,重置)
- RST=1 重置连接
- 用于异常关闭或拒绝连接
5. SYN (Synchronize,同步)
- SYN=1 请求建立连接
- 用于三次握手
6. FIN (Finish,结束)
- FIN=1 请求关闭连接
- 用于四次挥手
其他3个(较新):
- CWR: Congestion Window Reduced (拥塞窗口减少)
- ECE: ECN Echo (显式拥塞通知回显)
- NS: Nonce Sum (随机数和)
7. 窗口大小(16位)
作用:流量控制
含义:告诉对方自己的接收缓冲区还能接收多少字节
范围:0 ~ 65535 字节
窗口=0 表示"暂停发送,我的缓冲区满了"
示例:
Window = 8192 → "我最多还能接收8KB数据"
8. 校验和(16位)
作用:检测TCP段在传输过程中是否出错
计算范围:
- TCP伪头部(来自IP层)
- TCP头部
- TCP数据
如果校验失败 → 丢弃该段
9. 紧急指针(16位)
仅当URG=1时有效
指向紧急数据的最后一个字节
很少使用
10. 选项(可变长度)
常用选项:
1. MSS (Maximum Segment Size,最大段大小)
- 选项类型: 2
- 长度: 4字节
- 表示愿意接收的最大TCP段大小
- 通常在SYN包中协商
2. Window Scale (窗口扩大因子)
- 选项类型: 3
- 长度: 3字节
- 扩大窗口大小(突破65535限制)
- 实际窗口 = Window × 2^Scale
3. Timestamps (时间戳)
- 选项类型: 8
- 长度: 10字节
- 用于计算RTT和防止序列号回绕
4. SACK (Selective Acknowledgment,选择性确认)
- 选项类型: 5
- 允许确认不连续的数据块
3. TCP三次握手
3.1 为什么需要三次握手?
目的:
- 确认双方的发送和接收能力都正常
- 协商初始序列号(ISN)
- 协商连接参数(MSS、窗口大小等)
为什么不是两次或四次?
两次握手的问题:
- 客户端发送SYN
- 服务器回复SYN+ACK
❌ 服务器不知道客户端是否收到SYN+ACK
❌ 可能导致半开连接(服务器以为连接建立,客户端不知道)
❌ 旧的重复SYN可能导致资源浪费
三次握手刚好:
✅ 确认双方都能发送和接收
✅ 防止旧连接的初始化
✅ 资源开销合理
四次握手:
⚠️ 没必要,增加延迟
3.2 三次握手详细过程
客户端 服务器
CLOSED LISTEN
| |
| ① SYN=1, SEQ=x |
| "你好,我想建立连接,我的初始序列号是x" |
|─────────────────────────────────────────────>|
| |
SYN_SENT |
| |
| ② SYN=1, ACK=1, SEQ=y, ACK_NUM=x+1 |
| "好的,我同意。我的初始序列号是y,确认收到x" |
|<─────────────────────────────────────────────|
| SYN_RCVD
| |
| ③ ACK=1, SEQ=x+1, ACK_NUM=y+1 |
| "收到,我们开始通信吧!" |
|─────────────────────────────────────────────>|
| |
ESTABLISHED ESTABLISHED
| |
| 数据传输... |
|<─────────────────────────────────────────────>|
第一次握手(客户端 → 服务器):
TCP标志:SYN = 1
序列号: SEQ = x (客户端随机选择的ISN)
确认号: 无效(ACK=0)
客户端状态:CLOSED → SYN_SENT
服务器状态:LISTEN(保持不变)
作用:
- 客户端告诉服务器:"我想建立连接"
- 告诉服务器客户端的初始序列号
第二次握手(服务器 → 客户端):
TCP标志:SYN = 1, ACK = 1
序列号: SEQ = y (服务器随机选择的ISN)
确认号: ACK_NUM = x + 1
客户端状态:SYN_SENT(保持不变)
服务器状态:LISTEN → SYN_RCVD
作用:
- 服务器告诉客户端:"我同意建立连接"
- 告诉客户端服务器的初始序列号
- 确认收到了客户端的SYN(ACK=x+1)
第三次握手(客户端 → 服务器):
TCP标志:ACK = 1
序列号: SEQ = x + 1
确认号: ACK_NUM = y + 1
客户端状态:SYN_SENT → ESTABLISHED ✅
服务器状态:SYN_RCVD → ESTABLISHED ✅
作用:
- 客户端确认收到服务器的SYN+ACK
- 双方进入ESTABLISHED状态,可以开始传输数据
3.3 三次握手抓包示例
Wireshark抓包分析:
第1个包:客户端 → 服务器
TCP 54321 → 80 [SYN] Seq=0 Win=65535 Len=0 MSS=1460
第2个包:服务器 → 客户端
TCP 80 → 54321 [SYN, ACK] Seq=0 Ack=1 Win=29200 Len=0 MSS=1460
第3个包:客户端 → 服务器
TCP 54321 → 80 [ACK] Seq=1 Ack=1 Win=65535 Len=0
✅ 连接建立成功
3.4 三次握手的常见问题
Q1:第三次握手失败会怎样?
情况:
- 客户端发送第三次ACK
- ACK在网络中丢失,服务器没收到
服务器行为:
1. 服务器仍然处于SYN_RCVD状态
2. 等待超时后重传SYN+ACK
3. 重传几次后(默认5次)放弃,关闭连接
客户端行为:
1. 客户端已进入ESTABLISHED状态
2. 可以发送数据
3. 服务器收到数据包(含ACK)后也进入ESTABLISHED
Q2:SYN洪水攻击(SYN Flood)是什么?
攻击原理:
1. 攻击者发送大量SYN请求
2. 使用伪造的源IP地址
3. 服务器回复SYN+ACK到不存在的地址
4. 服务器维护大量半开连接(SYN_RCVD状态)
5. 耗尽服务器资源
防护措施:
- SYN Cookie技术
- 限制SYN请求速率
- 减少超时时间
- 增加半连接队列大小
4. TCP四次挥手
4.1 为什么需要四次挥手?
核心原因:TCP是全双工通信,双方都需要独立关闭自己的发送通道。
建立连接:握手(单向,一起建立)
关闭连接:挥手(双向,独立关闭)
类比:
建立电话:双方同时准备好就行
挂断电话:每个人都要说"再见"并确认
4.2 四次挥手详细过程
客户端 服务器
ESTABLISHED ESTABLISHED
| |
| ① FIN=1, SEQ=u |
| "我没有数据要发送了,准备关闭" |
|─────────────────────────────────────────────>|
| |
FIN_WAIT_1 |
| |
| ② ACK=1, SEQ=v, ACK_NUM=u+1 |
| "好的,我知道了。但我可能还有数据要发送" |
|<─────────────────────────────────────────────|
| CLOSE_WAIT
FIN_WAIT_2 |
| |
| 服务器继续发送数据... |
|<─────────────────────────────────────────────|
| |
| ③ FIN=1, ACK=1, SEQ=w, ACK_NUM=u+1 |
| "好了,我的数据也发送完了,可以关闭了" |
|<─────────────────────────────────────────────|
| LAST_ACK
TIME_WAIT |
| |
| ④ ACK=1, SEQ=u+1, ACK_NUM=w+1 |
| "收到,再见!" |
|─────────────────────────────────────────────>|
| |
| CLOSED
| (等待2MSL时间) |
| |
CLOSED |
第一次挥手(客户端 → 服务器):
TCP标志:FIN = 1, ACK = 1
序列号: SEQ = u
确认号: ACK_NUM = v
客户端状态:ESTABLISHED → FIN_WAIT_1
服务器状态:ESTABLISHED(保持不变)
含义:
- 客户端告诉服务器:"我没有数据要发送了"
- 客户端进入主动关闭状态
- 但客户端仍可以接收数据
第二次挥手(服务器 → 客户端):
TCP标志:ACK = 1
序列号: SEQ = v
确认号: ACK_NUM = u + 1
客户端状态:FIN_WAIT_1 → FIN_WAIT_2
服务器状态:ESTABLISHED → CLOSE_WAIT
含义:
- 服务器确认收到客户端的FIN
- 服务器可能还有数据要发送
- 服务器到客户端的方向还未关闭
第三次挥手(服务器 → 客户端):
TCP标志:FIN = 1, ACK = 1
序列号: SEQ = w
确认号: ACK_NUM = u + 1
客户端状态:FIN_WAIT_2(保持不变)
服务器状态:CLOSE_WAIT → LAST_ACK
含义:
- 服务器数据发送完毕
- 服务器请求关闭连接
- 等待客户端最后的确认
第四次挥手(客户端 → 服务器):
TCP标志:ACK = 1
序列号: SEQ = u + 1
确认号: ACK_NUM = w + 1
客户端状态:FIN_WAIT_2 → TIME_WAIT → CLOSED
服务器状态:LAST_ACK → CLOSED ✅
含义:
- 客户端确认收到服务器的FIN
- 服务器收到后立即关闭
- 客户端等待2MSL时间后关闭
4.3 TIME_WAIT状态
为什么需要TIME_WAIT?
客户端在发送最后一个ACK后不立即关闭,而是等待**2MSL(Maximum Segment Lifetime)**时间。
原因1:确保最后的ACK被服务器收到
场景:
1. 客户端发送最后的ACK
2. ACK在网络中丢失
3. 服务器超时重传FIN
4. 如果客户端已关闭,无法响应
5. 服务器会认为连接异常
解决:
客户端等待2MSL
如果收到重传的FIN,重新发送ACK
原因2:确保旧连接的数据包消失
场景:
1. 旧连接关闭
2. 立即用相同的四元组建立新连接
3. 旧连接的延迟数据包到达
4. 新连接收到旧数据,导致混乱
解决:
等待2MSL,确保所有数据包都消失
(1 MSL去程 + 1 MSL回程)
MSL时间:
RFC定义:2分钟
Linux默认:60秒(可调整)
Windows:4分钟
2MSL:
- Linux: 120秒
- Windows: 240秒
TIME_WAIT过多的问题:
现象:
netstat -an | grep TIME_WAIT | wc -l
显示大量TIME_WAIT连接
影响:
- 占用端口(客户端端口有限)
- 占用系统资源
解决方案:
1. 启用SO_REUSEADDR选项
2. 调整内核参数:
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
3. 减少MSL时间(需谨慎)
4. 使用连接池复用连接
4.4 四次挥手的特殊情况
情况1:三次挥手(同时关闭)
场景:双方同时发送FIN
客户端 服务器
| FIN |
|───────────────────────>|
| FIN |
|<───────────────────────|
| ACK |
|───────────────────────>|
| ACK |
|<───────────────────────|
结果:只需要3次握手(两个FIN + 两个ACK)
情况2:异常关闭(RST)
发送RST的情况:
1. 连接不存在时收到非SYN包
2. 连接队列溢出
3. 端口未监听
4. 应用程序调用close()时还有未读数据
RST特点:
- 立即关闭连接
- 不经过TIME_WAIT
- 接收方不发送ACK
5. TCP状态机
5.1 完整的TCP状态转换图
CLOSED
|
| (被动打开/listen)
↓
LISTEN ←───────────┐
| │
(主动打开/connect) | │
↓ | │
SYN_SENT | 收到SYN │
| | 发送SYN+ACK │
收到SYN+ACK| ↓ │
发送ACK | SYN_RCVD │
↓ | │
└─────→ ESTABLISHED ←───────────┘
| |
(主动关闭) | | (被动关闭)
发送FIN | | 收到FIN
| | 发送ACK
↓ ↓
FIN_WAIT_1 CLOSE_WAIT
| |
收到ACK | | (应用层关闭)
| | 发送FIN
↓ ↓
FIN_WAIT_2 LAST_ACK
| |
收到FIN | | 收到ACK
发送ACK | |
↓ ↓
TIME_WAIT CLOSED
|
(等待2MSL) |
↓
CLOSED
5.2 11种TCP状态详解
| 状态 | 说明 | 所处阶段 |
|---|---|---|
| CLOSED | 关闭状态,无连接 | 初始/结束 |
| LISTEN | 监听状态,等待连接 | 服务器 |
| SYN_SENT | 发送SYN后,等待SYN+ACK | 客户端三次握手 |
| SYN_RCVD | 收到SYN,发送SYN+ACK,等待ACK | 服务器三次握手 |
| ESTABLISHED | 连接建立,可以传输数据 | 数据传输 |
| FIN_WAIT_1 | 主动关闭,发送FIN,等待ACK或FIN | 主动关闭方 |
| FIN_WAIT_2 | 收到对方ACK,等待对方FIN | 主动关闭方 |
| TIME_WAIT | 收到FIN,发送ACK,等待2MSL | 主动关闭方 |
| CLOSE_WAIT | 收到对方FIN,发送ACK,等待关闭 | 被动关闭方 |
| LAST_ACK | 发送FIN,等待最后的ACK | 被动关闭方 |
| CLOSING | 双方同时关闭,等待ACK | 同时关闭 |
5.3 查看TCP连接状态
Linux/Mac:
# 查看所有TCP连接
netstat -ant
# 统计各状态连接数
netstat -ant | awk '{print $6}' | sort | uniq -c
# 查看ESTABLISHED连接
netstat -ant | grep ESTABLISHED
# 查看TIME_WAIT连接
netstat -ant | grep TIME_WAIT
# 使用ss命令(更快)
ss -ant
ss -ant state established
ss -ant state time-wait
Windows:
# 查看所有TCP连接
netstat -ano
# 查看指定端口
netstat -ano | findstr :80
# 查看连接统计
netstat -s -p tcp
6. 实战项目:TCP连接分析工具
6.1 项目功能
实现一个工具来:
- 模拟TCP三次握手
- 捕获和分析TCP连接
- 显示连接状态统计
- 检测异常连接
6.2 工具演示
# 查看TCP连接统计
python day15_tcp_analyzer.py --stats
# 监控指定端口的TCP连接
python day15_tcp_analyzer.py --monitor 80
# 测试TCP连接
python day15_tcp_analyzer.py --test www.baidu.com 80
# 显示连接状态分布
python day15_tcp_analyzer.py --state-distribution
7. 今日练习
练习1:观察三次握手
使用Wireshark抓包:
- 访问一个网站(如www.baidu.com)
- 筛选TCP流:
tcp.port == 80 - 找到三次握手的三个包
- 分析每个包的标志位、序列号、确认号
练习2:查看本机TCP连接
# Linux/Mac
netstat -ant | grep ESTABLISHED
ss -ant state established
# Windows
netstat -ano | findstr ESTABLISHED
思考:
1. 有多少个ESTABLISHED连接?
2. 它们连接到哪些服务器?
3. 有没有TIME_WAIT状态的连接?
练习3:计算三次握手时延
抓包分析:
- 记录第1个SYN的时间戳:t1
- 记录第2个SYN+ACK的时间戳:t2
- 记录第3个ACK的时间戳:t3
计算:
- 客户端到服务器RTT: (t2 - t1) × 2
- 完整握手时延: t3 - t1
练习4:理解TIME_WAIT
编写程序:
1. 创建TCP客户端
2. 连接服务器
3. 主动关闭连接
4. 使用netstat查看TIME_WAIT状态
5. 观察2MSL时间后状态变化
8. 常见问题
Q1:为什么建立连接是三次握手,关闭连接是四次挥手?
A:
- 建立连接:服务器可以把SYN和ACK合并在一个包中发送(SYN+ACK)
- 关闭连接:服务器收到FIN后,可能还有数据要发送,所以ACK和FIN要分开发送
Q2:如果三次握手的第三次ACK丢失会怎样?
A:
- 服务器会重传SYN+ACK(最多5次)
- 客户端已经认为连接建立,可以发送数据
- 服务器收到数据包(含ACK)后也认为连接建立
Q3:为什么TIME_WAIT要等2MSL?
A:
- 确保最后的ACK能够到达对方(1个MSL)
- 如果对方没收到,重传FIN需要1个MSL返回
- 确保旧连接的所有数据包都已消失
Q4:大量TIME_WAIT连接有什么危害?
A:
- 占用本地端口(客户端)
- 消耗系统资源(内存、CPU)
- 可能导致无法建立新连接
解决:
- 使用连接池
- 启用SO_REUSEADDR
- 调整系统参数
Q5:客户端和服务器都可以主动关闭连接吗?
A:
- 可以,任何一方都可以主动发送FIN
- 主动关闭方会进入TIME_WAIT状态
- 这就是为什么服务器端通常设计为由客户端主动关闭
9. 总结
今天我们学习了:
核心知识点
TCP特性:
- 面向连接
- 可靠传输
- 全双工通信
- 字节流服务
TCP报文格式:
- 20字节固定头部
- 重要字段:序列号、确认号、标志位、窗口大小
三次握手:
- SYN → SYN+ACK → ACK
- 目的:确认双方能力、协商参数、防止旧连接
四次挥手:
- FIN → ACK → FIN → ACK
- 原因:全双工,双向独立关闭
- TIME_WAIT:等待2MSL
TCP状态机:
- 11种状态
- 状态转换条件
- 异常情况处理
重点回顾
TCP三次握手:
1️⃣ 客户端 → 服务器:SYN
2️⃣ 服务器 → 客户端:SYN+ACK
3️⃣ 客户端 → 服务器:ACK
✅ 连接建立
TCP四次挥手:
1️⃣ 客户端 → 服务器:FIN
2️⃣ 服务器 → 客户端:ACK
3️⃣ 服务器 → 客户端:FIN
4️⃣ 客户端 → 服务器:ACK
⏳ TIME_WAIT (2MSL)
✅ 连接关闭
明天预告
第16天:TCP可靠传输机制
内容预览:
- 序列号和确认号详解
- 滑动窗口机制
- 超时重传
- 快速重传和快速恢复
- 流量控制
- 拥塞控制
继续加油! 🚀
今天我们深入学习了TCP协议的基础知识,理解了三次握手和四次挥手的完整过程。这些是TCP协议的核心,也是网络面试的高频考点。明天我们将学习TCP如何实现可靠传输!