🎉博主首页:
有趣的中国人
🎉专栏首页:
Linux网络
🎉其它专栏:
C++初阶 |
C++进阶 |
初阶数据结构
小伙伴们大家好,本片文章将会讲解 TCP的流量控制和滑动窗口 的相关内容。
如果看到最后您觉得这篇文章写得不错,有所收获,麻烦点赞👍、收藏🌟、留下评论📝。您的支持是我最大的动力,让我们一起努力,共同成长!
文章目录
- `1. 流量控制`
- ==<font color = blue><b>🎧1.1 流量控制是什么🎧==
- ==<font color = blue><b>🎧1.2 如何控制发送方的速度🎧==
- `2. 滑动窗口`
- ==<font color = blue><b>🎧2.1 什么是滑动窗口🎧==
- ==<font color = blue><b>🎧2.2 深入理解滑动窗口🎧==
上一篇文章中,博主介绍了 :
- TCP 的三次握手和四次挥手;
建议将上一篇文章看完之后再来看这篇文章,链接如下:
【Linux网络】详解TCP协议(2)那么接下来正片开始:
1. 流量控制
🎧1.1 流量控制是什么🎧
- 接收端处理数据的速度是有限的, 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送, 就会造成丢包;
- 继而引起丢包重传等等一系列连锁反应;
- 因此
TCP
支持根据接收端的处理能力, 来决定发送端的发送速度。 这个机制就叫做流量控制(Flow Control)。 - 总的来说就是根据接收方的接受能力控制发送方发送数据的速度。
🎧1.2 如何控制发送方的速度🎧
- 如何控制发送方的发送数据的速度分为两种情况:
- 一种是接收方接收缓冲区太大;
- 另一种是接收方接收缓冲区太小。
16位窗口大小
- 首先回顾一下
TCP
报头:
- 在报头中存在 16位窗口大小,这个窗口大小就是接收方用来通知发送方它的接收缓冲区大小而存在的。
- 当接收方接收缓冲区大,回复的
ACK
中的窗口大小就大,反之亦然;
那么这个时候很多人可能会有一个疑问,那么在发送方第一次给接收方发送消息的时候是怎么知道窗口大小的呢?
- 其实虽然是第一次发送消息,但是他们双方在三次握手建立连接的时候,接收方就已经给发送方发送了携带
ACK
的数据包; - 这个时候就已经给发送方通告了接收方接收缓冲区的大小。
- TCP报文中窗口大小是16位的,那么最大的窗口大小就是 2 16 − 1 = 65535 ( K B ) 2^{16} - 1 = 65535 (KB) 216−1=65535(KB),但是真的是这样的吗;
- 在
TCP
报头中,除了20Byte
的固定的TCP
报头大小,还有个40Byte
的选项,在这里面包含了窗口扩大因子M
,实际上的窗口大小就是窗口字段的值左移M
位。
PSH 标志位
- 当接收方接收缓冲区的大小很小,甚至为 0 0 0 的时候,怎么办呢?一般有三种做法:
- 首先当窗口大小为 0 0 0 的时候,如果过了一段时间接收方的接收缓冲区扩大了,就会给发送方发送一个窗口更新通知,告诉发送方可以发送数据了;
- 当然,如果过了超时重传的事件之后,方式方还没有接收到窗口大小变化的通知,发送方也会不断地向接收方发送窗口探测的数据包,如果发现接收方回复的
ACK
的数据包中窗口大小变大了,就会发送数据了; - 但是如果接收方的接收缓冲区长时间没有变化,发送方也会发送一个带有
PSH
标志位的报文,通知接收方得快速地处理接收缓冲区中的数据。
URG标志位
这样看来,我们已经学习了五个标志位,就差最后一个URG标志位了,那我们就来讲解一下URG标志位吧!
- 如果在
TCP
报头中URG
标志位置1
,说明报头中的16位紧急指针是有效的; - 这个紧急指针代表着在
TCP
的数据中,和首位置的偏移量是多少; - 一般在
TCP
中紧急数据只能有 1 B y t e 1 Byte 1Byte ;
在
send()
和recv()
系统调用中都有紧急数据的设置,我们再来看一下接口:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
- 使用
send()
系统调用发送数据时,可以选择将带外数据发送到指定的套接字。通过设置标志参数中的MSG_OOB
,可以标识要发送的带外数据。 - 其中,
flags
参数可以传递MSG_OOB
,以指示发送的字节是带外数据。
- 使用
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
- 使用
recv()
系统调用接收数据时,也可以指定标志以接收带外数据。通过设置标志参数中的MSG_OOB
,接收方可以读取带外数据。 - 其中,
flags
参数可以传递MSG_OOB
,以指示接收带外数据。
- 使用
2. 滑动窗口
在讲解完流量控制和超时重传机制,其实还是有两个问题:
- 发送方如何根据接收方的窗口大小发送数据呢?
- 超时时间内,数据并没有被丢弃,那存储在哪里了呢?
为了理解这两个问题,我们得先学习滑动窗口。
🎧2.1 什么是滑动窗口🎧
- 博主在
TCP
第一篇文章 中讲过,发送方可以一次性给接收方发送多条数据,例如下图:
- 这样发送数据就可以大大提高效率,因为可以在一个时间段发送多条数据。
- 而发送方就规定一个概念:滑动窗口,并且在滑动窗口内的数据可以直接发送,暂时不用收到应答。
- 在窗口左侧的数据就是已发送,已确认了的;
- 在窗口右侧的数据就是未发送,未确认;
- 在窗口中的数据就是等待发送的数据。
- 而滑动窗口的大小就是对方同步给我的接收缓冲区的大小,即对方的接受能力(暂时)。
滑动窗口抽象图
🎧2.2 深入理解滑动窗口🎧
那么依然有几个问题:
滑动窗口可以变大吗?
滑动窗口可以变小吗?
滑动窗口可以向左移动吗?OK,那么接下来就来解答这几个问题。
- 滑动窗口本质上就是对方接受能力的大小,即对方接收缓冲区的大小;
- 所以如果对方接受能力变大,那么滑动窗口就会变大,反之亦然;
- 如果对方的接收缓冲区中已经没有空间了,即对方接收缓冲区满了,那么滑动窗口的大小自然就变成了 0 0 0;
- 滑动窗口是不能向左滑动的。因为在窗口左边的数据是已发送已确认了的,所以就不可能再重新发送一次了。
滑动窗口表示方法
- 博主之前讲解过,缓冲区可以想象成一个很大的数组,那么缓冲区中的每一个数据就天然有了一个下标;
- 那么滑动窗口中的数据就也有自己的下标;
- 当接收方给我回复
ACK
的时候,在TCP
报头中有确认序号,所以这个序号就可以代表窗口的起始地址,而报头中窗口的大小就是滑动窗口的大小。 - 这样我们就可以用几行代码表示窗口的起始地址以及结束地址:
win_start = ack_seq;
win_end = win_start + win_size;
- 这样我们就可以直观的理解什么是窗口的变大、变小甚至为 0 0 0,以及为什么不能向左移动;
- 当然缓冲区的大小不可能是无穷大的,所以在底层应该使用环形队列来实现的,可能会用取模运算来实现。
滑动窗口丢包问题
如果滑动窗口中有数据丢包应该怎么办呢?丢包起始可以分成三种情况:
- 头部丢包;
- 中间丢包;
- 为不丢包;
ACK
丢包
先来看一下头部丢包:
- 确认序号的定义是表示接收端这个序号之前的所有数据我已经收到了;
- 那么假设头部丢包,接收方发给发送方的
TCP
报文中,确认序号就一直是 0 0 0(上图的情况)。 - 那么发送方接收到所有报文中确认序号就都是 0 0 0(上图的情况);
- 当发送方接收到连续三条相同的确认应答,就会进行重传;
- 这种机制叫做快速重发机制,又称为快重传。
- 快重传机制和超时重传机制并不冲突,超时重传机制相当于是给快重传机制兜了底。
- 上图如果发生了快重传,并且接收方接收到了数据,那么下一次的确认序号就是 8001 8001 8001;
中间数据和尾部数据丢包:
- 当中间数据或者尾部数据丢包,那么发送方收到的确认序号就是丢包的那个部分的开始序号;
- 由于滑动窗口的起始地址就是等于确认序号,因此丢包的那部分数据就变成了头部,进而全部转换成了头部数据丢包。
ACK丢包:
- 这种情况并不要紧,因为可以通过后续的
ACK
确认接收方是否收到了。