网络协议1-网络协议概述2-IP是怎么来的,为什么消失了?网络协议3-从物理层到MAC层网络协议4-交换机和VLAN:办公室太复杂,我要回学校了网络协议5-ICMP和ping:问路的童子军网络协议6-路由协议:敢问路在何方?网络协议7——UDP协议:善良的本性可以在城市中发挥网络协议8——TCP协议(上):邪恶的本性需要深套路?上次我们了解了TCP连接建立和断开的过程,发现TCP会通过通过各种“套路”保证传输数据的安全。另外,我们也对TCP头格式解决的五个问题有了一个大概的了解:顺序问题、丢包问题、连接维护、流量控制、拥塞控制。今天,我们就来看看TCP是如何使用来解决这五个问题的。在解决问题之前,我们先来看看TCP是如何成为一个“可靠”的协议的。“可靠”协议TCPTCP为了保证顺序,每个数据包都有一个ID。当连接建立时,会约定起始ID的值,然后根据ID一个一个发送。为了保证不丢包,所有发送的包都必须得到应答。但是这个响应并不是一个一个来的,而是会响应之前的某个ID,表示已经收到了。这种模式称为累积确认和累积响应。为了记录所有发送的数据包和接收的数据包,TCP还要求发送端和接收端使用缓冲区来保存这些记录。sender的缓存是按照packetID一个一个排列的,按照处理情况分为四个部分:第一部分:发送和确认;第二部分:已发送但尚未确认;第三部分:未发送,但一直等待发送;第四部分:未发送,暂时不会发送。所以发送端需要维护这样一个数据结构:LastByteAcked:第一部分和第二部分的分界线LastByteSent:第二部分和第三部分的分界线第四部分对于接收端而言,其缓存记录的内容比较简单,分为以下三部分:第一部分:接收并确认;第二部分:还没有收到,但很快就能收到;没有空间接收它。对应的数据结构是这样的:MaxRcvBuffer:最大缓冲区大小;LastByteRead:这个值已经收到,但是还没有被应用层读取;NextByteExpected:第一部分和第二部分的分界线,下一个预期的数据包ID。窗口的第二部分有多大?NextByteExpected和LastByteRead的区别在于MaxRcvBuffer还没有被应用层读取的数量。我们定义为A,即:A=NextByteExpected-LastByteRead-1.那么,窗口大小,AdvertisedWindow=MaxRcvBuffer-A.即:AdvertisedWindow=MaxRcvBuffer-(NextByteExpected-LastByteRead-1)和第二个之间的分界线部分和第三部分=NextByteExpected+AdvertisedWindow-1=MaxRcvBuffer+LastByteRead。顺序和丢包问题接下来我们结合上图举例,看看TCP是如何处理顺序和丢包问题的。还是刚才的图,从发送方的角度来看: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的acknowledgement,不幸的是5的ACK丢了,6和7的数据包也丢了,这时候怎么办?一种方法是超时重试,即对于每一个发送出去但没有ACK的包,设置一个定时器,一旦超过一定的时间,就重试。超时时间不能太短,必须大于往返时间RTT,否则会造成不必要的重传,不能太长,这样超时时间变长,访问会变慢。估算往返时间需要TCP对RTT时间进行采样,然后进行加权平均计算出一个值,而且这个值还在变化,因为网络情况在不断变化。除了对RTT进行采样外,还需要对RTT的波动范围进行采样,计算出一个预计的超时时间。由于重传时间是不断变化的,我们称之为自适应重传算法。如果一段时间后,5、6、7都超时,则重新发送。接收方发现之前收到过5,于是丢弃5。收到6,发送ACK,请求下一个为7,不幸又丢了7。当7再次超时时,如果需要重传,TCP的策略是加倍超时间隔。每当遇到超时重传时,将下一个超时间隔设置为前一个值的两倍。两次超时说明网络环境差,不适合频繁发送。可以看出超时重传的问题在于超时时间可能会更长。有没有更快的方法?有一种快速重传的机制。当接收方收到一个序列号大于下一个预期段的段时,它会检测数据流中的间隙,然后发送三个冗余ACK。客户端收到后,就在定时器到期之前,重新传输丢失的段。比如接收方发现6、8、9都收到了,但是7没有来。于是发送三个6的ACK,请求下一个是7。当客户端收到三个后,发现7确实丢了,不会等超时,马上重发。除此之外,还有另一种方式称为选择性确认(SACK)。这个方法需要在TCP头中加一个SACK的东西,可以把缓存的map发送给sender。比如在发送ACK6、SACK8、SACK9的时候,有一个map,发送方可以马上看出7丢了,然后迅速重发。流量控制问题接下来我们来看一下流量控制机制。在数据包的确认中,会同时携带窗口大小字段。假设窗口不变,发送方的窗口始终为9。当4的确认到来时,LastByteAcked将向右移动一位,此时可以发送第13个数据包。此时假设发送端发送过多,把第三部分的10、11、12、13全部发送完,然后停止发送,那么此时可以发送的未发送部分为0。当确认5号包裹到达,客户端相当于多滑动了一个窗口。这时候可以发更多的包裹,比如可以发第14个包裹。如果接收方处理的太慢,缓存中没有空间,可以通过确认消息修改窗口大小,甚至设置为0,发送方会暂时停止发送。我们可以假设一种极端情况,即接收端的应用程序永远不会读取缓存中的数据。当6号数据包被确认后,窗口大小不再是9,而是减1为8。为什么变成了8?你看,下图中,当6的确认报文到达发送端时,左边的LastByteAcked右移一位,右边未发送的可发送区域已经变成0,所以左边的LastByteSend做不动。因此,窗口大小So从9变为8。如果接收端还没有处理数据,随着确认的数据包越来越多,窗口会越来越小,直到为0。当窗口大小通过确认到达发送端packet14,发送方的窗口也调整为0,所以发送方停止发送。当出现这种情况时,发送方会定期发送窗口检测包,看看是否有机会调整窗口的大小。对于接收端来说,当接收速度比较慢时,需要防止低能窗综合症。一个字节空出来的时候不要马上告诉发送方,结果又重新补上。当窗口太小时,直到达到一定大小或缓冲区半空后才更新窗口大小,才更新窗口大小。这就是我们常说的流量控制。拥塞控制问题最后我们来看一下拥塞控制的问题。windows也解决了这个问题。前面的滑动窗口rwnd是怕sender把receiver的buffer填满,而拥塞窗口cwnd是怕网络爆满。这里有一个公式:LastByteSent-LastByteAcked<=min{cwnd,rwnd}可见拥塞窗口和滑动窗口共同控制发送速度。发送方如何判断网络是否已满?这其实是相当困难的。因为对于TCP协议来说,它并不知道整个网络路径会经过什么。TCP发送数据包常被比喻为往水管里倒水,TCP拥塞控制就是在不拥塞、不丢包的情况下最大化带宽。水管有粗细,也有网络带宽,即每秒可以发送多少数据;水管有长度和端到端延迟。理想情况下:水管中的水量=水管的厚度x水管的长度对于网络来说:信道的容量=带宽x往返时延如果我们设置发送窗口这样发送但未确认的数据包的数量就是通道的容量,然后能够填满整个管道。如上图所示:假设往返时间为8s,去4s,回4s,每秒发送一个数据包,每个数据包1024字节。然后在8s之后,发送了8个数据包。其中,前4个数据包已经到达接收端,但是还没有返回ACK,不能算是发送成功。来自5-8的最后四个数据包仍在途中,尚未收到。此时,整个流水线刚好满了。在发送端,有8个数据包已经发送但未确认,即:带宽=1024byte/sx8s(往返时间)如果我们在此基础上增加窗口,使得单位时间内可以发送更多的数据包,它会发生什么?原来从一端发送一个数据包到另一端,假设一共有4台设备,每台设备处理一个数据包需要1s,所以到达另一端需要4s。如果发送速度更快,单位时间内到达这些中间设备的数据包就会更多。如果这些设备每秒只能处理一个数据包,则多余的数据包将被丢弃。这不是我们希望看到的。这个时候,我们可以想其他的办法。比如这四台设备本来每秒处理一个数据包,但是我们给这些设备加上缓存,不能处理的就放在队列里,这样数据包就不会丢了,但是缺点也很明显,这增加延迟。这个缓存包肯定不会在4s内到达接收端。如果延迟达到一定程度就会超时,这不是我们希望看到的。针对以上两种现象:丢包和超时重传。一旦出现这些现象,就说明发送速度太快了,应该慢一些。但首先,发送方如何知道它有多快?你怎么知道把窗口调整到合适的大小呢?如果我们通过漏斗把水倒进瓶子里,我们知道不能一下子把一个桶里的水全部倒完,肯定会溢出来的。一开始慢慢倒,后来发现什么都能倒进去,就加快速度。这称为慢启动。在一个TCP连接开始时,cwnd被设置为一个段,一次只能发送一个段;收到此确认后,cwnd将增加1,因此可以一次发送两个段;当这两个数据包的确认到达时,每个确认的cwnd加1,两个确认的cwnd加2,所以一次可以发送4个;当这4个确认到达时,每个确认的cwnd加1,4个确认的cwnd加4,所以一次可以发送8。从上面的过程可以看出这是一个指数增长。但什么时候会达到顶峰呢?值ssthresh是65535字节。当超过这个值时,增长率会降低。这时,在收到确认后,cwnd增加1/cwnd。一次发送8个。当8个确认到达时,每个确认增加1/8,8个确认加1,所以一次可以发送9个,变成线性增长。即使增长变成线性增长,仍然会出现“溢出”和拥堵。这时候一般会直接减慢倒水的速度,让溢出的水慢慢渗入。拥塞的一种形式是丢包,需要超时重传。此时设置ssthresh为cwnd/2,设置cwnd为1,重启慢启动。即一旦超时重传,立即“从零开始”。显然,这种方法过于激进,一下子停止高速传输会导致网络卡顿。前面提到了快速重传算法。当接收端发现一个中间包丢了,就连发3次上一个包的ACK,告诉发送端马上给我发下一个包,不要等到超时再重传。TCP认为这不严重,因为大部分没有丢失,只有一小部分丢失,cwnd变成cwnd/2,然后sshthresh=cwnd。当返回三个数据包时,cwnd=sshthresh+3.可以看出这种情况下的降速不是那么激进,cwnd仍然处于一个比较高的值,呈线性增长。下图是两者的对比。前面说了,正是这种进退让延迟在非常重要的时候降低了速度。但是,仔细想想,TCP的拥塞控制主要用来避免的两种现象是有问题的。第一个问题是丢包。丢包并不一定意味着通道满了,也可能是管道已经“漏水”了。就像当公网带宽不够的时候,就会丢包。这时候就认为是拥塞了,降低发送速度其实是错误的。第二个问题是TCP的拥塞控制等到中间设备满了才发送丢包,从而降低了速度。但实际上,此时再减速已经来不及了。管道被填充后,直到出现丢包时才应该被填充。为了优化这两个问题,后来开发了TCPBBR拥塞算法。它试图找到一个平衡点,就是通过不断提高发送速度来填满管道,但不要填满中间设备的缓存,因为延迟会增加。在这个平衡点上,可以实现高带宽和低时间。扩展平衡。下图是BBR算法和普通TCP的对比:总结序列问题,丢包问题,流量控制都是通过滑动窗口来解决的。这相当于领导和员工的备忘录。分配的作业必须编号,完成后会有反馈。分配的工作不能太多也不能太少;拥塞控制是通过拥塞窗口来解决的,相当于往管道里灌水。速度太快容易溢出水,速度太慢浪费带宽。必须摸着石头过河才能找到最优值。参考:TCP/IP指南;百度百科——TCP入门;刘超——网络协议趣谈系列;
