10个Tcp三次握手四次挥手题目整理
【TCP连接管理核心总结】 本文整理了TCP三次握手与四次挥手的关键知识点,包括: 三次握手流程与状态变化,强调"确认双方收发能力"和"防止历史连接"的设计初衷 四层挥手过程详解,剖析TIME_WAIT状态的2MSL等待机制及其必要性 实战经验: 全连接队列溢出导致丢连接的解决方案 端口重用(SO_REUSEADDR)处理TIME_WAIT问题 SYN Co
TCP三次握手与四次挥手:核心知识点整理
这篇文章整理了TCP连接管理相关的核心问题,包括自己的理解、踩过的坑,以及在C++ Reactor项目中的实践经验。
目录
- 前言
- Q1: TCP三次握手的完整过程
- Q2: 为什么需要三次握手?两次不行吗?
- Q3: 第三次握手可以携带数据吗?
- Q4: 什么是SYN攻击?如何防御?
- Q5: 什么是半连接队列和全连接队列?
- Q6: TCP四次挥手的完整过程
- Q7: TIME_WAIT状态的作用
- Q8: 大量CLOSE_WAIT说明什么问题?
- Q9: close() vs shutdown()
- Q10: 在项目中如何处理连接关闭
- 学习总结
前言
这段时间在看王道的计算机网络课程,发现TCP连接管理这部分概念特别多:三次握手、四次挥手、TIME_WAIT、CLOSE_WAIT…光记住流程还不够,更重要的是理解为什么要这样设计。
这篇文章整理了我认为最重要的10个问题,每个问题我都会写下自己的理解,以及在实现Reactor项目时遇到的实际问题。
Q1: TCP三次握手的完整过程
我的理解
TCP建立连接需要三次握手,完整流程如下:
第一次握手:
客户端发送:SYN=1, seq=x
客户端状态:CLOSED → SYN_SENT
客户端向服务器发起连接请求。
第二次握手:
服务器发送:SYN=1, ACK=1, seq=y, ack=x+1
服务器状态:LISTEN → SYN_RCVD
服务器确认收到请求,同时也向客户端发起连接请求。
第三次握手:
客户端发送:ACK=1, ack=y+1
客户端:SYN_SENT → ESTABLISHED
服务器:SYN_RCVD → ESTABLISHED
客户端确认,连接建立完成。
状态变化图:
客户端 服务器
CLOSED LISTEN
| |
|--- SYN, seq=x -------------->|
| |
SYN_SENT SYN_RCVD
| |
|<-- SYN+ACK, seq=y, ack=x+1 --|
| |
|--- ACK, ack=y+1 ------------->|
| |
ESTABLISHED ESTABLISHED
我原来的疑问
Q:为什么ACK号是x+1,而不是x?
一开始我也搞不清楚,后来理解了:
- seq表示"我这次发送的数据从序号x开始"
- ack表示"我期望你下次从序号x+1开始发"
- 所以确认号永远是"对方seq + 1"
Q:客户端和服务器的状态为什么不同?
因为两者的角色不同:
- 客户端是主动发起方,状态是:CLOSED → SYN_SENT → ESTABLISHED
- 服务器是被动接受方,状态是:LISTEN → SYN_RCVD → ESTABLISHED
Q2: 为什么需要三次握手?两次不行吗?
我的理解
看完王道视频后,我总结了两个关键原因:
原因1:确认双方的收发能力
- 第一次握手:服务器确认"客户端能发"
- 第二次握手:客户端确认"服务器能发能收,客户端能收"
- 第三次握手:服务器确认"客户端能收"
如果只有两次握手,服务器无法确认"客户端能收"。
原因2:防止历史连接
这个例子帮我理解了:
场景:客户端发送的旧SYN因为网络延迟,在连接关闭后才到达服务器
两次握手的问题:
旧SYN → 服务器:建立连接
服务器:分配资源,等待数据
客户端:不知情,不会发数据
结果:服务器资源浪费
三次握手的解决:
旧SYN → 服务器:发送SYN+ACK
客户端:收到后发现不是自己期望的连接,发送RST
服务器:释放资源
结果:避免了错误连接
项目中的体现
在实现Reactor项目时,我用accept()从全连接队列中取出已经完成三次握手的连接:
int connfd = accept(listenfd, NULL, NULL);
// 此时TCP已经完成了三次握手
// connfd已经是ESTABLISHED状态
这个过程对应用层是透明的,内核自动完成了三次握手。
Q3: 第三次握手可以携带数据吗?
我的理解
可以!
前两次握手不能携带数据,因为连接还没完全建立,如果允许携带数据,攻击者可以发送大量带数据的SYN,造成SYN Flood攻击。
但第三次握手时,客户端已经进入ESTABLISHED状态,连接已经建立,所以可以携带数据。
实际应用
HTTP/1.1的请求就是在第三次握手中发送的:
第一次:客户端 → 服务器:SYN
第二次:服务器 → 客户端:SYN+ACK
第三次:客户端 → 服务器:ACK + HTTP请求(GET /index.html)
这样可以节省一个RTT,提高效率。
Q4: 什么是SYN攻击?如何防御?
我的理解
SYN攻击原理:
攻击者发送大量SYN请求(伪造IP地址),但不回复ACK。服务器会为每个SYN分配资源并等待,直到超时(默认63秒)。大量半连接会占满服务器资源,导致无法处理正常请求。
防御方法:SYN Cookie
最有效的防御方法是SYN Cookie:
- 不分配资源,将连接信息编码到seq中
- seq = hash(客户端IP, 端口, 服务器IP, 端口, 密钥)
- 收到ACK后验证Cookie,再分配资源
这样攻击者发再多SYN也不会占用服务器资源。
在Linux中启用:
echo 1 > /proc/sys/net/ipv4/tcp_syncookies
项目中的考虑
在部署Reactor服务器时,我查了一下系统配置:
cat /proc/sys/net/ipv4/tcp_syncookies
# 输出:1(已启用)
现代Linux系统默认都开启了SYN Cookie,所以不用太担心SYN攻击。
Q5: 什么是半连接队列和全连接队列?
我的理解
服务器维护两个队列:
半连接队列(SYN队列):
- 存储处于SYN_RCVD状态的连接
- 收到SYN后,发送SYN+ACK,加入半连接队列
- 队列长度:
tcp_max_syn_backlog
全连接队列(Accept队列):
- 存储处于ESTABLISHED状态的连接
- 收到第三次握手ACK后,从半连接队列移到全连接队列
accept()从这里取出连接- 队列长度:
min(somaxconn, backlog)
项目中遇到的问题
一开始我的Reactor服务器在高并发时会丢连接,后来发现是全连接队列满了。
原因分析:
// 我的代码
listen(listenfd, 5); // backlog太小了!
解决方法:
listen(listenfd, 128); // 增大backlog
同时检查系统参数:
cat /proc/sys/net/core/somaxconn
# 输出:128
现在高并发下也不会丢连接了。
Q6: TCP四次挥手的完整过程
我的理解
TCP关闭连接需要四次挥手:
第一次挥手:
客户端:调用close(),发送FIN=1
状态:ESTABLISHED → FIN_WAIT_1
第二次挥手:
服务器:收到FIN,发送ACK
服务器:ESTABLISHED → CLOSE_WAIT
客户端:FIN_WAIT_1 → FIN_WAIT_2
第三次挥手:
服务器:发送完数据后,调用close(),发送FIN
状态:CLOSE_WAIT → LAST_ACK
第四次挥手:
客户端:收到FIN,发送ACK
客户端:FIN_WAIT_2 → TIME_WAIT(等待2MSL)→ CLOSED
服务器:LAST_ACK → CLOSED
我原来的疑问
Q:为什么关闭需要四次,而建立只需要三次?
因为TCP是全双工的:
- 建立连接时,双方都没有数据要发送,可以把SYN和ACK合并
- 关闭连接时,一方想关闭,但另一方可能还有数据要发送,不能立刻关闭
- 所以ACK和FIN通常不能合并,需要四次
Q7: TIME_WAIT状态的作用
我的理解
TIME_WAIT持续2MSL(Linux默认60秒),有两个作用:
作用1:确保最后的ACK能到达对方
如果最后的ACK丢失了:
客户端 服务器
|-------- ACK ------------->| X(丢失)
| |
TIME_WAIT LAST_ACK
| |(超时,重发FIN)
|<------- FIN --------------|
|-------- ACK ------------->|(重新发送)
如果客户端立刻关闭,就无法收到重发的FIN,服务器就无法正常关闭。
作用2:防止旧连接的数据干扰新连接
关闭后,网络中可能还有延迟的数据包。TIME_WAIT等待2MSL,确保所有旧数据包都消失了,才允许建立相同四元组的新连接。
项目中遇到的问题
我的Reactor服务器关闭后,立刻重启会报错:
bind error: Address already in use
原因: 服务器主动关闭连接,进入TIME_WAIT,端口被占用60秒。
解决方法: 设置SO_REUSEADDR
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
这样即使处于TIME_WAIT状态,也可以重新bind。
Q8: 大量CLOSE_WAIT说明什么问题?
我的理解
CLOSE_WAIT状态表示:对方已经关闭连接(发送了FIN),但我方还没调用close()。
如果出现大量CLOSE_WAIT,说明程序有BUG:收到对方的FIN后,忘记调用close()。
项目中踩过的坑
一开始我的代码:
// 错误代码
int n = recv(connfd, buf, sizeof(buf), 0);
if (n == 0) {
// 对方关闭,进入CLOSE_WAIT
// 但忘记调用close()
// 一直停留在CLOSE_WAIT状态
}
正确做法:
if (n == 0) {
// 检测到对方关闭,立刻关闭连接
epoll_ctl(epollfd, EPOLL_CTL_DEL, connfd, NULL);
close(connfd); // 必须调用!
LOG_INFO("Connection closed by peer, fd=%d", connfd);
}
检测方法:
netstat -an | grep CLOSE_WAIT | wc -l
现在我的服务器不会有CLOSE_WAIT堆积了。
Q9: close() vs shutdown()
我的理解
在学习过程中,我发现了两个关闭连接的函数:
close():
- 彻底关闭连接,发送FIN
- 引用计数-1,为0时才真正关闭
- 无法控制只关闭读或写
shutdown():
- 可以只关闭读端或写端
- 立即关闭,不管引用计数
SHUT_WR:关闭写端,发送FIN,但还能读SHUT_RD:关闭读端,不发送FINSHUT_RDWR:关闭读写,相当于close()
项目中的应用
有时候需要只关闭写端,但还能继续读取数据:
void graceful_shutdown(int connfd) {
// 1. 关闭写端,发送FIN
shutdown(connfd, SHUT_WR);
// 2. 继续读取对方可能发送的数据
char buf[1024];
while (true) {
int n = recv(connfd, buf, sizeof(buf), 0);
if (n == 0) {
// 对方也关闭了
break;
}
// 处理数据...
}
// 3. 彻底关闭
close(connfd);
}
Q10: 在项目中如何处理连接关闭
我的实现
在Reactor项目中,我需要处理两种情况:
情况1:检测对方关闭(被动关闭)
if (events[i].events & EPOLLIN) {
int n = recv(connfd, buf, sizeof(buf), 0);
if (n == 0) {
// 对方关闭连接(收到FIN)
epoll_ctl(epollfd, EPOLL_CTL_DEL, connfd, NULL);
close(connfd);
LOG_INFO("Connection closed by peer");
}
else if (n < 0) {
// 错误处理
if (errno != EAGAIN && errno != EWOULDBLOCK) {
epoll_ctl(epollfd, EPOLL_CTL_DEL, connfd, NULL);
close(connfd);
}
}
else {
// 处理数据
}
}
情况2:主动关闭连接
// HTTP短连接:处理完请求后主动关闭
void handle_http_request(int connfd) {
// 接收请求
// 处理请求
// 发送响应
// 主动关闭
close(connfd); // 发送FIN,进入FIN_WAIT_1
}
注意事项:
- 检测到
recv() == 0,立刻close()(避免CLOSE_WAIT) - 所有错误路径都要
close()(避免fd泄漏) - 主动关闭会进入TIME_WAIT(60秒)
- 设置
SO_REUSEADDR,允许服务器重启
学习总结
这段时间系统复习TCP连接管理,收获很大:
理论层面:
- 理解了三次握手和四次挥手的完整流程
- 搞清楚了各种状态的含义和转换条件
- 知道了TIME_WAIT、CLOSE_WAIT的作用
实践层面:
- 在项目中正确处理连接关闭
- 解决了
Address already in use的问题 - 避免了CLOSE_WAIT堆积
遇到的坑:
- 忘记调用
close()导致CLOSE_WAIT堆积 listen()的backlog太小导致丢连接- 没设置
SO_REUSEADDR导致无法快速重启
下一步学习:
- TCP可靠传输机制(序列号、ACK、重传)
- 滑动窗口和拥塞控制
- HTTP协议和缓存机制
参考资料
- 王道计算机网络视频课程
这篇文章记录了我对TCP连接管理的理解和项目实践经验。如果有错误或不准确的地方,欢迎指正!
更多推荐



所有评论(0)