💐个人主页:初晴~
📚相关专栏:计算机网络那些事
通过上篇文章,我们可以得知TCP通过 “确认应答” 和 “超时重传”机制相辅相成,共同构建了 TCP的“可靠传输机制”。而为了保障建立通信和断开通信的可靠性,TCP还提供了“三次握手,四次挥手”的机制,来作为连接管理。
一、什么是“三次握手,四次挥手”
这里的“握手”与“挥手”(handshake),是一种形象的比喻。生活中握手就是一种打招呼的方式,挥手则是象征着“告别”,并没有什么实际的含义。网络中的握手与挥手,指发送不携带业务数据(没有载荷,只有报头)的数据包,起到类似打招呼或告别的效果。
事实上,握手指的就是建立连接的过程,挥手指的是断开连接的过程。
这里的连接并不是指拿个电线拴起来
- 通信双方保存好对方的信息,就是建立连接的过程
- 通信双方删除掉对方的信息,就是断开连接的过程
需要注意的是,在连接建立过程中,客户端主动发起连接请求;而在连接终止过程中,任何一方都可以首先提出关闭请求。此外,TCP连接的建立和终止都需要经过双方的确认,这也是“三次握手,四次挥手”的核心机制,让我们接着往下看
二、三次握手
1、三次握手的流程
-
第一次握手:客户端向服务器发送一个SYN(同步序列号)段(即报头中的标志位“syn”值为1),表示客户端请求建立连接。这个段落包含一个初始序列号x,用于后续的数据确认。
-
第二次握手:服务器接收到SYN段后,向客户端发送一个SYN+ACK(确认)段作为响应。这表示服务器同意建立连接,并且告知客户端它的序列号y,同时也确认了客户端的序列号。
-
第三次握手:客户端接收到SYN+ACK后,向服务器发送一个ACK段,确认服务器的序列号。至此,TCP连接建立完成,客户端和服务器可以开始数据交换。
完成以上步骤后,连接就建立完毕了。原本应该是四次交互,但是中间两次的执行时机差不多,于是系统内核就将这两次发送合并成了一次,目的是减少通信次数,提高效率。
这里的syn全称为synchronized,意为“同步的,锁”。在之前多线程的学习中是与加锁操作有关的关键字。而在此处,应该更偏向同步的意思,可以理解为客户端希望服务器与其统一步调,来完成后续的通信传输。
2、为什么是三次?
建立连接是一个“双向的操作”:
- A需要给B说,我想和你建立连接(A想保存B的信息)
- B也需要给A说,我也想和你建立连接(B想保存A的信息)
每次请求,对方都需要做一条反馈,告诉发送方是否可以连接。因此,请求加上反馈至少就需要四次通信,而TCP将第2、3次合并到了一起,使得总共就变成了“三次握手”。
举个例子:
现在有两名玩家 A 和 B 要互相连麦:
(1)玩家 A :“听得见吗?”
如果玩家 B能听到这句话,说明 A 的麦克风与 B 的耳机都能够正常使用,即 A 到 B 的通信正常。但 B 还不能确定 B 到A的通信是否正常,此时需要向 A 做出反馈
(2)玩家 B:“听得见,你能听见吗?”
如果 A 玩家能成功听见这句话,前半句“听得见”,回答了我之前的信息,A 就能知道 A 到 B 的通信正常;同时自己能听见这句话,也说明 B 的麦克风与 A 的耳机能够正常使用,即 B 到 A 的通信正常。此时 A 就能知道双方的麦克风与耳机都是正常的,但 B 还不知道,于是就需要再回答后半句 B 的问题,做出反馈:
(3)玩家 A:“听得见,我们可以开始聊天了”
当 B 听到这句话时,也就能确定双方的通信没有问题,接着就可以正常进行聊天了。
这里的麦克风就代表着发送能力,耳机代表接收能力。因此“三次握手”的目的可以看做是确认通信双方各自的发送能力和接收能力是否正常。
- 两次握手是不可行的,因为只有两次是无法完成通信双方各自的发送能力和接收能力的验证的
- 四次握手是可行的。理论上也至少要进行四次通信,只不过TCP将其优化成了三次。不过针对其它协议,握手过程就不一定是三次。
3、三次握手的意义
(1)如上文所述,验证双方的发送能力与接收能力
(2)投石问路,初步验证通信链路是否通畅
比如我们日常乘坐的地铁,其实在第一班车发车之前,会先空跑一趟。
目的是为了验证线路是否通畅,可以提前排查一些潜在的故障。如果在载客的情况下才发现线路故障的话,处理起来就会比较麻烦了,而相比之下空车做调整可就灵活多了。这里的空车就起到了“投石问路”的作用。
三次握手也是如此。相当于在正式通信前,先发了几个空载的报文“探路”,来“验证链路是否通畅”,相比于直接发送携带数据的报文来说,这种方式的代价就会比较小了。
(3)让通信双方在通信前,协商一些关键的参数
TCP通信时,起始数据的序号,就是通过三次握手来协商确定的。使得每次建立连接,TCP的起始序号都是不同的。其主要目的是避免“前朝的剑,斩本朝的官”:
该过程就是,A 和 B 正常连接的时候A发送了一个数据包,不过这个数据包传输的速度有些慢,在它传输的过程中,A 和 B 断开了连接,又重新建立起了连接。但此时虽然连接的还是 A 和 B 两个主机,但是可能已经是不同的应用程序了,相当于是“改朝换代”
而这个数据包就像是“旧朝遗老”,等它终于传输成功时,俨然已不是当年摸样。如果还去正常读取这个数据包,就会引起一系列问题。此时就需斩钉截铁,直接丢弃这个数据包。
为了达到这点,对于 B 来说,就需要区分,当前收到的数据时“本朝”还是“前朝”的。
于是,在每次连接时,都会尽量协商不同的起始序号,如果发现收到的数据,与起始序号差距非常大的话,就认为该数据是“前朝”的,会直接将其丢弃。
三、四次挥手
1、四次挥手的流程
-
第一次挥手:某一端(通常是从没有数据要发送的一端开始)决定关闭连接,并向另一端发送一个FIN(终止)段(标志位FIN值为1),表示它已经完成了数据发送任务。
-
第二次挥手:接收FIN段的一端(服务器)会发送一个ACK确认段,确认接收到FIN段。此时,连接还没有完全关闭,因为这一端可能还有数据需要发送。
-
第三次挥手:当接收FIN段的一端也完成数据发送后,它会发送自己的FIN段给最初发送FIN的一端。
-
第四次挥手:最初发送FIN段的一端接收到对方的FIN段后,会发送ACK确认段,表明所有数据已接收完毕,连接可以关闭。此时,TCP连接正式关闭。
这里的标志位“FIN”意为“Finnish”,可以理解为通信连接的终止。即双方各自把对端的信息删掉
- 为什么流程看起来差不多,握手能优化成三次,而挥手则必须要四次?
- 对于三次握手来说,中间的两次,ACK+SYN都是在内核中,由操作系统负责进行的。并且执行时机都是在收到发送方的 SYN 之后,同一时机,于是就直接将它们合并了
- 对于四次挥手来说,ACK 是内核控制的,但是 FIN 则是通过应用程序调用 close/进程退出,来触发的,在实际执行中,这两个操作的执行时机大概率是不同的,由于执行到close前可能还要完成一系列操作,所以一般 FIN 执行都会晚于 ACK,而且很可能相差会很大。因此大部分情况下,都不会把这两个操作合并。
比如在代码中调用socket.close() 时,系统内部就会发送 FIN
四、TCP的状态转换
TCP连接状态反映了TCP连接的不同阶段。这些状态可以帮助理解连接的生命周期,让我们更好地理解“三次握手,四次挥手”的流程:
大多数状态上图中都有体现,博主接下来就主要介绍几个比较特殊的状态吧
LISTEN
- 服务器监听客户端的连接请求。服务器把端口绑定号,就相当于进入 LISTEN 状态了。此时服务器已经初始化完毕,准备好随时迎接客户端
ESTABLISHED
- TCP连接建立完成,可以进行业务数据的通信了。客户端和服务端都会进入的状态
CLOSE_WAIT
- 被动断开连接的一方,会进入这个状态。先接收到“FIN”的一方,会等待代码执行“close()”方法
如果发现,服务器端存在大量的 CLOSE_WAIT 状态的TCP连接,就说明服务器代码可能出现bug了。这时需要排查服务端代码,看看是否写了 close() ,以及是否能够及时执行。
TIME_WAIT
- 主动断开连接的一方,会进入这个状态。一旦接收到对端的FIN段并回复了ACK,它就会进入TIME_WAIT状态。在此状态下,TCP连接保持打开一段时间。(通常是2MSL,即两倍的最大段生存时间),以确保对方的ACK段能够到达,从而确保连接正确关闭。
这时有人可能会问了,为啥不直接释放,还有等一段时间呢?
主要是为了防止最后的ACK丢包。如果最后一个ACK丢包了,此时 B 就会重传一次FIN。如果A立刻断开了连接,也就无法收到重传的FIN,更无法重新发送一次 ACK。这样 B 端就会进入死循环,迟迟无法真正断开连接
于是就让 A 先等待一段时间,如果这段时间内都没有收到 B 重发的 FIN,就可以认为之前发的 ACK成功送达了,这时才真正释放掉连接
注意:
这里的等待时间一般设置为 2MSL,其中MSL为数据报在网络传输中消耗的最大时间,不同的系统会有所差别,比如 Linux 默认为60s。这样就能确保ACK一定能够送达了
那么本篇文章就到此为止了,如果觉得这篇文章对你有帮助的话,可以点一下关注和点赞来支持作者哦。如果有什么讲的不对的地方欢迎在评论区指出,希望能够和你们一起进步✊