窗口

为什么要引入窗口机制

  • TCP 是每发送一个数据,都要进行一次确认应答。当上一个数据包收到了应答了, 再发送下一个。
  • 这个模式就有点像我和你面对面聊天,你一句我一句。但这种方式的缺点是效率比较低的。如果你说完一句话,我在处理其他事情,没有及时回复你,那你不是要干等着我做完其他事情后,我回复你,你才能说下一句话,很显然这不现实。
  • 所以,这种传输方式有一个缺点:数据包的晚返时间越长,通信的效率越低

为了解决这个问题,TCP引入了窗口这个概念。即使是在往返时间较长的情况下,它也不会降低网络通信的效率。

什么是窗口机制

有了窗口,就可以指定窗口的大小,窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值

  • 窗口的实现实际上是操作系统开辟的一个缓存空间
  • 发送方主机在等待确认应答返回之前,必须在缓冲区中保留已经发送的数据,如果按期收到确认应答,此时数据就可以从缓冲区清除。

假设窗口大小为3的TCP段,那么发送方就可以[连续发送]3个TCP段,并且中途如果有ACK丢失,可以通过[下一个确认应答进行确认]。如下图


图中的ACK 600确认应答报文丢失,也没有关系,因为可以通过下一个确认应答进行确认,只要发送方收到了ACK 700确认应答,就意味着700之前的所有数据[接收方]都已经收到了。这个模式就叫做[累计确认]或者[累计应答]

窗口分为滑动窗口和拥塞窗口。

这两个算法核心都是为了防止像网络中发送的包太多。不同的是两者的目的

  • 发送窗口用来控制发送接收端的流量:

    • 发送窗口反应了作为单TCP连接、点对点之间的流量控制模型,它是需要和接收端一起共同协调来调整大小的
    • 流量控制的滑动窗口rwnd是怕发送方把接收方缓存塞满;
    • 流量控制往往指点对点的通信量的控制,是一个端到端的问题。
  • 阻塞窗口用来控制多条连接公平使用的有限带宽:
    • 拥塞窗口反应了作为多个TCP连接共享带宽的拥塞控制模型,它是发送端独立的根据网络状况来动态调整的
    • 拥塞窗口cwnd是怕把网络塞满。若网络中有许多资源同时出现供应不足,网络性能就要明显变化,整个网络的吞吐量将随着输入负荷的增大而下降,这就是拥塞。
    • 拥塞控制就是防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不至于过载。
    • 拥塞控制是当整个网络的输入负载超过网络所能承受的时候,向发送方发送控制报文,要发送端放慢发送速率。
  • 拥塞窗口和滑动窗口共同控制着发送的速度

ps:

  • 对于发送端和接收端,并不是一个绝对的概念,而是同时存在的,即你在发数据的同时你也要接收数据不是?
  • 所以对于每个端来说,即是发送方也会是接受方。

TCP是双工的协议,会话的双方都可以同时接收、发送数据。TCP会话的双方都各自维护一个“发送窗口”和一个“接收窗口”。

  • 各自的“接收窗口”大小取决于应用、系统、硬件的限制(TCP传输速率不能大于应用的数据处理速率)。
  • 各自的“发送窗口”则要求取决于对端通告的“接收窗口”,要求相同

在任何一个时刻,TCP发送缓冲区的数据能否真正发送出去,至少取决于两个因素,一个是当前的发送窗口大小,另一个是拥塞窗口大小,而TCP协议中总是取两者中最小值作为判断依据,比如当前发送的字节为 100,发送窗口的大小是 200,拥塞窗口的大小是 80,那么取 200 和 80 中的最小值,就是 80,当前发送的字节数显然是大于拥塞窗口的,结论是不能发送出去。

发送方的滑动窗口,做流量控制

发送方的发送窗口是根据接收方的接收窗口设置的,接收端会根据自身的能力来调整win的大小

为了记录所有发送的包和接收的包,TCP也需要发送端和接收端分别都有缓存来保存这些记录,发送端的缓存里(即发送端口)是按照包的ID一个个排列,下图就是发送方缓存的数据,根据处理的情况分成四个部分,其中深蓝色方框是发送窗口,紫色方框是可用窗口:

  • #1 是已发送并收到 ACK确认的数据:1~31 字节(应该划掉的)
  • #2 是已发送但未收到 ACK确认的数据:32~45 字节(需要等待做完的恢复之后才能划掉)
  • #3 是未发送但总大小在接收方处理范围内:46~51字节(接收方还有空间)
  • #4 是未发送但总大小超过接收方处理范围:52字节以后(接收方没有空间)

这里面为什么要区分第三部分和第四部分呢? 这就关系到“流量控制”了。在TCP里面,接收端会给发送端报一个窗口的大小,叫做滑动窗口,这个窗口的大小应该等于上面的第二部分加上第三部分,就是已经交代了加上马上要交代的。超过这个窗口的,接收端做不过来,就不能发送了

在下图,当发送方把数据「全部」都一下发送出去后,可用窗口的大小就为 0 了,表明可用窗口耗尽,在没收到 ACK 确认之前是无法继续发送数据了。


在下图,当收到之前发送的数据32~36字节的 ACK 确认应答后,如果发送窗口的大小没有变化,则滑动窗口往右边移动 5 个字节,因为有 5 个字节的数据被应答确认,接下来 52~56 字节又变成了可用窗口,那么后续也就可以发送 52~56 这 5 个字节的数据了。

程序是如何表示发送方的四个部分的呢?

TCP 滑动窗口方案使用三个指针来跟踪在四个传输类别中的每一个类别中的字节。其中两个指针是绝对指针(指特定的序列号),一个是相对指针(需要做偏移)。

  • SND.WND:表示发送窗口的大小(大小是由接收方指定的);
  • SND.UNA:是一个绝对指针,它指向的是已发送但未收到确认的第一个字节的序列号,也就是 #2 的第一个字节。
  • SND.NXT:也是一个绝对指针,它指向未发送但可发送范围的第一个字节的序列号,也就是 #3 的第一个字节。
  • 指向 #4 的第一个字节是个相对指针,它需要 SND.UNA 指针加上 SND.WND 大小的偏移量,就可以指向 #4 的第一个字节了。

那么可用窗口大小的计算就可以是:可用窗口大 = SND.WND -(SND.NXT - SND.UNA)

  • 滑动窗口是接受端使用的窗口大小:

    • 用来告知发送端接收端的缓存大小,以此可以控制发送端发送数据的大小,用来控制流量,防止接收方处理不过来消息
    • 流量控制,主要就是为了防止接收方处理数据的速度跟不上发送方,避免随着时间推移,数据自然溢出接收方的缓冲区
    • 我们可以把理想中的TCP协议想象成一队运输货物的货车,运送的货物就是TCP数据包,这些货车把数据报从发送端运送到接收端,就这样不断周而复始。
      • 我们仔细想一下,货物到达接收端之后,是需要卸货处理、登记入库的,接收端限于自己的处理能力和仓库规模,是不可能让这队货车以不可控的速度发货的。
      • 接收端肯定会和发送端不断地进行信息同步,比如接收端通知发送端:“后面那 20 车你给我等等,等我这里腾出地方你再继续发货。”
    • 这其实就是发送窗口和接收窗口的本质,可以理解为TCP的生产者-消费者模型。
      • 发送窗口和接收窗口是TCP连接的双方,一个作为生产者,一个作为消费者,为了达到一致协同的生产-消费速率,而产生的算法模型的实现。
      • 说白了,作为TCP发送端,也就是生产者,不能忽略TCP的接收端,也就是消费者的实际状态,不管不顾的把数据包都传输过来。如果都传输过来,消费者来不及消费,必然会丢弃;而丢弃反过来使得生产者又重传,发送更多的数据包,最后导致网络崩溃。
  • 发送方会定时发送窗口探测数据包,看看是否有机会调整窗口的大小。当接收方比较慢的时候,要防止底层窗口综合征,别空出一个字节就赶快告诉发送方,然后马上又填满了,可以当窗口太小的时候,不更新窗口,直到达到一定大小,或者缓冲区一半为空,才更新窗口。这就是流量控制

举个例子

看这个场景, 老师说一段话, 学生来记.
老师说"从前有个人, 她叫马冬梅. 她喜欢他, 而他却喜欢她."

学生写道"从前有…". “老师你说的太快我跟不上”

于是他们换了一种模式.
老师说"从"

学生写"从". 学生说"嗯"

老师说"前"

学生写"前". 学生说"嗯"

老师说"今天我还想早点下班呢…"

于是他们换了一种模式.
老师说"从前有个人"

学生写"从前有个人". 学生说"嗯"

老师说"她叫马冬梅".

学生写"她叫马…梅". 学生说"马什么梅?"

老师说"她叫马冬梅".

学生写"她叫马冬…“. 学生说"马冬什么?”

老师"…"

学生说"有的时候状态好我能把5个字都记下来, 有的时候状态不好就记不下来. 我状态不好的时候你能不能慢一点. "

于是他们换了一种模式
老师说"从前有个人"

学生写"从前有个人". 学生说"嗯, 再来5个"

老师说"她叫马冬梅"

学生写"她叫马…梅". 学生说"啥?重来, 来2个"

老师说"她叫"

学生写"她叫". 学生说"嗯,再来3个"

老师说"马冬梅".

学生写"马冬梅". 学生说"嗯, 给我来10个"

老师说"她喜欢他,而他却喜欢她"

学生写…

所以呢

  • 第一种模式简单粗暴, 发的只管发, 收的更不上.
  • 第二种模式稳定却低效, 每发一个, 必须等到确认才再次发送, 等待时间很多.
  • 第三种模式提高了效率, 分组进行发送, 但是分组的大小该怎么决定呢?
  • 第四中模式才是起到了流控的作用, 接收方认为状态好的时候, 让发送方每次多发一点. 接收方认为状态不好的时候(阻塞), 让发送方每次少发送一点.

问题:她叫马 应该已经接受到了,然后应该让发送方发送 冬梅 而不是 她叫 吧

  • 数据片丢失,需要重新发送整个TCP报文段
  • 无法确认报文段中数据片丢失部分,因此整段重发

接收方的滑动窗口,做拥塞控制

接下来我们看看接收方的窗口,接收窗口相对简单一些,根据处理的情况划分成三个部分:

  • #1 + #2 是已成功接收并确认的数据(做完的,等待应用进程读取);
  • #3 是未收到数据但可以接收的数据(自己能够接收的最大工作量)
  • #4 未收到数据并不可以接收的数据(超过工作量的部分,实在做不完)


其中三个接收部分,使用两个指针进行划分:

  • RCV.WND:表示接收窗口的大小,它会通告给发送方。
  • RCV.NXT:是一个指针,它指向期望从发送方发送来的下一个数据字节的序列号,也就是 #3 的第一个字节。
  • 指向 #4 的第一个字节是个相对指针,它需要 RCV.NXT 指针加上 RCV.WND 大小的偏移量,就可以指向 #4 的第一个字节了。

“接收(拥塞)窗口”大小取决于应用、系统、硬件的限制(TCP传输速率不能大于应用的数据处理速率)。也就是说拥塞窗口用来做拥塞控制,拥塞窗口的大小会随着网络状况实时调整。

  • 接收窗口是发送端使用的窗口大小,用来处理网络上数据包太多的情况,以避免网络中出现拥塞

    • TCP的生产者-消费者模型,只是在考虑单个连接的数据传递,但是,TCP数据包是需要经过网卡、交换机、核心路由器等一系列的网络设备的,网络设备本身的能力也是有限的,当多个连接的数据包同时在网络上传送时,势必会发生带宽争抢、数据丢失等,这样,TCP就必须考虑多个连接共享在有限的带宽上,兼顾效率和公平性的控制,这就是拥塞控制的本质。
    • 举个形象一点的例子:
      • 有一个货车行驶在半夜三点的大路上,这样的场景是断然不需要拥塞控制的。
      • 我们可以把网络设备形成的网络信息高速公路和生活中实际的高速公路做个对比。正是因为有多个 TCP 连接,形成了高速公路上的多队运送货车,高速公路上开始变得熙熙攘攘,这个时候,就需要拥塞控制的接入了。
  • 那发送方怎么判断网络是不是满的呢?
    • 这其实是个挺难的事情,因为对于TCP协议来讲,它压根就不知道整个网络路径都会经历什么,相当于一个黑盒。TCP发送包常被比喻为往一个水管里面灌水,而TCP的拥塞控制就是在不阻塞,不丢包的情况下,尽量发挥带宽
    • 水管有粗细,网络有带宽,也就是每秒钟能够发送多少数据;水管有长度,端到端有时延。在理想情况下,水管里的水=水管粗细 * 水管长度,对应到网络上,通道的容量=带宽 * 时延


如果我们设置发送窗口,使得发送但是没有确认的包为管道的容量,就能够撑满整个管道

如图所示,假设往返时间为8s,去4s,回4s,每秒发送一个包,每个包1024bytes,已经过去了8s,则8个包都发出去了,其中前4个包已经到达了接收端,但是ACK还没有返回,不能算发送成功。5-8后四个包还在路上,还没有被接收。这个时候,整个管道正好撑满,在发送端,已发送未确认的为8个包,正好等于带宽,也就是每秒发送1个包,乘以来回时间8s。

如果我们在这个基础上再调大窗口,使得单位时间内可以有更多的包发送,会出现什么现象呢?

  • 我们来想,原来发送一个包,从一端到达另一端,假设一共经过四个设备,每个设备处理一个包时间耗费1s,所以等待另一端需要耗费4s,如果发送得更加快速,则单位时间内,会有更多的包到达这些中间设备,这些设备还是只能每秒处理一个包的话,多出来的包就会被丢弃,这是我们不想看到的。

  • 这时候,我们可以向其他方法,假设这四个设备本来每秒处理一个包,但是我们在这些设备上加上缓存,处理不过来就在队列里排着,这样包不会丢失,但是确认是会增加时延,这个缓存的包,4s肯定到达不了接收端,如果时延到达一定程序,就会超时重传,也是我们不想看到的。

于是TCP的拥塞控制主要用来避免两种情况:包丢失和超时重传。一旦出现了这些现象就说明,发送速度太快了,要慢一点。

慢开始和拥塞避免

但是一开始我们怎么知道要速度多快呢?我怎么知道应该把窗口调整到多快呢?

  • 就像往漏斗里面灌水一样,一开始慢慢的到,最后达到最快能允许的

  • 同样的:

    • 发送窗口先设置cwnd=1,发送第一个报文段,表示一次只能发送一个
    • 以后每收到一个报文段确认,cwnd加1,于是一次能够发送两个;
    • 当两个ACK来的时候,每个确认cwnd加1,两个确认cwnd加2,于是一次能够发送4个(即呈指数增长)
    • 假设增长到某值(如24),网络出现超时,此时将ssthresh值变为值的一半(如12)(乘法减小),让后将cwnd置1,重新采用慢开始算法,重复如上步骤。
    • 涨到什么时候是个头呢?有一个值ssthresh为65535个字节,当超过这个值的时候,就要小心一点,不能倒这么快了,可能快满了,要慢下来。改用拥塞避免算法(加法增大)
    • 每收到一个确认后,cwnd增加1/cwnd,比如一次发送8个,当8个确认到达的时候,每次增加1/8,8个确认一共cwnd增加1,于是一次能够发送9个,编程了线性增张
    • 就这样,知道涨到了管道满了,出现了拥塞,这时候就要降低速度,等待移除的水慢慢渗下去
  • 拥塞的一种表现形式是丢包,需要超时重传。就是慢启动

但是TCP拥塞控制也有问题:

  • 第一个问题是丢包并不代表管道满了,有可能了管道本来就漏水。比如公网上带宽不满也会丢包,这个时候就认为拥塞了,是不对的
  • 第二个是TCP的拥塞控制要等到将中间设备都填充满了,才发生丢包,从而降低速度,这个时候已经晚了。其实TCP只要填满管道就可以了,不应该接着填,直到连缓存也填满

为了优化这两个问题,出现了TCP BBR拥塞算法,它企图找到一个平衡点,就是通过不断的加快发送速度,将管道填满,但是不要填满中间设备的缓存,因为这样时延会增加,在这个平衡点可以很好的达到高带宽和低时延的平衡

即:“慢启动”,它通过一定的规则,慢慢的将网络发送数据的速率增加到一个阈值。超过这个阈值之后,慢启动就结束了,另一个叫做“拥塞避免”的算法登场。在这个阶段,TCP会不断的探测网络状况,并随之不断调整拥塞窗口的大小。

快重传和快恢复

一条TCP连接有时会因等待重传计时器的超时而空闲较长的时间,慢开始和拥塞避免无法很好的解决这类问题,因此提出了快重传和快恢复的拥塞控制方法。

快重传算法并非取消了重传机制,只是在某些情况下更早的重传丢失的报文段(如果当发送端接收到三个重复的确认ACK时,则断定分组丢失,立即重传丢失的报文段,而不必等待重传计时器超时)。

快重传的算法思路是:

  • 要求接收方每收到一个时序的报文段后立即发出重复确认,而不是等待发送数据时才进行捎带确认
  • 发送方只要一连收到三个重复确认,就应当立即重传对方尚未收到的报文段,而不必等待设置的重传计时器到期

快恢复的算法思路是:

  • 当发送方连续收到三个重复确认时,就执行“乘法减小”算法,把慢开始阀值ssthresh减半
  • 接着不执行慢开始,而是从新阀值ssthresh开始执行拥塞避免算法(加法增大)

接收窗口和发送窗口的大小是相等的吗?

并不是完全相等,接收窗口的大小是约等于发送窗口的大小的。

因为滑动窗口并不是一成不变的。比如,当接收方的应用进程读取数据的速度非常快的话,这样的话接收窗口可以很快的就空缺出来。那么新的接收窗口大小,是通过 TCP 报文中的 Windows 字段来告诉发送方。那么这个传输过程是存在时延的,所以接收窗口和发送窗口是约等于的关系。

窗口大小由哪一方决定

TCP头里面有一个字段叫做windows,也就是窗口大小。这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来

所以:

  • 通常窗口的大小是由接收方的窗口大小来决定的。
  • 发送方发送的数据大小不能超过接收方的窗口大小,否则接收方就无法正常接收数据

窗口引起的问题

问题:在任何一个时刻里,TCP 发送缓冲区的数据是否能真正发送出去,用了“至少两个因素”这个说法,那除了之前引入的发送窗口、拥塞窗口之外,还有什么其他因素吗?

延迟ACK

  • 场景:

    • 从接收端的角度来看,接收端需要对每个接收到的TCP分组进行确认,也就是发送ACK报文,但是ACK报文本身是不带数据的分段,如果一直这样发送大量的ACK报文,就会消耗大量的带宽。
    • 之所以会这样,是因为TCP报文、IP报文固有的消息头是不可或缺的,比如两端的地址、端口号、时间戳、序列号等信息,在这种情况下,合理的做法又是什么呢?
  • 怎么解决?
    • 在接收端进行优化,这个优化的算法叫做延时ACK。延时ACK在收到数据行并不马上回复,而是累计需要发送的ACK报文,等待有数据需要发送给对端时,将累计的ACK捎带一并发出去
    • 当然,延时ACK机制,不能无限的延时下去,否则发送端误认为数据包没有发送成功,引起重传,反而会占用额外的网络带宽

糊涂窗口综合症

在滑动窗口机制下,如果发送端和接收端速率很不一致,也会产生这种比较犯傻的状态:发送方发送的数据,只要一个大大的头部,携带数据很少。

  • 对于接收端来讲,如果接收很慢,一次接收1个字节或者几个字节,这个时候接收端 缓冲区很快就会被填满,然后窗口通告为0字节,这个时候发送端停止发送,应用程序收上去1个字节后,发出窗口通告为1字节,发送方收到通告之后,发出1个字节的数据,这样周而复始,传输效率会非常低
  • 同时如果发送端程序一次发送一个字节,虽然窗口足够大,但是发送仍是一个字节一个字节的传输,效率很低

为什么要解决?

  • 效率低
  • 极端情况下,有效载荷可能只有1个字节;传输开销有40字节(20字节的IP头+20字节的TCP头) 这种现象。

接收端引起的糊涂窗口综合症:(接收方可以通告一个小的窗口)

  • 场景:

    • 接收端处理得急不可耐,比如感刚刚读入了100个字节,就告诉发送端:“喂,我已经读走 100 个字节了,你继续发”,在这种情况下,发送端应该怎么做呢?
    • 就是接收方太忙了,那么就会导致发送方的发送窗口越来越小
    • 到最后,如果接收方腾出几个字节并告诉发送方现在有几个字节的窗口,而发送方会义无反顾地发送这几个字节,这就是糊涂窗口综合症
    • 要知道,我们的 TCP + IP 头有 40 个字节,为了传输那几个字节的数据,要达上这么大的开销,这太不经济了。
    • 就好像一个可以承载 50 人的大巴车,每次来了一两个人,就直接发车。除非家里有矿的大巴司机,才敢这样玩,不然迟早破产。要解决这个问题也不难,大巴司机等乘客数量超过了 25 个,才认定可以发车。
  • 怎么解决:让接收方不通告小窗口给发送方

    • 也就是说,接收端不能在接收缓冲区空出一个很小的部分之后,就急吼吼地向发送端发送窗口更新通知,而是需要在自己的缓冲区大到一个合理的值之后,再向发送端发送窗口更新通知。这个合理的值,由对应的 RFC 规范定义。
    • 接收方通常的策略如下:

      • 当「窗口大小」小于 min( MSS,缓存空间/2 ) ,也就是小于 MSS 与 1/2 缓存大小中的最小值时,就会向发送方通告窗口为 0,也就阻止了发送方再发数据过来。

      • 等到接收方处理了一些数据后,窗口大小 >= MSS,或者接收方缓存空间有一半可以使用,就可以把窗口打开让发送方发送数据过来。

发送端引起的糊涂窗口综合症:(发送端发送了大量的小数据)

  • 场景:

    • 交互式场景中,比如我们使用telnet登录到一台服务器上,或者使用SSH和远程的服务器交互,这种情况下,我们在屏幕上敲打了一个命令,等待服务器返回结果,这个结构需要不断和服务端进行数据传输。
    • 这里最大的问题是,每次传输的数据可能都非常小,比如敲打的命令“pwd”,仅仅三个字符。
    • 这意味着什么?这就好比,每次叫了一辆大货车,只送了一个小水壶。在这种情况下,发送端该怎么做才合理呢?
  • 怎么让发送方避免发送小数据呢?

    • 使用Nagle算法,该算法的思路是延时处理,它满足以下两个条件中的一条才可以发送数据:

      • 要等到窗口大小 >= MSS 或是 数据大小 >= MSS
      • 收到之前发送数据的 ack 回包
    • 只要没满足上面条件中的一条,发送方一直在囤积数据,直到满足上面的发送条件。
    • nagle算法的本质其实就是限制大批量的小数据包同时发送,为此,它提出,在任何一个时刻,未被确认的小数据包不能超过一个。这里的小数据包,指的是长度小于最大报文段长度MSS的TCP分组。
    • 是为了减少广域网的小分组数目,从而减少网络拥塞的出现;
    • 另外,Nagle 算法默认是打开的,如果对于一些需要小数据包交互的场景的程序,比如,telnet 或 ssh 这样的交互性比较强的程序,则需要关闭 Nagle 算法(对时延敏感的应用不适合nagle算法)。
    • 可以在 Socket 设置 TCP_NODELAY 选项来关闭这个算法(关闭 Nagle 算法没有全局参数,需要根据每个应用自己的特点来关闭)
setsockopt(sock_fd, IPPROTO_TCP, TCP_NODELAY, (char *)&value, sizeof(int));

延迟ACK VS nagle算法

小数据包加剧了网络带宽的浪费,为了解决这个问题,引入了如 Nagle 算法、延时 ACK 等机制。

Nagle算法:

  • 是为了减少广域网的小分组数目,从而减少网络拥塞的出现;
  • 该算法要求一个tcp连接上最多只能有一个未被确认的未完成的小分组,在该分组ack到达之前不能发送其他的小分组,tcp需要收集这些少量的分组,并在ack到来时以一个分组的方式发送出去;其中小分组的定义是小于MSS的任何分组;
  • 该算法的优越之处在于它是自适应的,确认到达的越快,数据也就发送的越快;而在希望减少微小分组数目的低速广域网上,则会发送更少的分组;

延迟ACK:

  • 如果tcp对每个数据包都发送一个ack确认,那么只是一个单独的数据包为了发送一个ack代价比较高,所以tcp会延迟一段时间,如果这段时间内有数据发送到对端,则捎带发送ack,如果在延迟ack定时器触发时候,发现ack尚未发送,则立即单独发送;
  • 好处:
    • 避免糊涂窗口综合症;
    • 发送数据的时候将ack捎带发送,不必单独发送ack;
    • 如果延迟时间内有多个数据段到达,那么允许协议栈发送一个ack确认多个报文段;

当Nagle遇上延迟ACK:

举个例子:

  • 比如,客户端分两次将一个请求发送出去,由于请求的第一部分的报文未被确认,Nagle 算法开始起作用;同时延时 ACK 在服务器端起作用,假设延时时间为 200ms,服务器等待 200ms 后,对请求的第一部分进行确认;接下来客户端收到了确认后,Nagle 算法解除请求第二部分的阻止,让第二部分得以发送出去,服务器端在收到之后,进行处理应答,同时将第二部分的确认捎带发送出去。

  • 从上图可以看到,Nagle 算法和延时确认组合在一起,增大了处理时延,实际上,两个优化彼此在阻止对方。

也就是说,有些情况下Nagle算法并不适用,比如对时延敏感的应用。

幸运的是,我们可以通过对套接字的修改来关闭Nale算法

int on = 1;
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void *)&on, sizeof(on));

值得注意的是,除非我们对此有十足的把握,否则不要轻易改变默认的TCP Nagle算法。因为在现代操作系统中,针对Nagle算法和延迟ACK的优化已经非常成熟了,有可能在禁用了Nagle算法之后,性能问题反而更加严重。

窗口关闭

TCP通过让接收方指明希望从发送方接收的数据大小(窗口大小)来进行流量控制。

如果窗口大小为0时,就会阻止发送方给接收方传递数据,知道窗口变为非0为止,这就是窗口关闭

举个例子:

流量控制机制中,在对于包的确认时,同时会携带一个窗口的大小。

我们先假设窗口不变的情况下,窗口始终是9。4的确认来的时候,会右移一个,这个使得第(4-9)19个包也可以发送了

这个时候,如果发送的比较猛,将第三部分的10、11、12、13全部发送完毕,之后就停止发送了,未发送可发送部分为0

当对于包5的确认到达时,在客户端相当于窗口再滑动了一格,这个时候,才可以有更多的包可以发送了,比如第14个包才可以发送。

如果接收方实在处理得太慢,导致缓存中没有空间了,可以通过确认信息修改窗口的大小,甚至可以设置为0,则发送方将暂时停止发送。

我们假设一个极端情况,接收方的应用一直不读取缓存中的数据,当数据报6确认后,窗口大小就不能再是9了,就要缩小变为8


这个新的窗口8通过6的确认消息到达发送端的时候,你会发现窗口并没有平行左移,而是仅仅左面的边右移了,窗口的大小从9改成了8

如果接收端还是一直不处理数据的话,则随着确认的包越来越多,窗口将越来越小,直到为0

当这个窗口通过包14的确认到达发送端的时候,发送端的窗口也调整为0,停止发送。

窗口关闭潜在的危险:死锁

接收方向发送方通告窗口大小时,是通过 ACK 报文来通告的。

那么,当发生窗口关闭时,接收方处理完数据后,会向发送方通告一个窗口非 0 的 ACK 报文,如果这个通告窗口的 ACK 报文在网络中丢失了,那麻烦就大了。

这会导致发送方一直等待接收方的非 0 窗口通知,接收方也一直等待发送方的数据,如不采取措施,这种相互等待的过程,会造成了死锁的现象。

如何解决窗口关闭时,潜在的死锁现象呢?

为了解决这个问题,

  • TCP为每个连接设有一个持续定时器,只要TCP任意一方收到对方的零窗口通知,就会启动持续定时器
  • 如果持续定时器到了,就会发送窗口探测报文(Window probe),而对方在确认这个探测报文时,给出自己现在的接收窗口大小
  • 如果接收窗口仍然为 0,那么收到这个报文的一方就会重新启动持续计时器;
  • 如果接收窗口不是 0,那么死锁的局面就可以被打破了。

窗口探测的次数一般为 3 次,每次大约 30-60 秒(不同的实现可能会不一样)。如果 3 次过后接收窗口还是 0 的话,有的 TCP 实现就会发 RST 报文来中断连接

socket编程时,如何应对小数据包的传输

  • 小数据包加剧了网络带宽的浪费,为了解决这个问题,引入了如 Nagle 算法、延时 ACK 等机制。
  • 在程序设计方面,不要多次频繁的发送小报文,如果有,可以使用writev批量发送

禁用 Nagle 算法

将写操作合并

我们可以将一个请求一次性发送出去,而不是分开两部分独立发送

所以,在写数据之前,将数据合并到缓冲区,批量发送出去,这是一个比较好的做法。

不过,有时候数据会存储在两个不同的缓存中,对此,我们可以使用如下的方法来进行数据的读写操作,从而避免Nagle算法引发的副作用

ssize_t writev(int filedes, const struct iovec *iov, int iovcnt)
ssize_t readv(int filedes, const struct iovec *iov, int iovcnt);

这两个函数的第二个参数都是指向某个 iovec 结构数组的一个指针,其中 iovec 结构定义如下:

struct iovec {void *iov_base; /* starting address of buffer */
size_t iov_len; /* size of buffer */
};”

在调用 writev 操作时,会自动把几个数组的输入合并成一个有序的字节流,然后发送给对端

总结

谈谈你对滑动窗口的了解?

  • TCP每发送一个数据,都要收到一个确认应答。并且序列号是递增的。当上一个数据包收到应答,再发送下一个。这样效率很低。于是,引入了窗口的概念,窗口大小,就是一次能并行发送消息的最大值。
  • 相应的序列号应答,即使中间有丢失,收到最后的应答,也认为是成功接收所有数据了,这种模式就是累计确认累计应答
  • 窗口实际上是操作系统开辟的缓存区域,发送方的叫发送窗口,接收方的叫接收窗口。发送窗口应该小于等于接收窗口。
  • 发送方,依据发送窗口发送数据,接收方接收到数据,放在接收窗口中,此时接收窗口减小,接收方返回window字段,告诉发送方,接收窗口大小,发送窗口减小。两端的窗口不断变化,这个就是滑动窗口的机制。
  • 滑动窗口的大小意味着接收方还有多大的缓冲区可以用于接收数据。发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。
  • 当滑动窗口为 0 时,发送方一般不能再发送数据报,但有两种情况除外,一种情况是可以发送紧急数据,另一种情况是发送方可以发送一个 1 字节的数据报来通知接收方重新声明它希望接收的下一字节及发送方的滑动窗口大小。

TCP报文的头部中有两个字段,一个是ACK确认号,一个是windows窗口值,在每次数据传递过程中,收发双方都会传递这两个字段,ACK表示对方已经对小于ACK-1的报文进行确认,发送方能够以此为动态调整窗口的左边界,windows窗口表示接收方能够接收的数据大小,发送方能够根据此动态调整窗口的右边界,这就形成了窗口的向前滑动,所以就叫做滑动窗口

什么是流量控制?为什么要流量控制?

TCP 利用滑动窗口实现流量控制的机制。

  • 连接的算法即是发送端也是接收端,它们会各自维护一个发送窗口和接收窗口。接收窗口大小取决于系统、软件、网络等,发送窗口大小取决于接收窗口大小。
  • 接收窗口是用来做拥塞控制的,避免把网络塞满,它会根据实际情况调整自己的大小,以告知发送窗口自己还能处理多少数据
  • 发送窗口总是小于等于接收窗口的,它会根据接收窗口的大小来调整自己发送速率,以达到流量控制的目的
  • 如果不做流量控制,可能会出现接收缓冲区爆满了,比如糊涂窗口政,导致网络传输效率低下

流量控制引发的死锁?怎么避免死锁的发生?

  • 当发送者收到了一个窗口为0的应答,发送者便停止发送,等待接收者的下一个应答。但是如果这个窗口不为0的应答在传输过程丢失,发送者一直等待下去,而接收者以为发送者已经收到该应答,等待接收新数据,这样双方就相互等待,从而产生死锁。
  • 为了避免流量控制引发的死锁,TCP使用了持续计时器。每当发送者收到一个零窗口的应答后就启动该计时器。时间一到便主动发送报文询问接收者的窗口大小。若接收者仍然返回零窗口,则重置该计时器继续等待;若窗口不为0,则表示应答报文丢失了,此时重置发送窗口后开始发送,这样就避免了死锁的产生。

糊涂窗口综合症?怎么解决?

什么是糊涂窗口综合征?

  • 接收方太忙,无时间处理接收的数据,并不断返回窗口大小,导致发送窗口越来越小。
  • 发送窗口也会继续发送几个字节的数据,导致流量浪费,因为TCP + IP头就有40个字节的数据,发送几个字节,非常不经济。

怎么解决?

  • 让接收方不通告小窗口给发送方:

    • 当「窗口大小」小于 min( MSS,缓存空间/2 ) ,也就是小于 MSS 与 1/2 缓存大小中的最小值时,就会向发送方通告窗口为 0,也就阻止了发送方再发数据过来。
    • 等到接收方处理了一些数据后,窗口大小 >= MSS,或者接收方缓存空间有一半可以使用,就可以把窗口打开让发送方发送数据过来。
  • 让发送方避免发送小数据:使用Nagle算法:
    • 使用 Nagle 算法,该算法的思路是延时处理,它满足以下两个条件中的一条才可以发送数据:

      • 要等到窗口大小 >= MSS 或是 数据大小 >= MSS
      • 收到之前发送数据的 ACK回包
    • 只要没满足上面条件中的一条,发送方一直在囤积数据,直到满足上面的发送条件。
    • 另外,Nagle 算法默认是打开的,如果对于一些需要小数据包交互的场景的程序,比如,telnet 或 ssh 这样的交互性比较强的程序,则需要关闭 Nagle 算法。

发送窗口和拥塞窗口的区别?

在任何一个时刻,TCP发送缓冲区的数据能否真正发送出去,至少取决于两个因素,一个是当前的发送窗口大小,另一个是拥塞窗口大小,而TCP协议中总是取两者中最小值作为判断依据,比如当前发送的字节为 100,发送窗口的大小是 200,拥塞窗口的大小是 80,那么取 200 和 80 中的最小值,就是 80,当前发送的字节数显然是大于拥塞窗口的,结论是不能发送出去。

这里千万要分清楚发送窗口和拥塞窗口的区别:

  • 发送窗口反应了作为单TCP连接、点对点之间的流量控制模型,它是需要和接收端一起共同协调来调整大小的
  • 拥塞窗口反应了作为多个TCP连接共享带宽的拥塞控制模型,它是发送端独立的根据网络状况来动态调整的

如何实现流量控制

由滑动窗口协议(连续ARQ协议)实现。滑动窗口协议既保证了分组无差错,有序接收,也实现了流量控制。主要方式就是计数法返回的ACK中包含自己的接收窗口的大小,并且利用大小来控制发送方的数据发送。

网络:TCP的滑动窗口与流量控制和拥塞控制相关推荐

  1. 计算机网络传输层(tcp滑动窗口与流量控制、拥塞控制)

    ④ TCP的滑动窗口 TCP的滑动窗口是以字节为单位的,是缓存的一部分,用来暂时存放字节流. 为了便于理解,我们只考虑A向B发送数据,B给出确认的场景.即A有发送窗口,B有接收窗口. 当发送方收到接收 ...

  2. 你还在为 TCP 重传、滑动窗口、流量控制、拥塞控制发愁吗?看完图解就不愁了...

    每日一句英语学习,每天进步一点点: 来自:小林coding 前言 前一篇「硬不硬你说了算!近 40 张图解被问千百遍的 TCP 三次握手和四次挥手面试题」得到了很多读者的认可,在此特别感谢你们的认可, ...

  3. 图解TCP 的重传、滑动窗口、流量控制和拥塞控制机制

    每日一句英语学习,每天进步一点点: 前言 前一篇「硬不硬你说了算!近 40 张图解被问千百遍的 TCP 三次握手和四次挥手面试题」得到了很多读者的认可,在此特别感谢你们的认可,大家都暖暖的. 来了,今 ...

  4. java tcp权限控制_「图解」TCP重传、滑动窗口、流量控制、拥塞控制

    前言 前一篇35 张图解被问千百遍的 TCP 三次握手和四次挥手面试题得到了很多读者的认可,在此特别感谢你们的认可,大家都暖暖的. 来了,今天又来图解 TCP 了,小林可能会迟到,但不会缺席. 迟到的 ...

  5. tcp下载窗口太小的问题_图解 TCP 重传、滑动窗口、流量控制、拥塞控制

    相信大家都知道 TCP 是一个可靠传输的协议,那如何它是如何保证可靠的呢? 为了实现可靠性传输,需要考虑很多事情,例如数据的破坏.丢包.重复以及分片顺序混乱等问题.如不能解决这些问题,也就无从谈起可靠 ...

  6. TCP 重传、滑动窗口、流量控制、拥塞控制★★★

    正文 相信大家都知道 TCP 是一个可靠传输的协议,那它是如何保证可靠的呢? 为了实现可靠性传输,需要考虑很多事情,例如数据的破坏.丢包.重复以及分片顺序混乱等问题.如不能解决这些问题,也就无从谈起可 ...

  7. TCP 是一个可靠传输的协议,那我们来重点介绍 TCP 的重传机制、滑动窗口、流量控制、拥塞控制。

    TCP 巨复杂,它为了保证可靠性,用了巨多的机制来保证,真是个「伟大」的协议,写着写着发现这水太深了... 本文的全部图片都是小林绘画的,非常的辛苦且累,不废话了,直接进入正文,Go! 相信大家都知道 ...

  8. 30 张图解: 面试必问的 TCP 重传、滑动窗口、流量控制、拥塞控制

    前言 前一篇「硬不硬你说了算!近 40 张图解被问千百遍的 TCP 三次握手和四次挥手面试题」得到了很多读者的认可,在此特别感谢你们的认可,大家都暖暖的. 来了,今天又来图解 TCP 了,小林可能会迟 ...

  9. 计算机网络:TCP滑动窗口的流量控制和拥塞控制

    1. 前言 最近在研究网络通信底层通信原理,所以不得不复习一波计算机网络传输控制协议.那么对于程序开发人员,了解底层网络通信原理,对于我们理解BIO.NIO网络通信十分重要.所以对于程序开发人员来说, ...

最新文章

  1. 使用PowerDesigner做数据库设计(二)
  2. 机器学习在销售报价单的产品推荐场景中的作用
  3. Spring MVC 5 + Thymeleaf 基于Java配置和注解配置
  4. Flask之WTForms
  5. android的动画实例
  6. Oracle 索引相关
  7. HTTP和HTTPS的区别是什么?
  8. Ubuntu服务器宕机排查记录
  9. _IO, _IOR, _IOW, _IOWR 宏的用法与解析
  10. linux的ssh漏洞,Debian GNU/Linux Rssh安全绕过漏洞
  11. AndroidStudio 抓包工具Profiler使用
  12. 2016年APP推广应该怎么做?
  13. R语言绘制韦布尔分布图和泊松(Poisson)分布图,并为二项分布(泊松分布)绘制不同颜色
  14. stm32f103r6最小系统原理图_stm32f103c8t6封装及最小系统原理图
  15. 人生法则:蝴蝶效应、青蛙现象、鳄鱼法则、鲇鱼效应、羊群效应、刺猬法则...
  16. 超详细的fiddler教程,从小白到精通(六)❤️
  17. 硬币(Leetcode)
  18. java使用字符流读取文件
  19. 淘宝小二腐败案,到底谁是黑幕?
  20. python中计算区间内的质子数

热门文章

  1. 工控系统设计(八)组态功能开发
  2. 浏览器 unload beforunload事件不触发
  3. jQuery学习理解(详细)
  4. android usb卸载不了,Android 安全卸载U盘的方法
  5. 年度绩效考核演示PPT模板
  6. 错题本——Python
  7. 群里关于一个硬件电路的讨论,纹波大导致烧坏主芯片
  8. 二进制如何转十进制,十进制如何转二进
  9. 为什么使用计算机辅助翻译工具中文译文,TCloud计算机辅助翻译工具
  10. limits.conf 配置不生效问题排查