TCP 三次握手与四次挥手
TCP 三次握手与四次挥手知识总结
一、TCP 连接与断开的核心机制
1. 三次握手(建立连接)
目的:
建立客户端与服务端之间的双向传输通道,确保双方都能确认对方的接收和发送能力,为后续的数据传输奠定可靠基础。
流程:
-
客户端发送 SYN
客户端发送 SYN 报文,请求建立连接,并包含初始序列号(SEQ),此时客户端进入 SYN_SENT 状态。 -
服务端回应 SYN-ACK
服务端收到 SYN 后,回应 SYN-ACK,其中 ACK 为客户端 SYN 的确认,ACK 号为客户端 SEQ+1。服务端也发送自己的 SYN,包含自己的初始序列号,此时服务端进入 SYN_RCVD 状态。 -
客户端发送 ACK
客户端收到 SYN-ACK 后,发送 ACK 报文,确认号为服务端的 SEQ+1,双方进入 ESTABLISHED 状态,连接建立成功。
关键状态:
- LISTEN(监听状态): 服务端调用
listen()
后进入该状态,等待客户端连接。 - ESTABLISHED(已建立连接): 连接建立后,客户端和服务端可以进行数据传输。
2. 四次挥手(断开连接)
目的:
安全拆除双向传输通道,确保双方都能完成数据的发送和接收,避免数据丢失。
流程:
(假设主动关闭方为 A,被动方为 B)
-
A 发送 FIN
A 发送 FIN,表示没有数据要发送,进入 FIN_WAIT_1 状态。 -
B 回复 ACK
B 回复 ACK,确认收到 A 的关闭请求,进入 CLOSE_WAIT 状态。 -
B 发送 FIN
B 完成数据发送后,发送 FIN,请求关闭 B 到 A 的通道,进入 LAST_ACK 状态。 -
A 回复 ACK
A 回复 ACK,确认 B 的关闭请求,进入 TIME_WAIT 状态,等待一段时间后才能真正关闭连接。
关键状态:
- TIME_WAIT: 主动关闭方等待残留报文消失,持续 2MSL 时间(通常 1-2 分钟)。
- CLOSE_WAIT: 被动关闭方等待应用层处理剩余数据。如果 CLOSE_WAIT 状态持续过长,可能是程序未正确关闭连接。
二、Socket 状态与命令工具
1. Socket 状态
- LISTEN: 服务端调用
listen()
后进入监听状态,等待客户端的连接请求。 - ESTABLISHED: 连接已建立,可以进行数据传输。
- TIME_WAIT: 主动关闭方等待残留报文消失的状态。
- CLOSE_WAIT: 被动关闭方等待应用层处理剩余数据。
2. 查看 Socket 状态
命令:
netstat -na | grep <端口号>
该命令列出特定端口的连接状态。例如,查看端口 8080 的连接状态:
netstat -na | grep 8080
关键列说明:
- 本地地址(Local Address): 本地主机的 IP 和端口号。
- 外网地址(Foreign Address): 远程主机的 IP 和端口号。
- 状态(State): 连接的当前状态,如 LISTEN、ESTABLISHED 等。
三、编程中的关键细节
1. 端口分配
- 服务端端口: 需要固定,使用
bind()
指定端口(如 HTTP 协议使用 80 端口)。 - 客户端端口: 由系统随机分配,通常从可用端口范围中选择未被占用的端口。
2. 高并发处理
backlog(已连接队列大小):
指定 listen()
的第二个参数,影响全连接队列的大小,默认值通常为 128,但可以根据实际需要增大。
示例:
listen(sockfd, 1024); // 设置队列长度为 1024
3. TIME_WAIT 问题与解决
问题:
主动关闭方在 TIME_WAIT
状态期间无法重用端口,可能导致高并发场景中的端口资源浪费。
解决方案:
设置 SO_REUSEADDR
属性来允许端口复用:
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
四、常见问题与面试要点
1. 为什么挥手需要四次?
- 全双工协议: TCP 需要独立关闭每一方向的传输通道。
- 数据发送完成: 被动关闭方可能需要继续发送数据,所以不能立即关闭连接,必须等到完成数据发送后再发送
FIN
。
2. SYN 泛洪攻击
攻击方式:
攻击者伪造大量源 IP 地址,发送大量 SYN
报文,消耗服务端资源。
防御方法:
启用 SYN Cookie 机制来防御该攻击。
3. TIME_WAIT 的意义
- 确保最后的 ACK 被送达: 防止被动关闭方没有收到最后的 ACK 而重发
FIN
。 - 等待残留报文消失: 确保旧连接的报文不会干扰新连接。
4. 常见面试问题
- SYN、ACK、FIN、SEQ 序列号的含义
- 半连接队列 vs 全连接队列
- CLOSE_WAIT 过多的原因
五、实际应用示例
1. 服务端代码示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#define PORT 8080
#define BACKLOG 1024
int main() {
int sockfd, new_sockfd;
struct sockaddr_in addr, client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int opt = 1;
// 创建 socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 设置端口复用
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) {
perror("setsockopt failed");
close(sockfd);
exit(EXIT_FAILURE);
}
// 绑定端口
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(PORT);
if (bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
perror("bind failed");
close(sockfd);
exit(EXIT_FAILURE);
}
// 监听连接
if (listen(sockfd, BACKLOG) == -1) {
perror("listen failed");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
// 接受客户端连接
new_sockfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_addr_len);
if (new_sockfd == -1) {
perror("accept failed");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("Client connected.\n");
// 处理客户端请求
close(new_sockfd);
close(sockfd);
return 0;
}
2. 客户端连接测试
使用 ab 工具测试并发连接:
ab -n 1000 -c 100 http://localhost:8080/
总结
理解三次握手和四次挥手是掌握 TCP 协议的基础,通过结合编程实践与网络状态分析工具(如 netstat
),可以有效排查连接问题并优化高并发场景下的服务端性能。对常见网络攻击(如 SYN 泛洪攻击)和连接状态(如 TIME_WAIT
、CLOSE_WAIT
)的理解和处理是网络编程和系统运维中不可或缺的技能。