TCP连接管理机制:三次握手四次挥手
TCP连接管理机制:三次握手四次挥手
目录
连接管理机制
在正常情况下, TCP 要经过三次握手建立连接, 四次挥手断开连接
三次握手
三次握手的目的
三次握手的主要目的是确保客户端和服务器之间的连接是可靠的,即双方都能确认自己与对方的发送与接收功能是正常的。
三次握手的步骤
第一次握手
- 客户端:客户端向服务器发送一个
SYN
(同步序列编号)报文段,并将SYN标志位置为1。同时,客户端随机生成一个初始序列号seq=x,将其放在TCP报文段的序列号字段中,然后发送给服务器。此时,客户端进入SYN_SENT(同步已发送)状态
。 - 服务器:服务器收到客户端的SYN报文段后,确认客户端请求建立连接。
第二次握手
- 服务器:服务器将自己的
SYN标志
位置为1,表示同意建立连接。同时,服务器将ACK(确认)标志位置为1,表示确认收到客户端的SYN报文段,并将确认号设置为客户端的序列号加1,即ack=x+1。此外,服务器也随机生成一个自己的初始序列号seq=y,并将其放在TCP报文段的序列号字段中,然后发送给客户端。此时,服务器进入SYN_RCVD(同步收到)状态
。 - 客户端:客户端收到服务器的SYN+ACK报文段后,确认服务器已经准备好建立连接。
第三次握手
- 客户端:客户端将ACK标志位置为1,表示确认收到服务器的SYN报文段。同时,客户端将确认号设置为服务器的序列号加1,即ack=y+1,并将自己的序列号设置为seq=x+1(因为客户端已经发送了一个SYN报文段,所以序列号需要加1)。然后,客户端将ACK报文段发送给服务器。此时,
客户端进入ESTABLISHED(已建立连接)状态
。 - 服务器:服务器收到客户端的ACK报文段后,确认客户端已经准备好进行数据传输。此时,
服务器也进入ESTABLISHED状态
,完成三次握手,连接建立。
注意:
-
只有监听(listen)了服务器才允许受理 SYN连接请求。
-
connect只是发起了三次握手
-
accept不参与三次握手,如果三次握手没有完成,accept会阻塞,accept只是三次握手结束后,才会返回,
-
三次握手不能保证100%能建立好连接,而是经历了三次握手,客户端与服务端都认为建立好连接了。
当客户端将最后一个ACK发出去的时候,并且这个ACK是没有应答的,客户端就认为连接已经建立好了。但是在客户端发送了ACK之后,可能会出现一些原因导致服务端没有建立好连接,这个时候,服务端就会给客户端发送RST报头
,让其关闭连接并且重新三次握手。
- 所谓的连接重置,就是让双方重新进行三次握手。
为什么建立连接是三次握手,而不是一次两次或则四次?
确保双方通信能力
- 第一次握手:客户端发送一个SYN报文给服务器,表示请求建立连接,并随机生成一个序列号seq。这一步主要是让服务器知道客户端有发送数据的能力,并且准备好接收服务器的响应。
- 第二次握手:服务器收到客户端的SYN报文后,回复一个SYN+ACK报文。其中,ACK表示确认收到客户端的SYN报文(ack=seq+1),SYN表示服务器也请求建立连接,并随机生成一个序列号seq2。这一步让客户端知道服务器有接收数据的能力,并且也准备好接收客户端的确认。
- 第三次握手:客户端收到服务器的SYN+ACK报文后,回复一个ACK报文,其中ACK表示确认收到服务器的SYN报文(ack=seq2+1)。这一步是让客户端和服务器都确认对方已经准备好进行通信,从而确保双方的数据发送和接收能力都正常。
防止失效连接请求
三次握手还可以有效防止已失效的连接请求报文段被误认为是新的连接请求。例如,如果客户端发送的SYN报文在网络中丢失,那么客户端会重新发送SYN报文。此时,如果服务器收到了两次SYN报文而只回复了一次SYN+ACK报文,那么客户端就无法确定哪个SYN报文被成功接收并回复,从而可能导致连接建立失败或混乱。而三次握手机制通过要求客户端对服务器的SYN+ACK报文进行确认,可以确保连接建立的准确性。
为什么不是一次或两次握手
一次握手:无法确认双方的通信能力,也无法防止失效的连接请求。(并且如果一个客服端一直给服务端发送连接请求,会造成SYN洪水攻击
。)
两次握手:虽然可以确认服务器的接收能力和客户端的发送能力,但无法确认客户端的接收能力。这可能导致服务器发送大量数据给客户端,而客户端由于某些原因(如网络故障)无法接收或确认这些数据,从而造成资源浪费和连接混乱。
为什么不是四次或更多次握手
四次(其实三次握手实际上也是四次握手,只是第中间一次是捎带应答)或更多次握手虽然可以进一步增加连接的可靠性,但也会增加连接的建立时间和复杂度。TCP协议设计时需要在可靠性和效率之间做出平衡。三次握手已经足够确保连接的可靠性和准确性,同时也不会引入过多的开销和复杂度。
四次挥手
TCP的四次挥手
是TCP连接释放的过程,用于终止客户端和服务器之间的连接
。这个过程确保了双方都能正确地关闭连接并释放资源,避免了数据丢失或资源泄露
。以下是TCP四次挥手的详细过程:
第一次挥手
发起者:客户端
动作:客户端发送一个FIN(Finish)
报文段给服务器,用来关闭客户端到服务器的数据传输。FIN报文段的标志位FIN被设置为1,表示客户端已经没有数据要发送给服务器了。
状态变化:客户端进入FIN_WAIT_1状态
,等待服务器的确认。
第二次挥手
接收者:服务器
动作:服务器收到客户端的FIN报文段后,发送一个ACK报文段给客户端,确认收到了客户端的关闭连接请求。ACK报文段 的确认号被设置为收到的FIN报文段的序号加1,表示服务器已经准备好接收客户端关闭连接的请求。
状态变化:服务器进入CLOSE_WAIT状态
,表示服务器已经同意关闭连接,但还需要完成一些清理工作(如发送剩余的数据)。同时,客户端收到服务器的ACK报文段后,进入FIN_WAIT_2状态
,继续等待服务器的关闭连接请求。
第三次挥手
发起者:服务器
动作:当服务器完成所有数据的发送后(包括可能的剩余数据),它会向客户端发送一个FIN报文段,用来关闭服务器到客户端的数据传输。FIN报文段的标志位FIN被设置为1,表示服务器也没有数据要发送给客户端了。
状态变化:服务器进入LAST_ACK状态
,等待客户端的确认。
第四次挥手
接收者:客户端
动作:客户端收到服务器的FIN报文段后,发送一个ACK报文段给服务器,确认收到了服务器的关闭连接请求。ACK报文段的确认号被设置为收到的FIN报文段的序号加1,表示客户端已经准备好关闭连接。
状态变化:客户端在发送完ACK报文段后,会进入TIME_WAIT状态
,等待一段时间
(通常是2MSL,即两倍的最大报文段生存时间)以确保服务器已经完全关闭连接。服务器在收到客户端的ACK报文段后,会关闭连接,进入CLOSED状态。客户端在等待时间结束后,如果没有收到任何来自服务器的报文段,也会关闭连接,进入CLOSED状态。
为什么需要四次挥手
-
TCP连接是全双工的,即数据可以在两个方向上同时传输。因此,每个方向上的连接都需要独立关闭。这就是为什么需要两次FIN报文(一次来自客户端,一次来自服务器)和两次ACK报文(一次确认客户端的FIN,一次确认服务器的FIN)的原因。
-
确保数据可靠传输:四次挥手过程中的每次报文交换都是对之前动作的确认。例如,客户端发送FIN报文后等待服务器的ACK报文确认;服务器发送FIN报文后也等待客户端的ACK报文确认。这种确认机制确保了数据的可靠传输直到连接完全关闭。
-
释放资源:四次挥手过程还负责适当地释放在建立连接时分配的资源,如端口号、内存缓冲区等。这是确保系统资源得到有效利用和管理的重要步骤。
-
关闭顺序的灵活性:任一端(客户端或服务器)都可以主动发起关闭连接的请求。这种双向的关闭过程确保双方都同意关闭,并且各自的数据传输都已经结束。
注意:
- 当客户端给服务端发送FIN报文请求断开连接时,如果服务端也正好有断开连接的意图,并且服务端此时已经没有数据需要发送给客户端了,那么服务端可能会采取“捎带应答”的方式,将ACK报文和自身的FIN报文合并发送,从而实现TCP连接的三次挥手过程。
第一次挥手后,客户端已经把文件描述符关闭了,那么后面服务端向客户端发送应答,客户端怎么知道的呢?
在TCP的四次挥手过程中,客户端和服务端之间的通信是通过TCP连接来维持的,这种连接状态在操作系统层面由TCP/IP协议栈管理。当客户端决定关闭连接时,它会发起第一次挥手(FIN包),但此时客户端可能并没有完全关闭其TCP连接(即TCP状态机可能还未到达CLOSED状态),特别是如果客户端的TCP栈还在等待服务端的确认(ACK)时。
系统中有系统调用可以实现把一个文件描述符 设置只将写/读/读写 关闭。
完整流程图
服务端状态转化
- [ CLOSED -> LISTEN ] 服务器端调用 listen 后进入 LISTEN 状态, 等待客户端连接;
- [ LISTEN -> SYN_RCVD ] 一旦监听到连接请求(同步报文段), 就将该连接放入
内核等待队列中
, 并向客户端发送 SYN 确认报文. - [ SYN_RCVD -> ESTABLISHED ] 服务端一旦收到客户端的确认报文, 就进入ESTABLISHED 状态, 可以进行读写数据了.
- [ ESTABLISHED -> CLOSE_WAIT ] 当客户端主动关闭连接(调用 close), 服务器会收到结束报文段, 服务器返回确认报文段并进入 CLOSE_WAIT;
- [ CLOSE_WAIT -> LAST_ACK ] 进入 CLOSE_WAIT 后说明服务器准备关闭连接(需要处理完之前的数据); 当服务器真正调用 close 关闭连接时, 会向客户端发送FIN, 此时服务器进入 LAST_ACK 状态, 等待最后一个 ACK 到来(这个 ACK 是客户端确认收到了 FIN)
- [ LAST_ACK -> CLOSED ] 服务器收到了对 FIN 的 ACK, 彻底关闭连接.
客户端状态转化:
- [ CLOSED -> SYN_SENT ] 客户端调用 connect, 发送同步报文段;
- [ SYN_SENT -> ESTABLISHED ] connect 调用成功, 则进入 ESTABLISHED 状态, 开始读写数据;
- [ ESTABLISHED -> FIN_WAIT_1 ] 客户端主动调用 close 时, 向服务器发送结束报文段, 同时进入 FIN_WAIT_1;
- [ FIN_WAIT_1 -> FIN_WAIT_2 ] 客户端收到服务器对结束报文段的确认, 则进入 FIN_WAIT_2, 开始等待服务器的结束报文段;
- [ FIN_WAIT_2 -> TIME_WAIT ] 客户端收到服务器发来的结束报文段, 进入TIME_WAIT, 并发出 LAST_ACK;
- [ TIME_WAIT -> CLOSED ] 客户端要等待一个 2MSL(Max Segment Life, 报文最大生存时间)的时间, 才会进入 CLOSED 状态.
理解 TIME_WAIT 状态
现在做一个测试,首先启动 server,然后启动 client,然后用 Ctrl-C 使 server 终止,这时马上再运行 server, 结果是:
这是因为,虽然 server 的应用程序终止了,但 TCP 协议层的连接并没有完全断开,因此不能再次监听同样的 server 端口. 我们用 netstat 命令查看一下:
-
TCP 协议规定,主动关闭连接的一方要处于 TIME_ WAIT 状态
,等待两个MSL(maximum segment lifetime)的时间后才能回到 CLOSED 状态。 -
我们使用 Ctrl-C 终止了 server, 所以 server 是主动关闭连接的一方, 在TIME_WAIT 期间仍然不能再次监听同样的 server 端口;
-
MSL 在 RFC1122 中规定为两分钟,但是各操作系统的实现不同, 在 Centos7 上默认配置的值是 60s;
-
可以通过 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看 msl 的值;
想一想, 为什么是 TIME_WAIT 的时间是 2MSL?
-
TCP协议中的TIME_WAIT状态持续时间为2MSL(两倍的最大段生存时间),这一设计是为了确保数据包的可靠传输以及连接的正确终止。具体原因如下:
-
确保所有数据包在网络中消失
在TCP连接关闭过程中,可能存在一些尚未到达目的地的旧数据包。这些数据包可能由于网络延迟、路由循环或其他原因而滞留。等待2MSL的时间可以确保这些数据包在网络中超时并被丢弃,从而避免它们干扰新的连接。 -
确保最后一个ACK报文段被正确接收
当连接的一方发送最后一个ACK报文段后,这个ACK可能会在网络中丢失。如果接收方没有收到这个ACK,它将重新发送FIN报文段。发送方必须等待足够长的时间以确保接收方收到了ACK并且不会重新尝试关闭连接。等待2MSL的时间可以确保发送方有足够的时间重新发送ACK报文段,以防接收方没有收到。 -
防止旧连接的数据包干扰新连接
如果旧的连接没有正确关闭,并且新的连接使用相同的四元组(源IP地址、源端口、目标IP地址、目标端口)建立,那么旧的数据包可能会干扰新连接的数据流。等待2MSL的时间可以确保所有旧的数据包已经消失,从而避免这种干扰。
解决 TIME_WAIT 状态引起的 bind 失败的方法
在 server 的 TCP 连接没有完全断开之前不允许重新监听, 某些情况下可能是不合理的
- 服务器需要处理非常大量的客户端的连接(每个连接的生存时间可能很短, 但是每秒都有很大数量的客户端来请求.
- 这个时候如果由服务器端主动关闭连接(比如某些客户端不活跃, 就需要被服务器端主动清理掉), 就会产生大量 TIME_WAIT 连接。
- 由于我们的请求量很大, 就可能导致 TIME_WAIT 的连接数很多, 每个连接都会占用一个通信五元组(源 ip, 源端口, 目的 ip, 目的端口, 协议). 其中服务器的 ip 和端口和协议是固定的. 如果新来的客户端连接的 ip 和端口号TIME_WAIT 占用的链接重复了, 就会出现问题.
使用 setsockopt()设置
socket 描述符的 选项 SO_REUSEADDR 为 1, 表示允许创建端口号相同但 IP 地址不同的多个 socket 描述符
理解 CLOSE_WAIT 状态
- 小结:
对于服务器上出现大量的 CLOSE_WAIT 状态, 原因就是服务器没有正确的关闭
socket, 导致四次挥手没有正确完成. 这是服务器上的一个 BUG,大概率是没有正确关闭自己的socket导致。 只需要加上对应的 close 即可解决问题.
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)