3. TCP三次握手与Socket:从理论到代码的落地
TCP三次握手,说白了就是两台机器建立连接前的一次「互相确认」。你发一个SYN,我回一个SYN+ACK,你再回一个ACK——这个流程大家背得滚瓜烂熟。但真正落到Socket代码里,每一步对应什么API、内核做了什么、网络栈怎么处理,很多人就懵了。
我刚开始做网络编程时,以为三次握手就是三次send/recv。结果线上服务偶尔出现连接超时,查了三天才发现是accept队列满了。嗯,这里面的门道,咱们今天一次讲透。
3.1 三次握手:不只是「你好、你好、你好」
先看标准流程。客户端和服务端通过三次报文交换,完成序列号同步和窗口协商。我用一张图把整个过程画清楚:
这张图里我特意把Socket API标注在对应位置。你会发现:connect() 只触发第一次握手,accept() 要等到三次握手全部完成才能拿到连接。这个时间差,就是很多问题的根源。
3.2 Socket API 在三次握手中的作用
很多人以为 connect() 返回就代表连接建立好了。其实 connect() 返回 只代表第三次握手发出去了,不代表服务端已经处理完。我踩过这个坑——客户端 connect() 成功立即发数据,结果服务端还没从 accept() 返回,数据就丢了。
3.2.1 connect():客户端的「敲门砖」
connect() 干了三件事:
- 构造 SYN 报文:内核自动填充源端口、序列号、窗口大小
- 发送 SYN:进入 SYN_SENT 状态
- 等待 SYN+ACK:阻塞直到收到服务端回复
connect() 返回时,客户端已经进入 ESTABLISHED 状态。但注意——服务端此时还在 SYN_RCVD 状态,要等第三次握手到达才变成 ESTABLISHED。
3.2.2 accept():服务端的「取号机」
accept() 不参与握手过程。它只是从内核的「已完成连接队列」里取一个连接。这个队列由三次握手完成后自动填充。
关键点:
- accept() 之前:三次握手已经完成,连接已经建立
- accept() 返回:拿到一个新的 socket fd,代表这个连接
- accept() 阻塞:如果队列为空,进程会休眠
3.3 三次握手的完整状态变迁
我把客户端和服务端的状态变化整理成一张表,方便对照:
| 步骤 | 客户端状态 | 服务端状态 | 报文 | Socket API |
|---|---|---|---|---|
| 初始 | CLOSED | LISTEN | - | socket() + bind() + listen() |
| 第一次握手 | SYN_SENT | SYN_RCVD | SYN (seq=x) | connect() 触发 |
| 第二次握手 | ESTABLISHED | SYN_RCVD | SYN+ACK (seq=y, ack=x+1) | 内核自动回复 |
| 第三次握手 | ESTABLISHED | ESTABLISHED | ACK (ack=y+1) | connect() 返回 / accept() 可取 |
注意一个细节:客户端在收到 SYN+ACK 后就进入 ESTABLISHED 了,比服务端早。这意味着客户端可以立即发数据,但数据要等到服务端完成第三次握手后才能被处理。
3.4 SYN Flood 攻击:三次握手的「死穴」
SYN Flood 是经典的 DDoS 攻击方式。原理很简单:攻击者伪造大量 SYN 报文,但不完成第三次握手。服务端为每个 SYN 分配资源(半连接队列),很快就把内存耗尽,无法处理正常连接。
3.4.1 攻击流程
- 攻击者发送大量 SYN 报文,源 IP 伪造为不存在的主机
- 服务端回复 SYN+ACK,等待 ACK
- 服务端将连接放入半连接队列(SYN_RCVD 状态)
- 攻击者不回复 ACK,半连接队列逐渐填满
- 新连接无法建立,服务端拒绝服务
3.4.2 防御手段
我整理了几种常见的防御方案,以及它们的优缺点:
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| SYN Cookie | 收到 SYN 时不分配资源,用 Cookie 编码信息 | 几乎不消耗内存 | 丢失部分 TCP 选项 |
| 增大 backlog | 调大半连接队列长度 | 简单粗暴 | 治标不治本 |
| SYN 重试 + 超时 | 减少 SYN+ACK 重试次数,加快超时 | 释放资源快 | 正常用户可能受影响 |
| 防火墙过滤 | 识别异常 SYN 流量并丢弃 | 从源头阻断 | 需要硬件/软件支持 |
3.4.3 代码层面的防御
在应用层,我们可以通过调整 Socket 选项来缓解 SYN Flood:
// 开启 SYN Cookie(Linux 内核参数)
// /etc/sysctl.conf 或 sysctl 命令
net.ipv4.tcp_syncookies = 1
// 调大半连接队列
net.ipv4.tcp_max_syn_backlog = 4096
// 减少 SYN+ACK 重试次数
net.ipv4.tcp_synack_retries = 2
// 开启 TCP 时间戳(辅助 SYN Cookie)
net.ipv4.tcp_timestamps = 1
这些参数在 /etc/sysctl.conf 里配置,执行 sysctl -p 生效。我个人习惯在部署脚本里自动设置,避免每次手动改。
3.5 本章小结
三次握手不是简单的三次报文交换。它背后涉及 Socket API 的调用时机、内核队列的管理、以及安全风险。记住几个关键点:
- connect() 只触发第一次握手,accept() 等三次握手完成
- 服务端在 SYN_RCVD 状态就分配了资源,这是 SYN Flood 的根源
- SYN Cookie 是有效的防御手段,但有副作用
- 调参要谨慎,别为了防攻击牺牲正常用户体验