第12讲 | TCP协议(下):西行必定多妖孽,恒心智慧消磨难

如何做个靠谱的人?

有问有答,任务发送和接收有记录,完成有反馈。

如何实现一个靠谱的协议?

TCP 协议使用的也是同样的模式。为了保证顺序性,每一个包都有一个 ID。在建立连接的时候,会商定起始的 ID 是什么,然后按照 ID 一个个发送。为了保证不丢包,对于发送的包都要进行应答,但是这个应答也不是一个一个来的,而是会应答某个之前的 ID,表示都收到了,这种模式称为累计确认或者累计应答(cumulative acknowledgment)。

TCP的应答方式:累计应答或累计确认,目的是节省时间,提高效率!

  为了记录所有发送的包和接收的包,TCP 也需要发送端和接收端分别都有缓存来保存这些记录。发送端的缓存里是按照包的 ID 一个个排列,根据处理的情况分成四个部分。

  第一部分:发送了并且已经确认的。这部分就是你交代下属的,并且也做完了的,应该划掉的。

  第二部分:发送了并且尚未确认的。这部分是你交代下属的,但是还没做完的,需要等待做完的回复之后,才能划掉。

  第三部分:没有发送,但是已经等待发送的。这部分是你还没有交代给下属,但是马上就要交代的。

  第四部分:没有发送,并且暂时还不会发送的。这部分是你还没有交代给下属,而且暂时还不会交代给下属的。

  “流量控制,把握分寸”。作为项目管理人员,你应该根据以往的工作情况和这个员工反馈的能力、抗压力等,先在心中估测一下,这个人一天能做多少工作。如果工作布置少了,就会不饱和;如果工作布置多了,他就会做不完;如果你使劲逼迫,人家可能就要辞职了。到底一个员工能够同时处理多少事情呢?

  在 TCP 里,接收端会给发送端报一个窗口的大小,叫 Advertised window。这个窗口的大小应该等于上面的第二部分加上第三部分,就是已经交代了没做完的加上马上要交代的。超过这个窗口的,接收端做不过来,就不能发送了。

  于是,发送端需要保持下面的数据结构。

  LastByteAcked:第一部分和第二部分的分界线

  LastByteSent:第二部分和第三部分的分界线

  LastByteAcked + AdvertisedWindow:第三部分和第四部分的分界线对于接收端来讲,它的缓存里记录的内容要简单一些。

  第一部分:接受并且确认过的。也就是我领导交代给我,并且我做完的。

  第二部分:还没接收,但是马上就能接收的。也即是我自己的能够接受的最大工作量。

  第三部分:还没接收,也没法接收的。也即超过工作量的部分,实在做不完。对应的数据结构就像这样。

  MaxRcvBuffer:最大缓存的量;LastByteRead 之后是已经接收了,但是还没被应用层读取的;

  NextByteExpected 是第一部分和第二部分的分界线。第二部分的窗口有多大呢?‘

  NextByteExpected 和 LastByteRead 的差其实是还没被应用层读取的部分占用掉的 MaxRcvBuffer 的量,我们定义为 A。

  AdvertisedWindow 其实是 MaxRcvBuffer 减去 A。也就是:AdvertisedWindow=MaxRcvBuffer-((NextByteExpected-1)-LastByteRead)。

那第二部分和第三部分的分界线在哪里呢?NextByteExpected 加 AdvertisedWindow 就是第二部分和第三部分的分界线,其实也就是 LastByteRead 加上 MaxRcvBuffer。

其中第二部分里面,由于受到的包可能不是顺序的,会出现空档,只有和第一部分连续的,可以马上进行回复,中间空着的部分需要等待,哪怕后面的已经来了。顺序问题与丢包问题

顺序问题与丢包问题

接下来我们结合一个例子来看。还是刚才的图,

  在发送端来看,1、2、3 已经发送并确认;4、5、6、7、8、9 都是发送了还没确认;10、11、12 是还没发出的;13、14、15 是接收方没有空间,不准备发的。  

  在接收端来看,1、2、3、4、5 是已经完成 ACK,但是没读取的;6、7 是等待接收的;8、9 是已经接收,但是没有 ACK 的。

  发送端和接收端当前的状态如下:  

  1、2、3 没有问题,双方达成了一致。

  4、5 接收方说 ACK 了,但是发送方还没收到,有可能丢了,有可能在路上。  

  6、7、8、9 肯定都发了,但是 8、9 已经到了,但是 6、7 没到,出现了乱序,缓存着但是没办法 ACK。

根据这个例子,我们可以知道,顺序问题和丢包问题都有可能发生,所以我们先来看确认与重发的机制。

  假设 4 的确认到了,不幸的是,5 的 ACK 丢了,6、7 的数据包丢了,这该怎么办呢?

  一种方法就是超时重试,也即对每一个发送了,但是没有 ACK 的包,都有设一个定时器,超过了一定的时间,就重新尝试。但是这个超时的时间如何评估呢?这个时间不宜过短,时间必须大于往返时间 RTT,否则会引起不必要的重传。也不宜过长,这样超时时间变长,访问就变慢了。

  估计往返时间,需要 TCP 通过采样 RTT 的时间,然后进行加权平均,算出一个值,而且这个值还是要不断变化的,因为网络状况不断地变化。除了采样 RTT,还要采样 RTT 的波动范围,计算出一个估计的超时时间。由于重传时间是不断变化的,我们称为自适应重传算法(Adaptive Retransmission Algorithm)。

  如果过一段时间,5、6、7 都超时了,就会重新发送。接收方发现 5 原来接收过,于是丢弃 5;6 收到了,发送 ACK,要求下一个是 7,7 不幸又丢了。当 7 再次超时的时候,有需要重传的时候,TCP 的策略是超时间隔加倍。每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。

有一个可以快速重传的机制,当接收方收到一个序号大于下一个所期望的报文段时,就会检测到数据流中的一个间隔,于是它就会发送冗余的 ACK,仍然 ACK 的是期望接收的报文段。而当客户端收到三个冗余的 ACK 后,就会在定时器过期之前,重传丢失的报文段。

  例如,接收方发现 6 收到了,8 也收到了,但是 7 还没来,那肯定是丢了,于是发送 6 的 ACK,要求下一个是 7。接下来,收到后续的包,仍然发送 6 的 ACK,要求下一个是 7。当客户端收到 3 个重复 ACK,就会发现 7 的确丢了,不等超时,马上重发。

  还有一种方式称为 Selective Acknowledgment  (SACK)。这种方式需要在 TCP 头里加一个 SACK 的东西,可以将缓存的地图发送给发送方。例如可以发送 ACK6、SACK8、SACK9,有了地图,发送方一下子就能看出来是 7 丢了。

流量控制问题

我们再来看流量控制机制,在对于包的确认中,同时会携带一个窗口的大小。我们先假设窗口不变的情况,窗口始终为 9。4 的确认来的时候,会右移一个,这个时候第 13 个包也可以发送了。

这个时候,假设发送端发送过猛,会将第三部分的 10、11、12、13 全部发送完毕,之后就停止发送了,未发送可发送部分为 0。

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

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

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

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

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

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

.

如果这样的话,发送方会定时发送窗口探测数据包,看是否有机会调整窗口的大小。当接收方比较慢的时候,要防止低能窗口综合征,别空出一个字节来就赶快告诉发送方,然后马上又填满了,可以当窗口太小的时候,不更新窗口,直到达到一定大小,或者缓冲区一半为空,才更新窗口。这就是我们常说的流量控制。

拥塞控制问题

  最后,我们看一下拥塞控制的问题,也是通过窗口的大小来控制的,前面的滑动窗口 rwnd 是怕发送方把接收方缓存塞满,而拥塞窗口 cwnd,是怕把网络塞满。

  这里有一个公式 LastByteSent - LastByteAcked <= min {cwnd, rwnd} ,是拥塞窗口和滑动窗口共同控制发送的速度。

  那发送方怎么判断网络是不是慢呢?这其实是个挺难的事情,因为对于 TCP 协议来讲,他压根不知道整个网络路径都会经历什么,对他来讲就是一个黑盒。TCP 发送包常被比喻为往一个水管里面灌水,而 TCP 的拥塞控制就是在不堵塞,不丢包的情况下,尽量发挥带宽。水管有粗细,网络有带宽,也即每秒钟能够发送多少数据;水管有长度,端到端有时延。在理想状态下,水管里面水的量 = 水管粗细 x 水管长度。对于到网络上,通道的容量 = 带宽 × 往返延迟。如果我们设置发送窗口,使得发送但未确认的包为为通道的容量,就能够撑满整个管道。

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

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

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

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

  于是 TCP 的拥塞控制主要来避免两种现象,包丢失和超时重传。一旦出现了这些现象就说明,发送速度太快了,要慢一点。但是一开始我怎么知道速度多快呢,我怎么知道应该把窗口调整到多大呢?

  如果我们通过漏斗往瓶子里灌水,我们就知道,不能一桶水一下子倒进去,肯定会溅出来,要一开始慢慢的倒,然后发现总能够倒进去,就可以越倒越快。这叫作慢启动。

  一条 TCP 连接开始,cwnd 设置为一个报文段,一次只能发送一个;当收到这一个确认的时候,cwnd 加一,于是一次能够发送两个;当这两个的确认到来的时候,每个确认 cwnd 加一,两个确认 cwnd 加二,于是一次能够发送四个;当这四个的确认到来的时候,每个确认 cwnd 加一,四个确认 cwnd 加四,于是一次能够发送八个。可以看出这是指数性的增长。

  涨到什么时候是个头呢?有一个值 ssthresh 为 65535 个字节,当超过这个值的时候,就要小心一点了,不能倒这么快了,可能快满了,再慢下来。

  每收到一个确认后,cwnd 增加 1/cwnd,我们接着上面的过程来,一次发送八个,当八个确认到来的时候,每个确认增加 1/8,八个确认一共 cwnd 增加 1,于是一次能够发送九个,变成了线性增长。但是线性增长还是增长,还是越来越多,直到有一天,水满则溢,出现了拥塞,这时候一般就会一下子降低倒水的速度,等待溢出的水慢慢渗下去。

  拥塞的一种表现形式是丢包,需要超时重传,这个时候,将 sshresh 设为 cwnd/2,将 cwnd 设为 1,重新开始慢启动。这真是一旦超时重传,马上回到解放前。但是这种方式太激进了,将一个高速的传输速度一下子停了下来,会造成网络卡顿。  

  前面我们讲过快速重传算法。当接收端发现丢了一个中间包的时候,发送三次前一个包的 ACK,于是发送端就会快速地重传,不必等待超时再重传。TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,cwnd 减半为 cwnd/2,然后 sshthresh = cwnd,当三个包返回的时候,cwnd = sshthresh + 3,也就是没有一夜回到解放前,而是还在比较高的值,呈线性增长。

就像前面说的一样,正是这种知进退,使得时延很重要的情况下,反而降低了速度。

但是如果你仔细想一下,TCP 的拥塞控制主要来避免的两个现象都是有问题的。

第一个问题是丢包并不代表着通道满了,也可能是管子本来就漏水。例如公网上带宽不满也会丢包,这个时候就认为拥塞了,退缩了,其实是不对的。

第二个问题是 TCP 的拥塞控制要等到将中间设备都填充满了,才发生丢包,从而降低速度,这时候已经晚了。其实 TCP 只要填满管道就可以了,不应该接着填,直到连缓存也填满。

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

小结

顺序问题、丢包问题、流量控制都是通过滑动窗口来解决的,这其实就相当于你领导和你的工作备忘录,布置过的工作要有编号,干完了有反馈,活不能派太多,也不能太少;

拥塞控制是通过拥塞窗口来解决的,相当于往管道里面倒水,快了容易溢出,慢了浪费带宽,要摸着石头过河,找到最优值。

最后留两个思考题:

TCP 的 BBR 听起来很牛,你知道他是如何达到这个最优点的嘛?

学会了 UDP 和 TCP,你知道如何基于这两种协议写程序吗?这样的程序会有什么坑呢?

1 设备缓存会导致延时?
假如经过设备的包都不需要进入缓存,那么得到的速度是最快的。进入缓存且等待,等待的时间就是额外的延时。BBR就是为了避免这些问题:
充分利用带宽;降低buffer占用率。

2 降低发送packet的速度,为何反而提速了?
标准TCP拥塞算法是遇到丢包的数据时快速下降发送速度,因为算法假设丢包都是因为过程设备缓存满了。快速下降后重新慢启动,整个过程对于带宽来说是浪费的。通过packet速度-时间的图来看,从积分上看,BBR充分利用带宽时发送效率才是最高的。可以说BBR比标准TCP拥塞算法更正确地处理了数据丢包。对于网络上有一定丢包率的公网,BBR会更加智慧一点。
回顾网络发展过程,带宽的是极大地改进的,而最小延迟会受限与介质传播速度,不会明显减少。BBR可以说是应运而生。

3 BBR如何解决延时?
S1:慢启动开始时,以前期的延迟时间为延迟最小值Tmin。然后监控延迟值是否达到Tmin的n倍,达到这个阀值后,判断带宽已经消耗尽且使用了一定的缓存,进入排空阶段。
S2:指数降低发送速率,直至延迟不再降低。这个过程的原理同S1
S3:协议进入稳定运行状态。交替探测带宽和延迟,且大多数时间下都处于带宽探测阶段。

第12讲 | TCP协议(下):西行必定多妖孽,恒心智慧消磨难的更多相关文章

  1. TCP协议下大数据传输IOCP乱序问题

    毕业后稀里糊涂的闭门造车了两年,自己的独立博客也写了两年,各种乱七八糟,最近准备把自己博客废了,现在来看了下这两年写的对我来说略微有点意义的文章只此一篇,转载过来以作留念. 写的很肤浅且凌乱,请见谅. ...

  2. TCP协议下的服务端并发,GIL全局解释器锁,死锁,信号量,event事件,线程q

    TCP协议下的服务端并发,GIL全局解释器锁,死锁,信号量,event事件,线程q 一.TCP协议下的服务端并发 ''' 将不同的功能尽量拆分成不同的函数,拆分出来的功能可以被多个地方使用 TCP服务 ...

  3. 第11讲 | TCP协议(上):因性恶而复杂,先恶后善反轻松

    第11讲 | TCP协议(上):因性恶而复杂,先恶后善反轻松 TCP 包头格式 我们先来看 TCP 头的格式.从这个图上可以看出,它比 UDP 复杂得多. 首先,源端口号和目标端口号是不可少的,这一点 ...

  4. tcp协议下的Socket

    import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net ...

  5. 基于tcp协议下粘包现象和解决方案,socketserver

    一.缓冲区 每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区.write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送 ...

  6. TCP协议下Socket的基础编程类型

    套接字的基本操作有: 创建(socket).命名(bind).侦听(listen).连接(accept).关闭(shutdown).发送(send).接受(recv). 下面逐个分析: 一.创建(so ...

  7. 《java入门第一季》之tcp协议下的网络编程

    tcp协议相对于udp更加安全. 首先看一下需求:服务器端开启,多个客户端同时向服务器发送数据,看哪个客户端先到达. 说明:这里我开启三个电脑实验,一台电脑写服务器端的程序,两台电脑开客户端的程序.服 ...

  8. 在TCP协议下的数据传送

    本人小白菜逼一枚,,,,刚建立博客,也写不了太深入的,就写点上课的笔记什么的.有错误希望广大博友指出,我一定虚心学习接收改正. 我的新浪邮箱:liudaohui0805@sina.com 我的QQ邮箱 ...

  9. tcp协议下粘包问题的产生及解决方案

    1 tcp有粘包及udp无粘包 - TCP 是面向连接的,面向流的可靠协议:发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据, 合并成 ...

随机推荐

  1. Go-31-杂序

    标识符: 在go语言里,标识符要么从包里公开,要么不从包里公开. 当代码导入一个包时,程序可以直接访问这个包中任意一个公开的标识符.这些标识符以大写字母开头.以小写字母开头的标识符是不公开的,不能被其 ...

  2. Salesforce学习之路(四)利用Jenkins和Git实现Salesforce的CI/CD功能

    上文提到,基于CRM的二次开发是必不可少的,但是在实际项目中CI/CD是不可忽略的一个重要部分,与传统的Java,Python项目不同,如果对Salesforce进行持续集成和持续部署呢? 结合找到的 ...

  3. red and black(BFS)

    Red and Black Time Limit: 1000MS   Memory Limit: 30000K Total Submissions: 40685   Accepted: 22079 D ...

  4. Thinkphp5助手函数和Thinkphp3的单字母函数对应参照表

  5. hdu1556 线段树段更新(简单题)

    题意: N个气球排成一排,从左到右依次编号为1,2,3....N.每次给定2个整数a b(a <= b),lele便为骑上他的"小飞鸽"牌电动车从气球a开始到气球b依次给每个 ...

  6. POJ3122贪心或者二分(分蛋糕)

    题意:        m+1个人来分n个蛋糕,每个人分到的蛋糕数必须一样而且还必须是同一个蛋糕上的,问每个人最多分多少蛋糕? 思路:      能想到的方法有两种,一个是直接贪心,另一个就是二分,这个 ...

  7. Python脚本扫描给定网段的MAC地址表(scapy或 python-nmap)

    目录 用scapy模块写 用 python-nmap 模块写 python3.7  windows环境 以下两个都可以扫描指定主机或者指定网段的 IP 对应的 MAC 地址,然后保存到 csv 文件中 ...

  8. Burpsuite工具的使用

    目录 Burpsuite Proxy代理模块 Repeater模块(改包,重放) Intruder模块(爆破) Target模块 position模块 Payloads模块 Options模块 一处爆 ...

  9. Windows核心编程 第十五章 在应用程序中使用虚拟内存

    第1 5章 在应用程序中使用虚拟内存 Wi n d o w s提供了3种进行内存管理的方法,它们是: • 虚拟内存,最适合用来管理大型对象或结构数组. • 内存映射文件,最适合用来管理大型数据流(通常 ...

  10. 第三部分 IDEA创建并运行项目

    可以创建一个maven,几行代码就解决了导入依赖,但是我的电脑不知道哪里出现了问题,IDEA重装,jdk重装,maven重装,都无法解决问题,找了3天,还是没有解决问题.最后只能采用手动导入包方法.看 ...