计算机网络学习指南计算机网络学习指南
首页
基础教程
进阶内容
实战案例
编程指南
首页
基础教程
进阶内容
实战案例
编程指南
  • 进阶内容

    • 🚀 进阶内容
    • 🎯 学习目标
    • 🚀 学习路线
    • 📊 章节概览
  • 💡 学习建议
  • 🎓 学完之后
  • 第1章 - TCP 三次握手与四次挥手
  • 第2章 - 负载均衡原理
  • 第3章 - CDN 内容分发网络
  • 第4章 - VPN 与代理
  • 第5章 - 网络安全基础

第1章 - TCP 三次握手与四次挥手

嗨,朋友!

还记得在基础教程第5章我们学过 TCP 是一个可靠的传输协议吗?今天我们要深入了解 TCP 是如何做到"可靠"的,其中最重要的就是三次握手和四次挥手机制。

这两个概念在面试中经常被问到,而且在实际工作中也很重要。理解了它们,你就能明白为什么有时候网络连接会变慢,为什么会出现 TIME_WAIT 状态,以及如何防御 SYN 攻击。

🤔 为什么需要三次握手?

生活中的类比

想象一下,你要给朋友打电话:

第一次(你拨号):

  • 你:喂,能听到我说话吗?📞

第二次(朋友回应):

  • 朋友:能听到!你能听到我说话吗?📱

第三次(你确认):

  • 你:能听到!那我们开始聊吧!✅

这三次对话确保了:

  1. 你的话筒和朋友的听筒都正常(你→朋友的通道 ✅)
  2. 朋友的话筒和你的听筒都正常(朋友→你的通道 ✅)
  3. 双方都准备好了可以开始通话

TCP 的三次握手也是一样的道理!

正式定义

三次握手的目的

  1. 确认双方的收发能力都正常
  2. 协商初始序列号(用于数据包排序和去重)
  3. 协商其他参数(如窗口大小、最大报文段长度等)

📖 三次握手详细过程

状态转换图

客户端状态                 服务器状态
CLOSED                    LISTEN(监听中)
   |
   | 发送 SYN
   ↓
SYN_SENT ─────────────→  SYN_RCVD
   |                         |
   |                         | 发送 SYN + ACK
   | ←─────────────────────┘
   |
   | 发送 ACK
   ↓                         ↓
ESTABLISHED ──────────→ ESTABLISHED
(连接建立)              (连接建立)

第一次握手(SYN)

客户端 → 服务器

客户端发送一个 SYN(Synchronize) 包,告诉服务器:

我想和你建立连接!
我的初始序列号是 x

关键信息:

  • SYN = 1(标志位,表示这是一个同步请求)
  • seq = x(客户端的初始序列号,随机生成)
  • 客户端进入 SYN_SENT 状态

为什么序列号要随机?

  • 安全性:防止被恶意猜测和劫持
  • 避免混淆:防止旧连接的数据包干扰新连接

第二次握手(SYN + ACK)

服务器 → 客户端

服务器收到 SYN 包后,如果同意建立连接,会发送一个 SYN + ACK 包:

好的,我收到了你的请求!
我的初始序列号是 y
我确认收到了你的序列号 x

关键信息:

  • SYN = 1(服务器也要同步)
  • ACK = 1(确认标志位)
  • seq = y(服务器的初始序列号)
  • ack = x + 1(确认号,表示期望收到客户端的下一个序列号)
  • 服务器进入 SYN_RCVD 状态

第三次握手(ACK)

客户端 → 服务器

客户端收到服务器的 SYN + ACK 后,再发送一个 ACK 包:

好的,我也收到了!
现在可以开始传输数据了!

关键信息:

  • ACK = 1
  • seq = x + 1
  • ack = y + 1(确认服务器的序列号)
  • 双方都进入 ESTABLISHED 状态
  • 可以开始传输数据了!

第三次握手可以携带数据吗?

可以!因为此时客户端已经确认了服务器的接收能力,所以第三次握手的 ACK 包可以携带应用层数据。但前两次握手不能携带数据,因为还没有完全确认连接。

🎯 为什么是三次,不是两次或四次?

为什么不是两次握手?

问题场景:

假设只有两次握手:

1. 客户端发送 SYN
2. 服务器回复 SYN + ACK,连接建立 ✅

看起来没问题?但考虑这个情况:

  1. 客户端发送了一个 SYN 包(包A)
  2. 因为网络延迟,包A 在网络中滞留了很久
  3. 客户端以为丢包了,又发送了一个 SYN 包(包B)
  4. 包B 正常到达,连接建立并完成通信,然后断开
  5. 过了很久,包A 终于到达服务器
  6. 服务器以为这是一个新的连接请求,发送 SYN + ACK
  7. 在两次握手的情况下,连接就建立了!
  8. 但客户端根本不知道这个连接,不会发送数据
  9. 服务器一直等待,浪费资源!❌

三次握手的解决:

在第7步,服务器发送 SYN + ACK 后,需要等待客户端的第三次握手(ACK)。但客户端不会发送这个 ACK(因为它根本不知道这个连接),所以服务器最终会超时关闭,不会浪费资源。✅

为什么不是四次握手?

没必要!三次已经足够确认双方的收发能力和同步序列号了。四次握手只会增加延迟,没有额外的好处。

🔥 四次挥手详细过程

为什么需要四次挥手?

断开连接为什么需要四次,而建立连接只需要三次呢?

核心原因:TCP 是全双工通信(双向都可以传输数据)。

  • 建立连接:双方都只需要表达"我准备好了",可以合并在一起
  • 断开连接:每一方都要独立表达"我发完了"和"我同意你发完了",不能合并

状态转换图

客户端状态                    服务器状态
ESTABLISHED                 ESTABLISHED
   |
   | ① 发送 FIN
   ↓
FIN_WAIT_1 ─────────────→  CLOSE_WAIT
   |                           |
   | ② 收到 ACK                 | (服务器可以继续发送数据)
   ↓                           |
FIN_WAIT_2                     |
   |                           | ③ 发送 FIN
   | ←─────────────────────────┘
   |                           ↓
   | ④ 发送 ACK              LAST_ACK
   ↓                           |
TIME_WAIT                      | 收到 ACK
   |                           ↓
   | (等待 2MSL)             CLOSED
   ↓
CLOSED

第一次挥手(FIN)

客户端 → 服务器

客户端发送一个 FIN(Finish) 包:

我的数据发送完了,准备关闭连接!

关键信息:

  • FIN = 1
  • seq = u
  • 客户端进入 FIN_WAIT_1 状态
  • 注意:客户端还可以接收数据,只是不再发送数据

第二次挥手(ACK)

服务器 → 客户端

服务器收到 FIN 后,先发送一个 ACK:

好的,我知道你要关闭了!
但我可能还有数据要发送,请稍等...

关键信息:

  • ACK = 1
  • ack = u + 1
  • 服务器进入 CLOSE_WAIT 状态
  • 客户端收到后进入 FIN_WAIT_2 状态

CLOSE_WAIT 状态

服务器处于 CLOSE_WAIT 状态时,可以继续发送数据给客户端。这就是为什么需要四次挥手,而不是三次。

第三次挥手(FIN)

服务器 → 客户端

服务器发送完剩余数据后,发送 FIN 包:

好了,我的数据也发送完了!
现在可以关闭连接了!

关键信息:

  • FIN = 1
  • ACK = 1
  • seq = w
  • ack = u + 1
  • 服务器进入 LAST_ACK 状态

第四次挥手(ACK)

客户端 → 服务器

客户端收到服务器的 FIN 后,发送最后一个 ACK:

好的,确认收到!连接关闭!

关键信息:

  • ACK = 1
  • ack = w + 1
  • 客户端进入 TIME_WAIT 状态
  • 服务器收到后进入 CLOSED 状态
  • 客户端等待 2MSL 后进入 CLOSED 状态

⏰ TIME_WAIT 状态详解

什么是 TIME_WAIT?

客户端发送最后一个 ACK 后,不会立即关闭连接,而是进入 TIME_WAIT 状态,等待 2MSL(Maximum Segment Lifetime)时间。

MSL:一个报文在网络中存活的最长时间,通常是 30秒、1分钟或2分钟(取决于操作系统)。

2MSL:所以 TIME_WAIT 通常等待 1-4 分钟。

为什么需要 TIME_WAIT?

原因1:确保最后的 ACK 能够到达

场景:客户端发送最后的 ACK 后立即关闭

客户端 ────ACK────X  服务器
         (ACK 丢失)  |
                     | 超时重传 FIN
     ?  ←───FIN────┘
   (已关闭,无法响应)

结果:服务器一直重传 FIN,无法正常关闭 ❌

有了 TIME_WAIT:

客户端 ────ACK────X  服务器
   |     (ACK 丢失)  |
   |                 | 超时重传 FIN
   | ←───FIN────────┘
   |
   | (还在 TIME_WAIT,可以重新发送 ACK)
   |
   | ────ACK───────→ ✅

原因2:避免旧连接的数据包干扰新连接

如果立即关闭,新连接可能会使用相同的端口,旧连接的延迟数据包可能会干扰新连接。

等待 2MSL 可以确保网络中所有旧连接的数据包都已经消失。

TIME_WAIT 过多怎么办?

问题场景:

在高并发的服务器上(比如短连接的 HTTP 服务),可能会有大量的 TIME_WAIT 连接,占用端口资源。

查看 TIME_WAIT 数量:

# Linux
netstat -an | grep TIME_WAIT | wc -l

# Windows
netstat -an | findstr TIME_WAIT

解决方案:

  1. 开启 SO_REUSEADDR(应用层):
// Node.js 示例
const server = net.createServer();
server.listen({
  port: 8080,
  host: '0.0.0.0',
  reuseAddress: true  // 允许重用 TIME_WAIT 状态的端口
});
  1. 调整系统参数(操作系统层):
# Linux - 减少 TIME_WAIT 时间(慎用!)
sudo sysctl -w net.ipv4.tcp_fin_timeout=30

# 开启 TIME_WAIT 重用
sudo sysctl -w net.ipv4.tcp_tw_reuse=1
  1. 使用连接池(推荐):
// 使用 HTTP Keep-Alive 保持连接
const http = require('http');
const agent = new http.Agent({
  keepAlive: true,
  maxSockets: 50
});

http.get('http://example.com', { agent }, (res) => {
  // ...
});

🔒 常见问题:半连接队列和全连接队列

半连接队列(SYN Queue)

存储内容:收到 SYN 但还没完成三次握手的连接(处于 SYN_RCVD 状态)

队列满了会怎样:

  • 新的 SYN 包会被丢弃
  • 客户端会超时重传
  • 用户感觉连接很慢或连接失败

全连接队列(Accept Queue)

存储内容:完成三次握手,等待应用程序 accept() 的连接(ESTABLISHED 状态)

队列满了会怎样:

  • 新的连接请求会被拒绝
  • 或者丢弃第三次握手的 ACK(取决于配置)

查看队列状态

# Linux - 查看队列溢出统计
netstat -s | grep -i "listen"

# 查看具体应用的队列状态
ss -lnt

调整队列大小

// Node.js - 设置全连接队列大小
const server = net.createServer();
server.listen(8080, '0.0.0.0', 511); // 第三个参数是 backlog
# Linux - 系统级调整
sudo sysctl -w net.ipv4.tcp_max_syn_backlog=2048  # 半连接队列
sudo sysctl -w net.core.somaxconn=1024            # 全连接队列

⚔️ SYN 攻击与防御

什么是 SYN 攻击?

SYN 洪水攻击(SYN Flood)是一种常见的 DDoS 攻击方式。

攻击原理:

攻击者不断发送 SYN 包,但不回复第三次握手的 ACK

攻击者 ──SYN──→ 服务器
攻击者 ──SYN──→ 服务器  
攻击者 ──SYN──→ 服务器
攻击者 ──SYN──→ 服务器
  ...
  
服务器的半连接队列很快被占满
正常用户无法连接 ❌

危害:

  • 半连接队列被占满
  • 服务器资源被耗尽
  • 正常用户无法建立连接

防御措施

1. SYN Cookie

原理:不使用半连接队列,而是用算法计算一个 Cookie 值作为序列号。

# Linux - 开启 SYN Cookie
sudo sysctl -w net.ipv4.tcp_syncookies=1

优点:

  • ✅ 不占用半连接队列
  • ✅ 能防御 SYN 攻击

缺点:

  • ❌ 无法保存 TCP 选项(如窗口大小)
  • ❌ 有轻微性能损耗

2. 增加半连接队列大小

sudo sysctl -w net.ipv4.tcp_max_syn_backlog=4096

3. 减少 SYN_RCVD 超时时间

# 默认可能是 60 秒,可以减少到 30 秒
sudo sysctl -w net.ipv4.tcp_synack_retries=1

4. 使用防火墙和负载均衡器

# iptables 限制单个 IP 的 SYN 速率
sudo iptables -A INPUT -p tcp --syn -m limit --limit 1/s --limit-burst 3 -j ACCEPT
sudo iptables -A INPUT -p tcp --syn -j DROP

💻 实战:监控 TCP 连接状态

Node.js 服务器示例

const net = require('net');

const server = net.createServer((socket) => {
  console.log('客户端已连接');
  console.log('本地地址:', socket.localAddress, ':', socket.localPort);
  console.log('远程地址:', socket.remoteAddress, ':', socket.remotePort);
  
  // 监听数据
  socket.on('data', (data) => {
    console.log('收到数据:', data.toString());
    socket.write('Echo: ' + data);
  });
  
  // 监听连接关闭
  socket.on('end', () => {
    console.log('客户端断开连接(发送了 FIN)');
  });
  
  socket.on('close', () => {
    console.log('连接已完全关闭');
  });
  
  socket.on('error', (err) => {
    console.error('连接错误:', err);
  });
});

server.listen(8080, () => {
  console.log('TCP 服务器启动在端口 8080');
});

// 监听服务器错误
server.on('error', (err) => {
  if (err.code === 'EADDRINUSE') {
    console.error('端口已被占用');
  } else {
    console.error('服务器错误:', err);
  }
});

使用 tcpdump 抓包

# 抓取 TCP 握手包
sudo tcpdump -i any port 8080 -nn

# 只看 SYN 包
sudo tcpdump -i any 'tcp[tcpflags] & tcp-syn != 0' -nn

# 只看 FIN 包
sudo tcpdump -i any 'tcp[tcpflags] & tcp-fin != 0' -nn

输出示例:

# 三次握手
14:23:45.123456 IP 192.168.1.100.54321 > 192.168.1.200.8080: Flags [S], seq 1234567890
14:23:45.123789 IP 192.168.1.200.8080 > 192.168.1.100.54321: Flags [S.], seq 9876543210, ack 1234567891
14:23:45.124012 IP 192.168.1.100.54321 > 192.168.1.200.8080: Flags [.], ack 9876543211

# 四次挥手
14:25:30.567890 IP 192.168.1.100.54321 > 192.168.1.200.8080: Flags [F.], seq 1234567900
14:25:30.568123 IP 192.168.1.200.8080 > 192.168.1.100.54321: Flags [.], ack 1234567901
14:25:30.568456 IP 192.168.1.200.8080 > 192.168.1.100.54321: Flags [F.], seq 9876543220
14:25:30.568789 IP 192.168.1.100.54321 > 192.168.1.200.8080: Flags [.], ack 9876543221

标志说明:

  • [S] = SYN
  • [.] = ACK
  • [S.] = SYN + ACK
  • [F.] = FIN + ACK

Python 客户端测试

import socket
import time

# 创建 TCP 客户端
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

print('准备连接服务器...')
try:
    # 三次握手
    client.connect(('127.0.0.1', 8080))
    print('连接成功!')
    
    # 发送数据
    client.send(b'Hello, Server!')
    print('数据已发送')
    
    # 接收响应
    response = client.recv(1024)
    print('收到响应:', response.decode())
    
    time.sleep(1)
    
    # 四次挥手
    print('准备关闭连接...')
    client.close()
    print('连接已关闭')
    
except Exception as e:
    print('错误:', e)
finally:
    client.close()

📊 总结对比表

三次握手 vs 四次挥手

对比项三次握手四次挥手
次数3次4次
目的建立连接断开连接
发起方客户端客户端或服务器
能否合并第2、3次可以携带数据第2、3次通常不能合并
关键状态SYN_SENT, SYN_RCVDFIN_WAIT_1/2, CLOSE_WAIT, TIME_WAIT
特殊等待无TIME_WAIT(2MSL)

TCP 连接状态速查表

状态说明何时出现
CLOSED关闭状态初始状态
LISTEN监听状态服务器等待连接
SYN_SENT已发送 SYN客户端发起连接后
SYN_RCVD已收到 SYN服务器收到 SYN 后
ESTABLISHED连接已建立三次握手完成
FIN_WAIT_1等待对方 FIN发送 FIN 后
FIN_WAIT_2等待对方 FIN收到对方 ACK 后
CLOSE_WAIT等待关闭收到对方 FIN 后
CLOSING双方同时关闭同时发送 FIN
LAST_ACK等待最后 ACK发送 FIN 后
TIME_WAIT时间等待发送最后 ACK 后

🎯 学习建议

1. 理解原理优先 🧠

不要死记硬背"三次握手"、"四次挥手"的步骤,要理解为什么要这样设计:

  • 为什么需要三次而不是两次?→ 防止旧连接干扰
  • 为什么断开需要四次?→ 全双工通信,双方都要独立关闭
  • 为什么需要 TIME_WAIT?→ 确保 ACK 到达和避免数据包混淆

2. 动手实践 💻

  • 用 tcpdump 或 Wireshark 抓包,看真实的握手和挥手过程
  • 用 netstat 观察连接状态的变化
  • 写代码测试不同场景(正常关闭、异常关闭、TIME_WAIT 等)

3. 关注实际问题 🔍

  • 生产环境中 TIME_WAIT 过多怎么办?
  • 如何防御 SYN 攻击?
  • 连接建立慢可能是什么原因?

📝 练习题

基础题

  1. 为什么 TCP 需要三次握手?两次握手有什么问题?
点击查看答案

两次握手的问题:

  • 无法防止旧连接的 SYN 包建立无效连接
  • 客户端无法确认服务器是否收到了自己的 ACK
  • 容易造成服务器资源浪费

三次握手可以:

  • 确认双方的收发能力
  • 防止旧连接干扰
  • 同步双方的初始序列号
  1. TIME_WAIT 状态的作用是什么?等待多长时间?
点击查看答案

作用:

  1. 确保最后的 ACK 能够到达服务器(如果丢失,服务器会重传 FIN)
  2. 确保旧连接的数据包在网络中消失,不会干扰新连接

时间:2MSL(Maximum Segment Lifetime)

  • MSL 通常是 30秒-2分钟
  • 所以 TIME_WAIT 通常等待 1-4 分钟
  1. 半连接队列和全连接队列有什么区别?
点击查看答案

半连接队列(SYN Queue):

  • 存储处于 SYN_RCVD 状态的连接
  • 已收到 SYN,但还未完成三次握手
  • 队列满了会丢弃新的 SYN 包

全连接队列(Accept Queue):

  • 存储处于 ESTABLISHED 状态的连接
  • 已完成三次握手,等待应用程序 accept()
  • 队列满了会拒绝新连接

进阶题

  1. 如何检测和防御 SYN 攻击?
点击查看答案

检测方法:

# 查看半连接队列溢出
netstat -s | grep -i "SYNs to LISTEN"

# 查看 SYN_RCVD 状态的连接数
netstat -an | grep SYN_RCVD | wc -l

防御措施:

  1. 开启 SYN Cookie:sysctl -w net.ipv4.tcp_syncookies=1
  2. 增加半连接队列大小:sysctl -w net.ipv4.tcp_max_syn_backlog=4096
  3. 减少 SYN-ACK 重传次数:sysctl -w net.ipv4.tcp_synack_retries=1
  4. 使用防火墙限制 SYN 速率
  5. 使用负载均衡器和 CDN 分散流量
  1. 生产环境中发现大量 TIME_WAIT 连接,应该如何优化?
点击查看答案

优化策略:

  1. 应用层优化(推荐):

    • 使用 HTTP Keep-Alive 保持长连接
    • 使用连接池复用连接
    • 让客户端主动关闭连接(TIME_WAIT 在客户端)
  2. 系统参数优化(谨慎使用):

    # 允许 TIME_WAIT 重用
    sysctl -w net.ipv4.tcp_tw_reuse=1
    
    # 减少 FIN_WAIT 超时时间
    sysctl -w net.ipv4.tcp_fin_timeout=30
    
  3. 架构优化:

    • 使用负载均衡器分散连接
    • 增加服务器数量
    • 使用更多的 IP 地址和端口

注意:不要使用 tcp_tw_recycle,它在 NAT 环境下会导致问题,已在 Linux 4.12 中移除。

  1. 如果在第三次握手时,客户端发送的 ACK 丢失了会怎样?
点击查看答案

场景分析:

客户端                    服务器
SYN_SENT ──SYN──→    LISTEN
          ←─SYN+ACK─ SYN_RCVD
ESTABLISHED ──ACK──X SYN_RCVD (ACK 丢失)

结果:

  1. 客户端:

    • 认为连接已建立(ESTABLISHED)
    • 可以开始发送数据
  2. 服务器:

    • 仍处于 SYN_RCVD 状态
    • 超时后会重传 SYN+ACK
    • 如果客户端收到重传的 SYN+ACK,会再次发送 ACK
    • 如果一直收不到,最终超时关闭连接
  3. 如果客户端发送数据:

    • 服务器会收到数据包(带有 ACK 标志)
    • 服务器会认为这是第三次握手的 ACK
    • 连接进入 ESTABLISHED 状态
    • 所以实际上不会有问题!

结论:TCP 的设计非常健壮,即使第三次握手的 ACK 丢失,也可以通过后续的数据包恢复连接状态。

🎓 下一步学习

恭喜你!现在你已经深入理解了 TCP 连接的建立和断开过程。

建议继续学习:

  • 第2章 - 负载均衡原理 - 了解如何提高系统性能
  • 第5章 - 网络安全基础 - 学习更多网络攻击和防御
  • 基础教程第5章 - 回顾 TCP 基础知识

推荐阅读:

  • 《TCP/IP 详解 卷1:协议》- 经典教材
  • RFC 793 - TCP 协议标准文档

关键概念回顾:

  • ✅ 三次握手:SYN → SYN+ACK → ACK
  • ✅ 四次挥手:FIN → ACK → FIN → ACK
  • ✅ TIME_WAIT:等待 2MSL,确保连接正常关闭
  • ✅ SYN 攻击:利用半连接队列的 DDoS 攻击

返回进阶内容 | 下一章:负载均衡原理

最近更新: 2025/12/27 10:13
Contributors: 王长安
Prev
🎓 学完之后
Next
第2章 - 负载均衡原理