内容来源:小林coding
本文是对小林coding的TPC流量控制的精简总结
发送方不能无脑的发数据给接收方,要考虑接收方处理能力
如果一直无脑的发数据给对方,但对方处理不过来,那么就会导致触发重发机制
从而导致网络流量的无端的浪费
为了解决这种现象发生,TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是所谓的流量控制
前面的流量控制例子,我们假定了发送窗口和接收窗口是不变的
但是实际上,发送窗口和接收窗口中所存放的字节数,都是放在操作系统内存缓冲区中的
而操作系统的缓冲区,会被操作系统调整
当应用进程没办法及时读取缓冲区的内容时,也会对我们的缓冲区造成影响
当应用程序没有及时读取缓存时,发送窗口和接收窗口的变化。
考虑以下场景:
根据上图的流量控制,说明下每个过程:
1.应用进程没有及时读取部分数据,导致数据暂时留在了缓冲区
2.为了让接收窗口能成功我们缓冲区中还没读取的数据,所以我们的接收窗口要变小,告诉发送端我们不能再接收大于这个量的数据了(接收大于这个量的数据我们会丢失)
3.然后我们把接收端改变的窗口大小发送给发送端,发送窗口大小减少
可见最后窗口都收缩为 0 了,也就是发生了窗口关闭。
当发送方可用窗口变为 0 时,发送方实际上会定时发送窗口探测报文,以便知道接收方的窗口是否发生了改变
当服务端系统资源非常紧张的时候,操作系统可能会直接减少了接收缓冲区大小
这时应用程序又无法及时读取缓存数据,那么这时候就有严重的事情发生了,会出现数据包丢失的现象
说明下每个过程:
所以,如果发生了先减少缓存,再收缩窗口,就会出现丢包的现象
为了防止这种情况发生,TCP 规定是不允许同时减少缓存又收缩窗口的
而是采用先收缩窗口,过段时间再减少缓存
这样就可以避免了丢包情况
在前面我们都看到了,TCP 通过让接收方指明希望从发送方接收的数据大小(窗口大小)来进行流量控制
如果窗口大小为 0 时,就会阻止发送方给接收方传递数据,直到窗口变为非 0 为止
这就是窗口关闭
接收方向发送方通告窗口大小时,是通过 ACK 报文来通告的
那么,当发生窗口关闭时,接收方处理完数据后,会向发送方通告一个窗口非 0 的 ACK 报文
如果这个通告窗口的 ACK 报文在网络中丢失了,那麻烦就大了
这会导致发送方一直等待接收方的非 0 窗口通知,接收方也一直等待发送方的数据
如不采取措施,这种相互等待的过程,会造成了死锁的现象
为了解决这个问题,TCP 为每个连接设有一个持续定时器
只要 TCP 连接一方收到对方的零窗口通知,就启动持续计时器。
如果持续计时器超时,就会发送窗口探测(Window probe)报文
而对方在确认这个探测报文时,给出自己现在的接收窗口大小
窗口探测的次数一般为 3 次,每次大约 30 - 60 秒(不同的实现可能会不一样)
如果 3 次过后接收窗口还是 0 的话,有的 TCP 实现就会发 RST 报文来中断连接
如果接收方太忙了来不及取走接收窗口里的数据,那么就会导致发送方的发送窗口越来越小。
到最后,如果接收方腾出几个字节并告诉发送方现在有几个字节的窗口
而发送方会义无反顾地发送这几个字节
这就是糊涂窗口综合症(接收方一有空间,发送方就会拼尽全力发送去争夺接收方那一点空间)
要知道,我们的 TCP + IP 头有 40 个字节,为了传输那几个字节的数据,要搭上这么大的开销,这太不经济了
就好像一个可以承载 50 人的大巴车,每次来了一两个人,就直接发车
除非家里有矿的大巴司机,才敢这样玩,不然迟早破产
要解决这个问题也不难,大巴司机等乘客数量超过了 25 个,才认定可以发车
现举个糊涂窗口综合症的栗子,考虑以下场景:
接收方的窗口大小是 360 字节,但接收方由于某些原因陷入困境,假设接收方的应用层读取的能力如下:
每个过程的窗口大小的变化,在图中都描述的很清楚了,可以发现窗口不断减少了,并且发送的数据都是比较小的了。
总结:也就是我们的窗口不断减小,导致我们能发送的数据都在不断减小,用更多的请求发更少的数据造成了资源浪费
所以,糊涂窗口综合症的现象是可以发生在发送方和接收方:
要解决糊涂窗口综合症,就要同时解决上面两个问题:
MSS(Maximum Segment Size):最大报文段长度,指的是 TCP 报文段中数据部分的最大长度。它决定了每次传输的数据块大小,与网络传输效率相关。
缓存空间:接收方用于存储接收到但尚未被应用程序读取的数据的内存空间
接收方通常的策略如下:
当「窗口大小」小于 min (MSS,缓存空间 / 2),也就是小于 MSS 与 1/2 缓存大小中的最小值时 就会向发送方通告窗口为 0,也就阻止了发送方再发数据过来。
等到接收方处理了一些数据后,窗口大小 >=MSS,或者接收方缓存空间有一半可以使用 就可以把窗口打开让发送方发送数据过来
发送方通常的策略如下:
使用 Nagle 算法,该算法的思路是延时处理,只有满足下面两个条件中的任意一个条件,才可以发送数据:
只要上面两个条件都不满足,发送方一直在囤积数据,直到满足上面的发送条件
Nagle 伪代码如下:
if 有数据要发送 {
if 可用窗口大小 >= MSS and 可发送的数据 >= MSS {
立刻发送MSS大小的数据
} else {
if 有未确认的数据 {
将数据放入缓存等待接收ACK
} else {
立刻发送数据
}
}
}
PS:如果接收方不能满足「不通告小窗口给发送方」,那么即使开了 Nagle 算法,也无法避免糊涂窗口综合症
因为如果对端 ACK 回复很快的话(达到 Nagle 算法的条件二),Nagle 算法就不会拼接太多的数据包,这种情况下依然会有小数据包的传输,网络总体的利用率依然很低。
所以,接收方得满足「不通告小窗口给发送方」+ 发送方开启 Nagle 算法,才能避免糊涂窗口综合症
另外,Nagle 算法默认是打开的,如果对于一些需要小数据包交互的场景的程序,比如,telnet 或 ssh 这样的交互性比较强的程序,则需要关闭 Nagle 算法。
可以在 Socket 设置 TCP_NODELAY 选项来关闭这个算法(关闭 Nagle 算法没有全局参数,需要根据每个应用自己的特点来关闭)
setsockopt(sock_fd, IPPROTO_TCP, TCP_NODELAY, (char *)&value, sizeof(int));
发送方不能无脑的发数据给接收方,要考虑接收方处理能力
不然就会导致触发重发机制,造成网络流量的无端浪费
发送窗口和接收窗口中所存放的字节数,都是放在操作系统内存缓冲区中的,所以说和缓冲区大小有关
因为应用层没有及时读取数据导致我们的接收窗口的量不断减少
因为接收窗口不断减少,导致我们的发送窗口量不断减少
如果发生了先减少缓存,再收缩窗口,就会出现丢包的现象
为了防止这种情况发生,TCP 规定是不允许同时减少缓存又收缩窗口的
而是采用先收缩窗口,过段时间再减少缓存
这样就可以避免了丢包情况
如果窗口大小为 0 时,就会阻止发送方给接收方传递数据,直到窗口变为非 0 为止
当发生窗口关闭时,接收方处理完数据后,会向发送方通告一个窗口非 0 的 ACK 报文,如果这个通告窗口的 ACK 报文在网络中丢失了,这会导致发送方一直等待接收方的非 0 窗口通知,接收方也一直等待发送方的数据
如不采取措施,这种相互等待的过程,会造成了死锁的现象
TCP 为每个连接设有一个持续定时器
只要 TCP 连接一方收到对方的零窗口通知,就启动持续计时器
如果持续计时器超时,就会发送窗口探测(Window probe)报文
窗口探测的次数一般为 3 次,每次大约 30 - 60 秒(不同的实现可能会不一样)
如果 3 次过后接收窗口还是 0 的话,有的 TCP 实现就会发 RST 报文来中断连接
(接收方一有空间,发送方就会拼尽全力发送去争夺接收方那一点空间)
太浪费了,那么点空间我们还拼尽全力去发送还发送不完全
一个可以承载 50 人的大巴车,每次来了一两个人,就直接发车
除非家里有矿的大巴司机,才敢这样玩,不然迟早破产
要解决这个问题也不难,大巴司机等乘客数量超过了 25 个,才认定可以发车
糊涂窗口综合症的现象是可以发生在发送方和接收方:
我们要解决两个问题:
1.让接收方不通告小窗口给发送方
2.让发送方避免发送小数据
两个概念
1.MSS(Maximum Segment Size):最大报文段长度,指的是 TCP 报文段中数据部分的最大长度。它决定了每次传输的数据块大小,与网络传输效率相关
2.缓存空间:接收方用于存储接收到但尚未被应用程序读取的数据的内存空间
当「窗口大小」小于 min (MSS,缓存空间 / 2),也就是小于 MSS 与 1/2 缓存大小中的最小值时 就会向发送方通告窗口为 0,也就阻止了发送方再发数据过来。
等到接收方处理了一些数据后,窗口大小 >=MSS,或者接收方缓存空间有一半可以使用 就可以把窗口打开让发送方发送数据过来
使用 Nagle 算法,该算法的思路是延时处理,只有满足下面两个条件中的任意一个条件,才可以发送数据:
PS:如果接收方不能满足「不通告小窗口给发送方」,那么即使开了 Nagle 算法,也无法避免糊涂窗口综合症
因为如果对端 ACK 回复很快的话(达到 Nagle 算法的条件二),Nagle 算法就不会拼接太多的数据包,这种情况下依然会有小数据包的传输,网络总体的利用率依然很低。
所以,接收方得满足「不通告小窗口给发送方」+ 发送方开启 Nagle 算法,才能避免糊涂窗口综合症