引言
TCP 三次握手和四次挥手不管是在开发还是面试中都是一个非常重要的知识点, 它是我们优化 web 程序性能的基础但是大部分教材都对这部分解释的比较抽象, 本文我们就利用 wireshark 来抓包以真正体会整个流程的细节
三次握手
根据下面这幅图我们来看一下 TCP 三次握手 p.s: 每个箭头代表一次握手
第一次握手
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
第二次请求服务器会发送一个 SYN 和一个 ACK 包, 序列号是 0,ack 号是 1
第三次本地客户端请求会发送一个 ACK 包, 序列号是 1,ack 号是 1 来回复服务器
四次挥手
以下面这张图为例, 我们来分析一下 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 滑动窗口可以解决我们上面提到的概念, 滑动窗口中的数据主要分为下面几类:
Sent and Acknowledged: 这些数据表示已经发送成功并已被确认的数据
Sent But Not Yet Acknowledged: 这部分数据称为发送但还没有被确认, 数据被发送出去, 没有收到接收端的 ACK, 认为并没有完全发送, 这个属于窗口内的数据
Not Sent, Recipient Ready to Receive: 这部分是尽快发送的数据, 这部分数据已经被加载到缓存中, 也就是窗口中了, 等待发送, 接受端已经告诉发送端自己能够完全接受这些包, 所以发送方需要尽快发送这些包
Not Sent, Recipient Not Ready to Reccive: 这些数据属于未发送, 因为这些数据已经超出了接收端所能接受的范围
对于接收端也是有一个接收窗口, 类似发送端, 接收端的数据有三个分类(注意接收端并不要等待 ACK):
Received and ACK Not Send to Process: 这部分数据属于接收了数据但是还没有被上层的应用程序接收, 也是被缓存在窗口中
Received Not ACK: 已经接收, 但是还没有 ACK
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 的重传
对此有两种选择:
一种是仅重传 timeout 的包也就是第 3 份数据
另一种是重传 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: github.com/ziwenxie
- Blog: www.ziwenxie.site
来源: https://juejin.im/post/5a7835a46fb9a063606eb801