每个时代,都不会亏待会学习的人。
在进入今天主题之前我先抛几个问题,这篇文章一共提出 23 个问题。
TCP 握手一定是三次?TCP 挥手一定是四次?
为什么要有快速重传,超时重传不够用?为什么要有 SACK,为什么要有 D-SACK?
都知道有滑动窗口,那由于接收方的太忙了滑动窗口降为了 0 怎么办?发送方就永远等着了?
Silly Window 又是什么?
为什么有滑动窗口流控还需要拥塞控制?
快速重传一定要依赖三次重复 ACK ?
这篇文章我想由浅到深地过一遍 TCP,不是生硬的搬出各个知识点,从问题入手,然后从发展、演进的角度来看 TCP。
起初我在学计算机网络的时候就有非常非常多的疑问,脑子里简直充满了十万个为什么,而网络又非常的复杂,发展了这么多年东西真的太多了,今天我就大致的浅显地说一说我对 TCP 这些要点的理解。
好了,废话不多说,开始上正菜。
TCP 是用来解决什么问题?
TCP 即 Transmission Control Protocol,可以看到是一个传输控制协议,重点就在这个控制。
控制什么?
控制可靠、按序地传输以及端与端之间的流量控制。够了么?还不够,它需要更加智能,因此还需要加个拥塞控制,需要为整体网络的情况考虑。
这就是出行你我他,安全靠大家。
为什么要 TCP,IP 层实现控制不行么?
我们知道网络是分层实现的,网络协议的设计就是为了通信,从链路层到 IP 层其实就已经可以完成通信了。
你看链路层不可或缺毕竟咱们电脑都是通过链路相互连接的,然后 IP 充当了地址的功能,所以通过 IP 咱们找到了对方就可以进行通信了。
那加个 TCP 层干啥?IP 层实现控制不就完事了嘛?
之所以要提取出一个 TCP 层来实现控制是因为 IP 层涉及到的设备更多,一条数据在网络上传输需要经过很多设备,而设备之间需要靠 IP 来寻址。
假设 IP 层实现了控制,那是不是涉及到的设备都需要关心很多事情?整体传输的效率是不是大打折扣了?
我举个例子,假如 A 要传输给 F 一个积木,但是无法直接传输到,需要经过 B、C、D、E 这几个中转站之手。 这里有两种情况:
- 假设 BCDE 都需要关心这个积木搭错了没,都拆开包裹仔细的看看,没问题了再装回去,最终到了 F 的手中。
- 假设 BCDE 都不关心积木的情况,来啥包裹只管转发就完事了,由最终的 F 自己来检查这个积木答错了没。
你觉得哪种效率高?明显是第二种,转发的设备不需要关心这些事,只管转发就完事!
所以把控制的逻辑独立出来成 TCP 层,让真正的接收端来处理,这样网络整体的传输效率就高了。
连接到底是什么?
我们已经知道了为什么需要独立出 TCP 这一层,并且这一层主要是用来干嘛的,接下来就来看看它到底是怎么干的。
我们都知道 TCP 是面向连接的,那这个连接到底是个什么东西?真的是拉了一条线让端与端之间连起来了?
所谓的连接其实只是双方都维护了一个状态,通过每一次通信来维护状态的变更,使得看起来好像有一条线关联了对方。
TCP 协议头
在具体深入之前我们需要先来看看一些 TCP 头的格式,这很基础也很重要。
我就不一一解释了,挑重点的说。
首先可以看到 TCP 包只有端口,没有 IP。
Seq 就是 Sequence Number 即序号,它是用来解决乱序问题的。
ACK 就是 Acknowledgement Numer 即确认号,它是用来解决丢包情况的,告诉发送方这个包我收到啦。
标志位就是 TCP flags 用来标记这个包是什么类型的,用来控制 TPC 的状态。
窗口就是滑动窗口,Sliding Window,用来流控。
三次握手
明确了协议头的要点之后,我们再来看三次握手。
三次握手真是个老生常谈的问题了,但是真的懂了么?不是浮在表面?能不能延伸出一些点别的?
我们先来看一下熟悉的流程。
首先为什么要握手,其实主要就是为了初始化Seq Numer,SYN 的全称是 Synchronize Sequence Numbers,这个序号是用来保证之后传输数据的顺序性。
你要说是为了测试保证双方发送接收功能都正常,我觉得也没毛病,不过我认为重点在于同步序号。
那为什么要三次,就拿我和你这两个角色来说,首先我告诉你我的初始化序号,你听到了和我说你收到了。
然后你告诉我你的初始序号,然后我对你说我收到了。
这好像四次了?如果真的按一来一回就是四次,但是中间一步可以合在一起,就是你和我说你知道了我的初始序号的时候同时将你的初始序号告诉我。
因此四次握手就可以减到三次了。
不过你没有想过这么一种情形,我和你同时开口,一起告诉对方各自的初始序号,然后分别回应收到了,这不就是四次握手了?
我来画个图,清晰一点。
看看是不是四次握手了? 不过具体还是得看实现,有些实现可能不允许这种情况出现,但是这不影响我们思考,因为握手的重点就是同步初始序列号,这种情况也完成了同步的目标。
初始序列号 ISN 的取值
不知道大家有没有想过 ISN 的值要设成什么?代码写死从零开始?
想象一下如果写死一个值,比如 0 ,那么假设已经建立好连接了,client 也发了很多包比如已经第 20 个包了,然后网络断了之后 client 重新,端口号还是之前那个,然后序列号又从 0 开始,此时服务端返回第 20 个包的ack,客户端是不是傻了?
所以 RFC793 中认为 ISN 要和一个假的时钟绑定在一起ISN 每四微秒加一,当超过 2 的 32 次方之后又从 0 开始,要四个半小时左右发生 ISN 回绕。
所以 ISN 变成一个递增值,真实的实现还需要加一些随机值在里面,防止被不法份子猜到 ISN。
SYN 超时了怎么处理?
也就是 client 发送 SYN 至 server 然后就挂了,此时 server 发送 SYN+ACK 就一直得不到回复,怎么办?
我脑海中一想到的就是重试,但是不能连续快速重试多次,你想一下,假设 client 掉线了,你总得给它点时间恢复吧,所以呢需要慢慢重试,阶梯性重试。
在 Linux 中就是默认重试 5 次,并且就是阶梯性的重试,间隔就是1s、2s、4s、8s、16s,再第五次发出之后还得等 32s 才能知道这次重试的结果,所以说总共等63s 才能断开连接。
SYN Flood 攻击
你看到没 SYN 超时需要耗费服务端 63s 的时间断开连接,也就说 63s 内服务端需要保持这个资源,所以不法分子就可以构造出大量的 client 向 server 发 SYN 但就是不回 server。