目录
0.前言
由于TCP协议内容比较多,笔者我便将其分为上下两篇来进行讲解。
上篇中的内容包括但不局限于:确认应答机制、超时重传机制、连接管理机制。
感兴趣的读者可以阅读一下:传输层协议 —— TCP协议(上篇)
1.流量控制
什么是流量控制?
通信双方使用TCP协议进行通信时,接收方处理数据的能力是有限的,如果发送方发送的太快,就会导致接收方的缓冲区很快就会被写满,如果这个时候发送方还一直发送数据,但是对方的接收缓冲区已经被写满了,那么接收方就会直接将报文丢弃掉。而对于发送方来讲,发送数据之后,并没有收到应答,就会启用超时重传机制,额…… 发送方就这样一错再错下去吗?
显然这是不合理的,对于接收方来讲,人家千里迢迢发送过来的报文,消耗了这么多网络资源,你收到后就直接丢弃了,那人家还不如不发呢。对于发送方来讲,当接收方缓冲区写满之后,再发送数据,接收方直接丢弃,那你还不如不发呢?毕竟这种吃力不讨好的事情没必要干!
反之,如果接收方处理数据的能力比较强,发送方发送的太慢,接收方就会有大量的时间处于等待状态,这样一来,接收方就太闲了,这也是不合理的,因为我们需要保证通信的效率。
针对这种问题,TCP协议引入流量控制来解决该问题。
什么是流量控制呢?
根据接收方接收数据的能力, 来决定发送端的发送数据的速度。这个机制就叫做流量控制(Flow Control);
如何做到流量控制?
首先,我们需要明确,流量控制是根据接收方接收数据的能力来决定发送方发送数据的速度。那么流量控制机制就要求发送方得知道接收方接收数据的能力,要达到这一点,只能是接收方告知发送方自己的接收能力,那么问题来了,接收方如何告知发送方自己的接收能力呢?
这个时候,就需要使用到TCP协议报头中的16位窗口大小字段了。16位窗口大小会表明接收方自己的接收缓冲区中剩余空间的大小。接收方通过确认应答机制以及捎带应答策略,不就可以向发送方表明自己的接收能力了吗?发送方收到报文之后,提取出16位窗口大小,不就知道接收方的接收能力了吗?
于是,发送方就可以根据响应报文中该字段的值动态的调整待发送数据的大小了。
窗口探测和窗口更新
如果接收端缓冲区被写满了, 就会将窗口大小设置为 0。这时发送方不再发送数据,但是需要定期发送一个窗口探测报文,使接收方把窗口大小告诉发送方。
同时,如果接收方的接收缓冲区经过上层的处理,已经有剩余空间了,就需要主动发送窗口更新通知,告诉发送方自己的接收能力。这种双向奔赴的机制能够最大程度保证双方通信的效率。
PSH标志位与流量控制
虽然流量控制可以根据接收方的接收能力动态的调整发送方发送数据的大小,但如果接收方的接收缓冲区被写满之后,接收方上层又一直不处理接收缓冲区中的内容,那么发送方不就不能向接收方发送数据了吗?
这个时候,发送方就会发送一个PSH标志位被置1的报文,用来催促接收方赶紧将接收缓冲区中的数据读走。
当然PSH标志位不仅仅局限于这种场景中使用,任何需要接收方赶紧处理接收缓冲区的场景都可以使用。
补充:URG标志位
TCP协议中,还有一个16位的紧急指针。这个紧急指针指向TCP报文中的紧急数据,该数据通常是1字节。如果URG标志位被置位1,说明紧急指针有效,这个时候就需要处理紧急数据了。
紧急数据人如其名,是需要赶紧被处理的数据,也就是说紧急数据被处理的优先级是高于其他数据的。
应用场景:比如说一下停止下载,停止上传的场景,都可以使用紧急指针来处理。
一个问题
16 位数字能表示的最大数字是 65535, 那么 TCP 窗口最大就是 65535 字节么?
并不是。实际上,TCP 首部 40 字节选项中还包含了一个窗口扩大因子 M,实际窗口大小是 窗口字段的值左移 M 位;
2.滑动窗口
为什么要有滑动窗口?
我们知道使用TCP协议进行通信的时候,是基于一发一收的形式,只有收到了对方的应答,发送方才能确保自己曾经发送的报文被对方接收了,只有确认自己发送的报文被对方接收了,发送方才会发送下一个报文;这种通信模式本没有错,但是效率太低了,如果数据往返时间比较长的时候,该缺点就更加明显了。
其实在计算机当中有很多这种串行导致低效的问题,比如CPU对于进程的调度…… 最后都可以通过改用并行策略来解决,网络通信也是如此。
如果发送方一次性发送不止一个报文,而是可以同时发送多个报文,接收方响应的时候也是对多个报文进行响应,这样一来,通信效率不就提高了吗?
分析上图可知,想要一次发送多个报文,注定了发送靠后的报文的时候,并没有收到对前面发送报文的应答。也就是说,要想能够一次发送多个报文,发送方必须要具有没收到对历史报文应答而可以直接发送的数据。
这些不需要应答就可以直接发送的数据,就是滑动窗口中的数据,所以滑动窗口是用来提高TCP协议通信效率的一种机制
滑动窗口在哪?
那么滑动窗口在哪呢?我们都知道,操作系统会为TCP协议分配两个缓冲区,一个发送缓冲区,一个接收缓冲区。使用TCP协议进行通信的时候,待发送的数据肯定是存放在发送缓冲区中的,所以不需要应答就可以直接发送的数据同样是存放在发送缓冲区中的。
发送缓冲区中不需要应答就可以直接发送的数据的大小就一同构成了滑动窗口的大小,所以滑动窗口是在发送缓冲区中的。
滑动窗口到底是什么?
我们已经知道了滑动窗口其实就是发送缓冲区中的一段区域, 所以我们要表示滑动窗口的话,只需要表示发送缓冲区中不需要应答就可以直接发送的数据所占的区域即可。
在代码实现上不就是一个start变量标记这段区间的开始位置,一个end变量标记这段区间的结束位置吗?
更何况,TCP是面向字节流的协议,如果我们把TCP的缓冲区当做大数组来看,缓冲区中的每个字节不就天然的具有下标了吗?所以滑动窗口其实就是用两个下标维护的一段发送缓冲区。
滑动窗口的工作原理
滑动窗口中数据的更新
我们已经认识了滑动窗口,滑动窗口其实就是发送缓冲区中的一段区域,处于滑动窗口中的内容不需要应答可以直接发送,那滑动窗口左边的数据代表什么呢?滑动窗口右边的的数据又代表什么呢?
滑动窗口可以把发送缓冲区划分为三段:
滑动窗口左侧的数据表示已发送的数据。
滑动窗口中的数据表示可发送的数据。
滑动窗口右侧的数据表示待发送的数据。
如果发送方收到了对方的应答报文,就会根据应答报文中的确认序号调整滑动窗口的起始位置,并且根据应答报文中的16位窗口大小计算出滑动窗口的结束位置(这个地方有点问题,先这样理解,后面纠正)。这样,发送方就更新出下一次要发送的数据了。在更新可发送的数据的过程中,滑动窗口一直是向右边移动的。
如果发送方没有收到对方的应答报文,历史上发送的数据还在滑动窗口中,也就是说,滑动窗口会保存未收到应答的数据,方便进行超时重传。
你可能会有一个疑问,滑动窗口一直向右移动,走到最后无路可走了怎么办?
我们可以这样理解,如果发送缓冲区在逻辑上是一个环形的,就可以通过更新待发送的数据覆盖已发送的数据,不就可以一直在发送缓冲区中移动了吗?
滑动窗口对丢包的处理
滑动窗口中数据丢包问题可以划分为三类:左侧丢包,中间丢包,右侧丢包。
在这里我们必须非常明确一个概念,确认序号的意义是什么?
确认序号表明了接收方告诉发送方下一次发送报文的起始位置,也表明了该确认序号之前的报文接收方都收到了。
对于左侧丢包
我们假设发送的一个报文的长度是1000字节。如果是最左侧报文丢失,也就是序号为 1000 ~ 2000 的报文丢失,接收方给发送方的应答报文中,填充的确认序号就是1000。并且滑动窗口中的报文是可以同时发送的,也就意味着发送方未来会收到多个应答报文,如果应答报文中出现三个相同的确认序号时,发送方就会意识到,可能是该确认序号后面的1000字节的报文丢失了,此时就会触发快重传机制,对确认序号后面的1000字节的报文进行重发。也就能解决左侧丢包问题了。
快重传 VS 超时重传
什么是快重传? 当发送方收到三个同样的确认应答时,立即进行重传的策略。
什么是超时重传? 当发送方在规定时间内没有收到对方的应答,默认认为历史上发送的报文丢失,进行重传的策略。
为什么有超时重传还要快重传?
快重传不需要进行等待,直接就可以进行重传,与超时重传相比,能够提高通信的效率;
快重传能否替代超时重传?
不可以!因为快重传是有条件限制的,需要收到三个相同的确认应答才会触发。但如果发送的报文不足三个,就怎么都不会收到三个确认应答,也就不会触发快重传。
对于中间丢包
如果是中间的报文丢失,我们假设是 3000 ~ 4000 的报文丢失。此时,接收方给发送方的确认序号就是3000,表明序列号为3000之前的报文我都收到了。此时发送方就会根据确认序号调整滑动窗口的起始位置,根据16位窗口大小计算出滑动窗口的结束位置。那么中间丢包问题不就转换成左侧丢包问题了吗?
此时,同样可以通过快重传机制进行补发,从而解决丢包问题。
对于右侧丢包
还是同样的道理,右侧丢包问题也会转化为左侧丢包问题,但是需要注意的是,如果没有收到三个相同的确认应答,此时触发的就是超时重传机制而不是快重传机制。
滑动窗口和流量控制的关系
不考虑网络状况。流量控制是接收方通过应答报文中的16位窗口大小向发送方表明自己的接收能力,也就决定了发送方发送数据的上限。但是,流量控制并没有提供具体的调整发送方发送数据大小的策略,这是由滑动窗口来完成的。
也就是说,滑动窗口是流量控制的具体实现,流量控制是滑动窗口的指导思想。
3.拥塞控制
什么是拥塞控制?
前面我们所谈论的所有内容都没有考虑网络状况,现在,我们需要考虑网络状况了。
你有没有思考过这样一个问题:不同主机进行网络通信的时候,一开始应该发送多大的数据呢?
如果网络状态比较差的时候,网络已经比较拥堵了,贸然发送大量的数据,只会加重网络的拥堵。如果网络比较好的时候,发送的数据量较少,就没有充分利用网络资源。
所以最好的方式是先摸清楚网络的状态,再决定按照多大的速率传输数据。于是,TCP引入慢启动机制 来摸清楚网络的状态以及判断网络是否拥塞。
当网络状态比较拥塞了,发送方发出去的报文很有可能不会被对方收到,这时候可以进行重传吗?这是不可以的,因为,当网络状况比较拥塞了,再向网络中发送数据,只会让网络更加拥塞。
所以拥塞控制就是,当主机判断网络比较拥塞时,就动态的调整发送数据的大小的一种机制。是一种为了防止因网络中的流量超过其容量而导致网络性能下降的一种技术。
所以拥塞控制 控制的是在不导致网络拥塞的前提下,发送方能向网络当中发送的最大数据量。
拥塞窗口
这里我们引入一个拥塞窗口的概念。使用TCP协议进行通信时,发送方发送的数据必须从发送缓冲区中发送,这是TCP的特点。
我们类比于滑动窗口,其实拥塞窗口也是发送缓冲区中的一段区域,这段区域中的数据是受网络控制的。发送方在不导致网络拥塞的前提下,能向网络中发送的最大数据量就构成了拥塞窗口。所以拥塞控制控制的是拥塞窗口中的数据。
所以,拥塞窗口的作用是什么呢?
拥塞窗口用于控制发送方在任何时候可以发送的最大字节数。它的大小取决于网络的拥塞程度,并动态变化。
当我们有了拥塞窗口的概念之后,我们就需要明白,滑动窗口的大小不应该直接等于接收方的接收缓冲区中剩余空间的大小,而是:滑动窗口大小 = min(16为窗口大小,拥塞窗口)
慢启动机制
我们已经了解了拥塞控制和拥塞窗口,那拥塞控制到底是如何工作的呢?这里就需要详谈一下慢启动机制了。
当TCP开始启动的时候,拥塞窗口中的大小为1,也就是只能向网络中发送一个报文,紧接着以指数形式增长,我们知道指数增长是爆炸式的,当超过一个临界点之后,会增长的非常快,但是不能一直这么爆炸式的增长下去,当增长到一个阈值的时候的时候,会以线性增长的方式平缓的增长,直到网络拥塞。
此时,慢启动阈值就会变成原来的一半,同时,拥塞窗口大小被置为1,然后继续进行慢启动,不断地重复慢启动过程,就能不断地检测网络状态,从而动态的调整发送数据的大小。
可以看出,拥塞控制是需要依靠慢启动机制的。
4.延时应答
在网络通信的过程中,如果接收数据的主机立即返回应答报文,此时返回的剩余空间的大小可能比较小,但如果等一会再向发送方应答,在这段时间内,接收方的上层可能从接受缓冲区中拿走了部分数据,此时再向发送方应答,就可以返回一个更大的空间。这就是延时应答机制。
延时应答机制可以提高网络带宽,但也不是对所有的报文都要进行延时应答,延时应答是有数量和时间限制的,也就是说,每隔n个报文就进行一次延时应答,超过一定时间就进行一次延时应答。 具体实现依操作系统不同也有差异,一般 N 取 2,超时时间取 200ms。
一定要记得,窗口越大,网络吞吐量就越大,传输效率就越高,我们的目标是在保证网络不拥塞的情况下尽量提高传输效率。
5.捎带应答
在网络通信的过程中,通信双方通常是一发一收的,也就是说收到一个报文之后就要向对方进行响应,表明你发送的数据我收到了,并且还要向对方发送自己的数据。这个时候如果应答和要发送的数据分开发送,就需要发送两个报文,那可不可以合并成一个呢?
这是可以的,因为应答其实就是将TCP协议报头中的ACK标志位置为1,要发送的数据是跟在协议报头后面的,双方并不影响,此时就可以合二为一,进行捎带应答。
6.粘包问题
什么是粘包?
• 首先要明确,粘包问题中的 "包",是指的应用层的数据包。
• 在 TCP 的协议头中,没有如同 UDP 一样的 "报文长度" 这样的字段,但是有一个序号这样的字段。
• 站在传输层的角度,TCP 是一个一个报文过来的,按照序号排好序放在缓冲区中。
• 站在应用层的角度,看到的只是一串连续的字节数据。
• 那么应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分结束是一个完整的应用层数据包。
如何解决粘包问题?
• 对于定长的包,保证每次都按固定大小读取即可。
• 对于变长的包,可以在包头的位置,约定一个包总长度的字段,从而就知道了包的结束位置。
• 对于变长的包,还可以在包和包之间使用明确的分隔符(应用层协议,是程序猿自己来定的, 只要保证分隔符不和正文冲突即可)。
UDP是否存在粘包问题呢?
• 对于 UDP,如果还没有上层交付数据,UDP 的报文长度仍然在。同时,UDP 是一个一个把数据交付给应用层,就有很明确的数据边界。
• 站在应用层的角度,使用 UDP 的时候,要么收到完整的 UDP 报文,要么不收,不会出现"半个"的情况。所以UDP是不存在粘包问题的。
7.TCP异常情况
情况一:进程终止
我们知道,网络通信的本质是进程和进程之间的通信,如果通信的过程中,其中一个进程崩溃了,会怎么样呢?
一个进程崩溃,也就是其中一个进程终止,进程终止会释放文件描述符,仍然可以发送 FIN,通信双方会进行正常的四次挥手断开连接,和正常关闭没有什么区别.
情况二:机器重启
当一台通信主机重启时,通信双方会怎么样呢?
主机重启,操作系统需要终止所有进程,然后再进行重启。此时,不就和进程终止的情况一样了?所以,通信双方会正常进行四次挥手断开连接,和正常关闭是一样的。
情况三:机器掉电 / 网线断开
当发送方机器掉电或者网线断开,通信双方会怎么样呢?
此时,接收端认为连接还在,一旦接收端有写入操作,接收端发现连接已经不在了,就会进行 reset,要求对方重新建立连接。
即使没有写入操作,TCP 自己也内置了一个保活定时器,会定期询问对方是否还在。如果对方不在, 也会把连接释放。
另外,应用层的某些协议,也有一些这样的检测机制。例如 HTTP 长连接中,也会定期检测对方的状态。
8.TCP小结
TCP之所以设计的这么复杂,是因为它既要保证可靠性,又要尽可能的提高通信的效率。
保证可靠性的机制有:
• 校验和
• 序列号(按序到达)
• 确认应答
• 超时重传
• 连接管理
• 流量控制
• 拥塞控制
提高通信效率的机制有:
• 滑动窗口
• 快速重传
• 延迟应答
• 捎带应答
9.TCP和UDP的使用场景
TCP的特点是 可靠,有连接,面向字节流。适用于可靠传输的情况,应用于文件传输,重要状态更新等场景。例如:金融领域、支付……
UDP的特点是 不可靠,无连接,面向数据报。适用于对高速传输和实时性要求较高的通信领域,例如:视频传输,直播,游戏等……