三次握手流程
服务端调用 listen() 之后,内核会为这个端口维护两个队列:
-
半连接队列 (SYN Queue):来存放那些只完成了第一次握手(收到了 SYN),正在等待客户端回复 ACK 的连接。
-
全连接队列 (Accept Queue): 用来存放那些已经彻底完成了三次握手的连接。这些连接处于 ESTABLISHED 状态,随时可以开始传输数据
三次握手的这个流程如下:
- 客户端发送 SYN 报文: 服务端内核收到 SYN,回复 SYN-ACK ,服务器会进入
SYN_RCVD状态,内核把这个新连接塞进半连接队列 - 客户端发送 ACK:服务端内核收到了客户端的最终 ACK,三次握手在内核层面 宣告完成。 内核会做一件重要的事情:把这个连接从"半连接队列"里揪出来,丢进全连接队列。
accept()终于被唤醒并返回:accept() 的唯一工作机制,就是死死盯着"全连接队列"。 一旦内核把完成了握手的连接放进了全连接队列,accept() 就会立刻被唤醒。它从全连接队列里取走这个连接,为你生成一个全新的 Socket 文件描述符(FD),然后成功返回。之后,你的代码就可以用这个新的 Socket 读写数据了。
如果服务端是非阻塞的情况下(非阻塞 Socket (Non-blocking)),会有个例外
如果你把服务端的 listening socket 设置成了非阻塞模式,那么当全连接队列为空时,accept() 不会傻等客户端的 ACK,而是会立刻返回一个 EAGAIN,告诉你"现在没连接,过会儿再来查"。(这也是 Netty、Nginx 等基于 Epoll/Reactor 模型的高性能框架的基石)。
三次握手结论:
- 服务端的内核需要维护两个队列:因为服务端是被动监听的一方面对四面八方同时涌入的巨量客户端请求,内核必须有地方给它们"排队"
- 我们在用户空间编写的代码(无论是用 Java Netty、Go、还是 Python)是无法直接触碰这两个队列的,程序只能通过调用系统调用(如 accept())去间接消费全连接队列里的成品。
四次分手过程中 TIME_WAIT 与 CLOSE_WAIT状态
结论先行:"谁主动发起断开,谁就会进入 TIME_WAIT;谁被动接受断开,谁就可能卡在 CLOSE_WAIT。"
首先客户端和服务端是建立好的连接,双端的状态此时都是ESTABLISHED,此时我们做个假设,客户端(Client)主动发起断开连接请求,服务端(Server)被动接受:
-
- 第一次挥手
动作: Client 发送一个 FIN 报文,表示"我没有数据要发了,准备关连接"。
状态: Client 进入 FIN_WAIT_1 状态。
-
- 第二次挥手(也就是针对主动关闭方
FIN包的ACK回复)
- 第二次挥手(也就是针对主动关闭方
动作: Server 收到 FIN,内核回应一个 ACK,表示"我知道了,但我这边可能还有数据没发完,你等我一下"。
状态: * Server 进入 CLOSE_WAIT 状态。 处于"半关闭"状态。上面的ACK是内核自动回复的,但此时 Server 的应用程序还没有调用 close() 关闭 Socket。Client 收到 ACK 后,进入 FIN_WAIT_2 状态。
-
- 第三次挥手
动作: Server 把剩余的数据发完了,应用程序调用 close(),向 Client 发送 FIN 报文,表示"我也发完了,可以正式关了"。
状态: Server 进入 LAST_ACK 状态。
-
- 第四次挥手
动作: Client 收到 Server 的 FIN,回应最后一个 ACK。
状态:Client 进入 TIME_WAIT 状态。
Server 收到 ACK 后,彻底关闭连接(CLOSED)。
Client 在 TIME_WAIT 持续 2MSL(最大报文生存时间)的时间后,确认没事了,才彻底进入 CLOSED 状态。
四次挥手结论:
如果发 SYN :代表你想建立一个新连接。
如果发 FIN:代表你想断开连接
通信双方如果有一方想要断开连接,就会发送FIN包,然后进入 FIN_WAIT_1 状态,内核收到 FIN,立刻回一个 ACK(第二次挥手),告诉客户端:"收到,我知道你没数据发了。"
客户端收到 ACK 后,进入 FIN_WAIT_2 状态,静静等待服务端最后发来的 FIN。此时,客户端的发送通道关闭 ,接收通道保持开启 ,此时的服务端在发送ACK 之后处于ClOSE_WAIT 状态。 所以只有被动关闭方才有ClOSE_WAIT状态,意思是内核在等待自家应用程序进行关闭。