顺序问题和丢包问题
- 为了保证顺序性,每个包都有一个ID(序号),在建立连接时沟通初始ID,然后按照ID一个一个发送,为了保证不丢包,需要对接收的包有确认,但是这个确认不是收到一个包就确认一个,而是会应答某个ID之前所有的包,这叫
累计确认或累计应答
。 - 为了记录所有发送的包和接收的包,TCP也需要发送端和接收端分别都有缓存来保存这些记录。
发送端的缓存
里是按照包的ID一个一个排列,根据处理情况分成四个部分:- 1、发送了且受到确认了,可以从缓存移除。
- 2、发送了尚未收到确认,需要等待确认,可能需要重发,所以不能直接移除。
- 3、未发送,等待发送的。
- 4、未发送,并且暂时还不会发送的。
- 区分3、4部分是因为有流量控制,在TCP中,接收端会给发送端报一个窗口大小,叫
Advertised window
,这个窗口的大小应该等于上面的第二部分加上第三部分,超过这个窗口,接收端处理不过来,就不能发送了。
LastByteAcked
是发送已确认和发送未确认的分界线,LastByteSent
是已发送和未发送的分界线,LastBytesSent加上AdvertisedWindow大小
是可发送和不可发送的分界线。接收端的缓存
里根据处理情况分成三部分:- 1、接受过且确认了。
- 2、等待接收,未确认。
- 3、不能接收的。
LastByteRead
之后是已经接收,还没被应用层读取的。MaxRcvBuffer
是缓存最多放多少,NextByteExpected
是已确认和未确认的分界线(未确认分为接收和等待接收,接收的可能不是按顺序的
)。AdvertisedWindow=MaxRcvBuffer-((NextByteExpected-1)-LastByteRead)
,比如图上NextByteExpected=6,那么实际上有6-1=5个包已经接收了(假设序号从1开始),缓存中已经接收了5个,那么剩余空间就是窗口大小。已经被应用层处理的包的序号加上缓存大小就是能接收的最大序号
。
确认和重发的机制:
- 方式一,
超时重试
,对于每一个发送但未收到确认的包,过一段时间就重发,超时时间通过采样往返时间RTT加权平均得到,这个叫自适应重传算法
。如果重发第二次又没有收到确认,TCP策略是超时时间加倍
,理由是两次超时,说明网络较差,不宜频繁发送。超时重传的问题在于重传周期较长。 - 方式二,
快速重传
,当接收方收到一个序号大于下一个所期望的报文段时,就检测到数据流中的间格,发送三个冗余的ACK,比如收到6,8,9,接收方知道7丢了,那么就发送三个(为什么是三个,是一个估计值)6的确认,要求下一个是7,发送方收到这三个确认,就会发现7的确丢了,不等超时,马上重传。 - 方式三,
SACK
,这种方式需要在TCP头加一个SACK,可以将缓存地图发给发送方,比如ACK6,SACK8,SACK9。
流量控制
确认包同时会携带一个窗口(滑动窗口rwnd)的大小
。- 如果窗口不变,确认了一个包,窗口右移一格,有一个新包进入可发送状态。
- 如果发送端过猛,发送了窗口大小个包,而这些包都还未得到确认,那此时未发送可发送部分为0。
- 发送方只有在收到一个包的确认,窗口才能右移一个格,才有新包可以发。
- 如果接收方实在处理慢,导致缓存没空间了,可以通过确认信息修改窗口大小(甚至可以设置为0,让发送方停止发送)。假如接收方一直不处理数据,那么收到一个包,窗口就只能减小一。
- 发送方收到确认,窗口只有左边界移动了,窗口大小变小了。
- 如果接收端一直不处理数据,那么随着确认包越来越多,窗口就越来越小,直到为0。
- 当发送端收到最后一个包的确认到达时,发送端的窗口也会调整为0。
当发送端窗口变为0后,会定时发送窗口探测数据包
,看是否有机会调整窗口大小。当接收方比较慢时,要防止低能窗口综合征,不要有个空位就告诉发送方,而是要当空位数量达到一定大小,或缓冲区一半为空,才更新窗口
。
拥塞控制
- 拥塞控制也是通过窗口大小控制的(拥塞窗口cwnd),拥塞控制的目的是防止网络堵车。
- 发送速度由滑动窗口和拥塞窗口共同控制:
LastByteSent - LastByteAcked <= min {cwnd, rwnd},即发送的数量最大等于两个窗口较小值加上已确认
。 - 发送方如何判断网络其实很难,对于TCP协议来讲,网络路径会经历什么压根不知道。
TCP发送包就像往水管灌水,而拥塞控制就是在不堵塞不丢包的情况下,尽量发挥带宽
。 - 水管粗细就是网络带宽,即每秒能够发送多少数据;水管长度就是端到端的时延,理想状态下,水管里的水量 = 水管粗细 x 水管长度。类比到网络上,
通道的容量 = 带宽 * 往返延迟
。 - 如果设置发送窗口,使得
发送但未确认
的包为通道的容量,就能够撑满整个管道。 - 假设往返需要8s,每秒发一个包,每个包1024byte。过去的8s,则8个包都发出去了,其中前4个包已经到达接收端,但是ACK还没有返回不能算发成功,后4个包还在路上,还未被接收,这个时候整个管道正好撑满。
在发送端,这8个包的大小正好等于带宽,即每秒一个包乘以返回时间8秒
。
- 在此基础上再调大窗口,使得单位时间发送更多的包,会出现什么现象呢:要么中间设备处理不过来,包丢弃;要么包在缓存中排队时间过长,超时重传。
拥塞控制就是为了避免这两个现象
。 - 一开始如何知道发送速度多块呢?就像倒水,开始慢一点,如果一直能倒进去就加快速度,这叫
慢启动
。 - 一条TCP连接开始,cwnd设置为一个报文段,一次只发一个;当收到这一个包的确认后,cwnd加1,这次可发两个;再收到这两个确认,每个确认cwnd加1,这次可以发四个;可以看出是
以2为指数增长
。 - 当超过ssthresh 65535字节(阈值),就要慢点来,因为可能快满了,从此时开始,每收到一个确认,cwnd增加1/cwnd。比如上面一次发送四个,这时一个确认增长1/4,四个确认共增长1;然后一次发五个,五个确认共增长1,变成了
线性增长
。 - 线性增长到拥塞时,表现为丢包,需要超时重传,这时将ssthresh设为cwnd/2,将cwnd设为1,重新开始慢启动,
但这种方式太激进,将一个高速传输速度一下子停下来,会造成网络卡顿
。 - 前面讲过
快速重传算法,收到三个前一个包的确认,就快速重传当前包,不等待超时
。TCP认为这种情况大部分没丢,cwnd减半为cwnd/2,sshthresh = cwnd。当三个包返回的时候,cwnd = sshthresh + 3
,接下来依旧是线性增长。
其实使用拥塞控制避免丢包和超时重传是有问题的
。丢包并不代表通道满了,例如公网上带宽不满也会丢包;此外TCP的拥塞控制要等到将中间设备都填满才发生丢包,但此时已经晚了,只要填满管道就可以了。- 为了优化,后来有了
TCP BBR拥塞算法
,企图找到一个平衡点:通过不断加快发送速度,将管道填满,但不要填满中间设备缓存。