滑动窗口机制
1. 滑动窗口介绍
在进行数据传输时,如果传输的数据比较大,就需要拆分为多个数据包进行发送。TCP 协议需要对数据进行确认后,才可以发送下一个数据包,如图所示。
从上图中可以看到,发送端每发送一个数据包,都需要得到接收端的确认应答以后,才可以发送下一个数据包。这种一发一收的方式大大浪费了时间。为了避免这种情况,TCP引入了窗口概念,其可以一次发送多条数据,并接收多条应答,如下图所示
窗口大小
指的是不需要等待确认应答包而可以继续发送数据包的最大值,上图的窗口大小就是4000个字节(四个字段)发送前四个字段的时候, 不需要等待任何ACK, 直接发送;
收到第一个ACK后, 滑动窗口向后移动, 继续发送第五个段的数据; 依次类推;
操作系统内核为了维护这个滑动窗口, 需要开辟
发送缓冲区
来记录当前还有哪些数据没有应答; 只有确认应答过的数据, 才能从缓冲区删掉;
窗口越大, 则网络的吞吐率就越高;
窗口大小指的是可以发送数据包的最大数量。在实际使用中,它可以分为两部分。第一部分表示数据包
已经发送
,但未
得到确认应答
包;第二部分表示
允许发送
,但
未发送
的数据包。在进行数据包发送时,当发送了最大数量的数据包(窗口大小数据包),有时不会同时收到这些数据包的确认应答包,而是收到部分确认应答包。
那么,此时窗口就通过滑动的方式,向后移动,确保下一次发送仍然可以发送窗口大小的数据包。这样的发送方式被称为滑动窗口机制。设置窗口大小为 3,滑动窗口机制原理如图所示。
上图中,每 1000 个字节表示一个数据包。发送端同时发送了 3 个数据包(2001-5000),接收端响应的确认应答包为“下一个发送4001”,表示接收端成功响应了前两个数据包,没有响应最后一个数据包。此时,最后一个数据包要保留在窗口中。
由于窗口大小为 3,发送端除了最后一个包以外,还可以继续发送下两个数据包(5001-6000 和 6001-7000)。窗口滑动到 7001 处。
2. 数据重发
在进行数据包传输时,难免会出现数据丢失情况。这种情况一般分为两种。
2.1 确认应答包(ACK)丢了
对上图进行图解
- 发送端发送数据包(窗口大小为3):同时发送 3 个数据包 1-1000、1001-2000 和 2001-3000。
- 接收端返回确认应答包:接收端接收到这些数据,并给出确认应答包。数据包 1-1000 和数据包 2001-3000 的确认应答包没有丢失,但是数据包 1001-2000 的确认应答包丢失了。
- 发送端第 2 次发送数据包:发送端收到接收端发来的确认应答包,虽然没有收到数据包 1001-2000 的确认应答包,但是收到了数据包 2001-3000 的确认应答包**(下一个是3001)**,于是判断第一次发送的 3 个数据包都成功到达了接收端。再次发送 3 个数据包 3001-4000、4001-5000 和 5001-6000。
- 接收端返回确认应答包:接收端接收到这些数据,并给出确认应答包。数据包 3001-4000 和数据包 4001-5000 的确认应答包丢失了,但是数据包 5001-6000 没有丢失。
- 发送端第 3 次发送数据包:发送端收到接收端发来的确认应答包,查看到数据包 5001-6000 收到了确认应答包**(下一个是6001)**。于是判断第 2 次发送的 3 个数据包都成功到达了接收端。
由于序号是有序的,如果接收到后面数据的ACK,说明前面的数据已经被接收,只是发送的ACK丢包了。这种情况就表示前面的数据包已经成功被接收端接收了,发送端也就不需要重新发送前面的数据包了。
2.2 发送数据包丢失
- 发送端发送数据包(窗口大小为3):同时发送 3个数据包,分别为 1-1000、1001-2000 和 2001-3000。
-
接收端返回确认应答包:接收端接收到这些数据,并给出确认应答包。接收端收到了数据包 1-1000,返回了确认应答包;但是数据包 1001-2000,在发送过程中丢失了,没有成功到达接收端。数据包 2001-3000 没有丢失,成功到达了接收端,但是该数据包不是接收端应该接收的数据包,数据包 1001-2000 才是真正应该接收的数据包。因此收到数据包 2001-3000 以后,接收端
第一次
返回
下一个是 1001
的确认应答包。 - 发送端发送数据包:发送端仍然继续向接收端发送 3个数据包,分别为 3001-4000、4001-5000 和 5001-6000。
-
接收端返回确认应答包:接收端接收到这些数据,并给出确认应答包。当接收端收到数据包 3001-4000 时,发现不是自己应该接收的数据包 1001-2000,
第二次
返回
下一个是 1001
的确认应答包。当接收端收到数据包 4001-5000 时,仍然发现不是自己应该接收的数据包 1001-2000,
第三次
返回**下一个是1001
的确认应答包。以此类推直到接收完所有数据包,接收端都返回
下一个是1001 **的确认应答包。 -
发送端重发数据包:
发送端连续
3 次
收到
接收端发来的**下一个是1001 **的确认应答包,认为数据包 1001-2000 丢失了,就进行重发该数据包。 - 接收端收到重发数据包:接收端收到重发数据包以后,查看这次是自己应该接收的数据包 1001-2000,并返回确认应答包,告诉发送端,下一个该接收 6001 的数据包了。
- 发送端发送数据包:发送端收到确认应答包后,继续发送窗口大小为 3的数据包,分别为 6001-7000、7001-8000 和8001-9000。
对于步骤6、7:由于之前2001-6000的数据接收端其实之前就已经收到了, 被放到了接收端操作系统内核的
接收缓冲区
中,所以直接发送6001开始的数据即可。
3. 流量控制
在使用滑动窗口机制进行数据传输时,发送方根据实际情况发送数据包,接收端接收数据包。但是,接收端处理数据包的能力是不同的,因此可能出现下面两种现象
-
如果窗口过小,发送端发送少量的数据包,接收端很快就处理了,并且还能处理更多的数据包。这样,当传输比较大的数据时需要不停地等待发送方,造成很大的延迟。
-
如果窗口过大,发送端发送大量的数据包,而接收端处理不了这么多的数据包,这样,就会堵塞链路。如果丢弃这些本应该接收的数据包,又会触发重发机制。
为了避免这种现象的发生,TCP 提供了流量控制。所谓的流量控制就是
动态调节窗口大小发送数据包
。发送端
第一次
以窗口大小**(第一次的窗口大小是根据链路带宽的大小来决定的)**发送数据包,接收端接收这些数据包,并返回确认应答包,告诉发送端自己下次希望收到的数据包是多少(新的窗口大小),发送端收到确认应答包以后,将以该窗口大小进行发送数据包。
图解如下:
-
首先发送端根据
当前链路带宽大小
决定发送数据包的
窗口大小
。假设初始窗口大小为3,因此发送端发送了 3 个数据包,分别为 1-1000、1001-2000 和 2001-3000。 - 接收端接收这些数据包,但是缓冲区只能处理 2 个数据包,第 3 个数据包 2001-3000 没有被处理。因此只返回前两个的确认应答包,并设置窗口大小为 2000,告诉发送端自己现在只能处理 2 个数据包,下一次请发送 2 个数据包。
- 发送端接收到确认应答包,查看到接收端返回窗口大小为 2000,知道接收端只处理了 2 个数据包。发过去的第 3 个数据包 2001-3000 没有被处理。这说明此时接收端只能处理 2 个数据包,第 3 个数据包还需要重新发送。
- 因此发送端发送 2 个数据包 2001-3000 和 3001-4000。接收端收到这两个数据包并进行了处理。此时,还是只能处理 2 个窗口,继续向发送端发送确认应答包,设置窗口为 2,告诉发送端,下一个应该接收 4001 的数据包。
窗口探测
但是,如果在接收端返回的确认应答包中,窗口设置为 0,则表示现在不能接收任何数据。这时,
发送端将不会再发送数据包
,只有等待接收端发送窗口更新通知才可以继续发送数据包。
如果这个更新通知在传输中
丢失
了,那么就可能导致无法继续通信。为了避免这样的情况发生,发送端会时不时地发送
窗口探测包
,该包仅有1个字节,用来获取最新的窗口大小的信息,如下图所示
上述图解
- 发送端发送数据。发送端以窗口大小为 2000,发送了 2 个数据包,分别为 4001-5000 和 5001-6000。接收端接收到这些数据以后,缓冲区满了,无法再处理数据,于是向发送端返回确认应答包,告诉它下一个接收 6001 的数据,但是现在处理不了数据,先暂停发送数据,设置窗口大小为 0。
- 发送端暂停发送数据。发送端收到确认应答包,查看到下一次发送的是 6001 的数据,但窗口大小为 0,得知接收端此时无法处理数据。此时,不进行发送数据,进入等待状态。
- 接收端发送窗口大小更新包。当接收端处理完发送端之前发来的数据包以后,将会给发送端发送一个窗口大小更新包,告诉它,此时可以发送的数据包的数量。这里设置窗口大小为 2000,表示此时可以处理 2 个数据包,但是该数据包丢失了,没有发送到发送端。
-
发送端发送窗口探测包。由于窗口大小更新包丢失,发送端的等待时间超过了重发超时时间。此时,发送端向接收端发送一个窗口探测包,
大小为 1 字节
,这里是 6001。 - 接收端再次发送窗口大小更新包。接收端收到发送端发来的探测包,再次发送窗口大小更新包,窗口大小为 2000。
- 发送端发送数据。发送端接收到窗口大小更新包,查看到应该发的是 6001 的数据包,窗口大小为2000,可以发送 2个数据包。因此发送了数据包,分别为 6001-7000和 7001-8000。
4. 拥塞控制
虽然TCP有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据. 但是如果在刚开始阶段就发送大量的数据, 仍然可能引发问题.因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵. 在不清楚当前网络状态下, 贸然发送大量的数据, 是很有可能引起雪上加霜的. 于是,TCP引入
慢启动
机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据。
如上图所示
- 发送开始的时候, 定义拥塞窗口大小为1;
- 每次收到一个ACK应答, 拥塞窗口加1;
-
每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较,
取较小
的值作为实际发送的窗口;
线增积减(和式增加,积式减少)
像上面这样的拥塞窗口增长速度, 是指数级别的. “慢启动” 只是指初使时慢, 但是增长速度非常快,具体的增长如下所示
刚开始的时候从1指数增长,到达阈值后开始线性增长,如果出现网络阻塞,直接减小到初始值,然后再次指数增长到达新的阈值(新阈值为上次阻塞窗口大小的一半),再次线性增长直到网络阻塞,一直这样动态变换循环。
5. 延迟应答
在之前的问题中我们提到,如果发送端发送数据后,接收数据的主机需要返回ACK应答, 但这时候如果立刻返回的话,窗口可能比较小(缓冲区的数据只处理了一部分),所以TCP中采用了延迟应答机制,举个例子
现在有一个超市,里面卖泡面,假设库房最多存储100箱,隔段时间就有人来补货。有一天
早上
,超市还有50箱泡面时,补货的人来询问,现在需要补多少箱泡面,这时最多补货50箱(已经有50箱,库房只能装100箱),但是白天肯定会卖出去一部分,如果这时候补货,第二天又要再次补货,就太麻烦。 所以老板给补货的人说,我晚上给你打电话,告诉你我要多少箱。那天白天卖出去了30箱,所以库房只剩20箱,于是老板晚上给补货的人打电话说,你明天给我补货80箱~
上述例子中,老板晚上告诉补货员的方式就相当于延迟应答。
那么所有的包都可以延迟应答么? 肯定也不是
数量限制: 每隔N个包就应答一次。(N一般为2)
时间限制: 超过最大延迟时间就应答一次。(时间一般为200 ms,必须小于超时重传时间,不然就重传了)
6. 捎带应答
根据应用层协议,发送出去的消息到达对端,对端进行处理之后,会返回一个回执。即在很多情况下,客户端服务器在应用层也是“一发一收”的,意味着客户端给服务器说了“How are you”,服务器再给客户端返回ACK后,接着会给客户端回一个“Fine,thank you”,在延时应答的基础上,让ACK等待一段时间(不能超过超时重发时间)后和“Fine,thank you”通过一个包同时发送。
另外接受数据以后如果立刻返回数据,就无法实现捎带应答,所以
是在
延迟应答
的基础上,才能进行的
捎带应答
。
延迟确认应该是能够提高网络利用率从而降低计算机处理负荷的一种较优的处理机制。
就像之前提到的问题:四次挥手可以只挥手三次吗?
在捎带应答的情况下是可以的,当ACK延迟应答,就可能刚好与在数据缓冲区中的数据处理完后,与FIN合并,在一起发送。(相当于FIN捎带上了ACK)
捎带应答是指在同一个TCP包中即发送数据又发送确认应答的一种机制。由此,网络的利用率会提高,计算机的负荷也会减轻。不过,确认应答必须等到应用处理完数据并将作为回执的数据返回为止,才能进行捎带应答。
7. 粘包问题
TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。
如果双方建立连接,需要在连接后一段时间内发送不同结构数据,如连接后,有好几种结构:
- “你好不好”
- “我很好”
那这样的话,如果发送方连续发送这个两个包出去,接收方一次接收可能会是”你好不好我很好”这样对方可能就傻了,到底是好还是不好?
不知道,因为协议没有规定这么诡异的字符串,所以要处理把它分包,怎么分也需要双方组织一个比较好的包结构
如何处理粘包问题?
-
方式1:
在头加一个数据长度之类的包
比如,上述例子改为 “4你好不好3我很好”
这样就知道了,4就表示后面的数据内容应该是4个,3也类似,之前讲过的TCP服务器的
Content-length
字段就是这个作用
-
方式2:
使用特殊标记来区分消息间隔
比如,上述例子改为 “你好不好;我很好”
用**;
当做两个包的分隔符,之前讲到的TCP服务器中,响应头和响应体中间的
响应空行**就是这个作用
8. 保活机制
TCP协议中有长连接和短连接之分。短连接环境下,数据交互完毕后,主动释放连接;
双方建立交互的连接,但是并不是一直存在数据交互,有些连接会在数据交互完毕后,主动释放连接,而有些不会,那么在长时间无数据交互的时间段内,
交互双方都有可能出现掉电、死机、异常重启,还是中间路由网络无故断开、NAT超时等各种意外。
当这些意外发生之后,这些TCP连接并未来得及正常释放,那么,连接的另一方并不知道对端的情况,它会一直维护这个连接,长时间的积累会导致非常多的半打开连接,造成端系统资源的消耗和浪费,为了解决这个问题,在传输层可以利用TCP的保活报文来实现,这就有了TCP的Keep-alive(保活探测)机制。
服务器或客户端建立连接后,会按照一定时间间隔(保活定时器时长)发送一个“心跳包”,来保证对方还在线,如果对方长时间不在线就将断开连接。如下所示