图解TCP三次握手与四次分手

引言

TCP三次握手和四次挥手不管是在开发还是面试中都是一个非常重要的知识点,它是我们优化web程序性能的基础。但是大部分教材都对这部分解释的比较抽象,本文我们就利用wireshark来抓包以真正体会整个流程的细节。

三次握手

根据下面这幅图我们来看一下TCP三次握手。p.s: 每个箭头代表一次握手。

tcp三次握手

第一次握手

client发送一个SYN(J)包给server,然后等待server的ACK回复,进入SYN-SENT状态。p.s: SYN为synchronize的缩写,ACK为acknowledgment的缩写。

第二次握手

server接收到SYN(seq=J)包后就返回一个ACK(J+1)包以及一个自己的SYN(K)包,然后等待client的ACK回复,server进入SYN-RECIVED状态。

第三次握手

client接收到server发回的ACK(J+1)包后,进入ESTABLISHED状态。然后根据server发来的SYN(K)包,返回给等待中的server一个ACK(K+1)包。等待中的server收到ACK回复,也把自己的状态设置为ESTABLISHED。到此TCP三次握手完成,client与server可以正常进行通信了。

为什么要进行三次握手

我们来看一下为什么需要进行三次握手,两次握手难道不行么?这里我们用一个生活中的具体例子来解释就很好理解了。我们可以将三次握手中的客户端和服务器之间的握手过程比喻成A和B通信的过程:

  • 在第一次通信过程中,A向B发送信息之后,B收到信息后可以确认自己的收信能力和A的发信能力没有问题。
  • 在第二次通信中,B向A发送信息之后,A可以确认自己的发信能力和B的收信能力没有问题,但是B不知道自己的发信能力到底如何,所以就需要第三次通信。
  • 在第三次通信中,A向B发送信息之后,B就可以确认自己的发信能力没有问题。

wireshark

上面分析还不够形象,很容易忘记,下面我们利用wireshark来证明一下上面的分析过程。从下面的的输出就可以很容易看出来,必须要经过前面的三次tcp请求才会有起一次http请求。

第一次请求客户端发送一个SYN包,序列号是0。

wireshark-tcp-01

第二次请求服务器会发送一个SYN和一个ACK包,序列号是0,ack号是1。

wireshark-tcp-02

第三次本地客户端请求会发送一个ACK包,序列号是1,ack号是1来回复服务器。

wireshark-tcp-03

四次挥手

以下面这张图为例,我们来分析一下TCP四次挥手的过程。

tcp四次挥手

第一次挥手

client发送一个FIN(M)包,此时client进入FIN-WAIT-1状态,这表明client已经没有数据要发送了。

第二次挥手

server收到了client发来的FIN(M)包后,向client发回一个ACK(M+1)包,此时server进入CLOSE-WAIT状态,client进入FIN-WAIT-2状态。

第三次挥手

server向client发送FIN(N)包,请求关闭连接,同时server进入LAST-ACK状态。

第四次挥手

client收到server发送的FIN(N)包,进入TIME-WAIT状态。向server发送ACK(N+1)包,server收到client的ACK(N+1)包以后,进入CLOSE状态;client等待一段时间还没有得到回复后判断server已正式关闭,进入CLOSE状态。

TCP滑窗

一般在提到TCP三次握手的时候,同样会涉及到TCP滑窗,下面我们补充一下什么是TCP滑窗。如果采用PAR的形式来传递的话,每一次发送方发送完包后必须得到接收方的确认回复,改进一下这个流程,发送端一次可以发送多个包,不必每次等到接收方的ack回复,同时接收端也要告诉发送端自己能接收多少。当然还需要保证顺序性,对于乱序的状况,我们可以允许等待一定时间的乱序,比如先缓存提前到达的数据,然后等待需要的数据,如果一定时间没有达到就drop掉。

TCP滑动窗口可以解决我们上面提到的概念,滑动窗口中的数据主要分为下面几类:

  1. Sent and Acknowledged: 这些数据表示已经发送成功并已被确认的数据。
  2. Sent But Not Yet Acknowledged: 这部分数据称为发送但还没有被确认,数据被发送出去,没有收到接收端的ACK,认为并没有完全发送,这个属于窗口内的数据。
  3. Not Sent, Recipient Ready to Receive: 这部分是尽快发送的数据,这部分数据已经被加载到缓存中,也就是窗口中了,等待发送,接受端已经告诉发送端自己能够完全接受这些包,所以发送方需要尽快发送这些包。
  4. Not Sent, Recipient Not Ready to Reccive: 这些数据属于未发送,因为这些数据已经超出了接收端所能接受的范围。

对于接收端也是有一个接收窗口,类似发送端,接收端的数据有三个分类(注意接收端并不要等待ACK):

  1. Received and ACK Not Send to Process: 这部分数据属于接收了数据但是还没有被上层的应用程序接收,也是被缓存在窗口中。
  2. Received Not ACK: 已经接收,但是还没有ACK。
  3. Not Received: 有空位但是还没有接收数据。

TCP重传机制

下面这部分内容参考自coolshell,我认为写的比较好,所以转过来分享一下,感兴趣的朋友可以阅读一下 :)

TCP要保证所有的数据包都可以到达,所以,必需要有重传机制。

注意,接收端给发送端的Ack确认只会确认最后一个连续的包,比如,发送端发了1,2,3,4,5一共五份数据,接收端收到了1,2,于是回ack 3,然后收到了4(注意此时3没收到),此时的TCP会怎么办?我们要知道,因为正如前面所说的,SeqNum和Ack是以字节数为单位,所以ack的时候,不能跳着确认,只能确认最大的连续收到的包,不然,发送端就以为之前的都收到了。

超时重传机制

一种是不回ack,死等3,当发送方发现收不到3的ack超时后,会重传3。一旦接收方收到3后,会ack 回 4——意味着3和4都收到了。

但是,这种方式会有比较严重的问题,那就是因为要死等3,所以会导致4和5即便已经收到了,而发送方也完全不知道发生了什么事,因为没有收到Ack,所以,发送方可能会悲观地认为也丢了,所以有可能也会导致4和5的重传。

对此有两种选择:

  1. 一种是仅重传timeout的包。也就是第3份数据。
  2. 另一种是重传timeout后所有的数据,也就是第3,4,5这三份数据。

这两种方式有好也有不好。第一种会节省带宽,但是慢,第二种会快一点,但是会浪费带宽,也可能会有无用功。但总体来说都不好。因为都在等timeout,timeout可能会很长。

快速重传机制

于是,TCP引入了一种叫Fast Retransmit 的算法,不以时间驱动,而以数据驱动重传。也就是说,如果,包没有连续到达,就ack最后那个可能被丢了的包,如果发送方连续收到3次相同的ack,就重传。Fast Retransmit的好处是不用等timeout了再重传。

比如:如果发送方发出了1,2,3,4,5份数据,第一份先到送了,于是就ack回2,结果2因为某些原因没收到,3到达了,于是还是ack回2,后面的4和5都到了,但是还是ack回2,因为2还是没有收到,于是发送端收到了三个ack=2的确认,知道了2还没有到,于是就马上重转2。然后,接收端收到了2,此时因为3,4,5都收到了,于是ack回6。

Fast Retransmit只解决了一个问题,就是timeout的问题,它依然面临一个艰难的选择,就是,是重传之前的一个还是重传所有的问题。对于上面的示例来说,是重传#2呢还是重传#2,#3,#4,#5呢?因为发送端并不清楚这连续的3个ack(2)是谁传回来的?也许发送端发了20份数据,是#6,#10,#20传来的呢。这样,发送端很有可能要重传从2到20的这堆数据(这就是某些TCP的实际的实现)。可见,这是一把双刃剑。

References

TCP-CONNECTION
TCP-TERMINATION

Contact

GitHub: https://github.com/ziwenxie
Blog: https://www.ziwenxie.site