一、重传机制
tcp的可靠性依赖于序列号机制和确认应答机制,即一端发送数据给另一端,另一端都会回复ack包,这样才保证这条数据发送成功,而在这个过程中会有两种可能发生:
- 一种是数据包未到达接收端,原因是数据丢失或者延时了;
- 一种是ack包未到达发送端,原因也是丢失或延时了。
前者数据未到达接收端,后者数据已经到达接收端,只是回复的ack包丢失了,未到达发送端。
tcp采用重传机制解决丢包和重复发送问题,tcp中重传包括超时重传,快速重传,sack和d-sack。
1.超时重传
顾名思义就是超过一定时间未收到回复就重新发送数据,这里比较难以确定是超时重传的时间RTO,这个时间太大和太小都不合适,应该是比数据包一个来回的时间RTT多一点才合理,但是数据包一个来回的时间RTT不是固定的,会受到网络波动的影响,所以RTT的时间是按照几次来回时间进行加权平均值和RTT的波动范围计算出来的,而RTO是在此基础上通过一些系数换算出来的。
2.快速重传
虽然有超时重传,但是有些数据包等到真的超时再重传就有些太慢了,因此linux还有一种重传机制叫做快速重传。
原理是:当发送方发送一条数据seq2后,未能得到ack数据包返回,此时如果后面又连续发送了几条数据seq3,seq4,seq5,seq6,而后面这几次收到的ack数据包都是ack2,ack2,ack2,ack2,意思是接收端已经收到了seq2之前的数据,但是seq2还没有收到,快速重传机制就是在发送端如果发送了多条数据,但是每个数据包的回复包的序列号都是相同的,比如这个例子中seq3,seq4,seq5,seq6返回的数据包都是ack2,这个2指的是序列号,表示接收端缺失的最大的数据的序列号是2,发送端应该发送seq2过来,如果有连续的三个ack2,tcp就会判断需要重发seq2,这种情况可以解决超时重传等待时间过长的问题,但是新的问题是发送端不知道重新发送seq2还是重新发送seq3,seq4,seq5,seq6,这种情况下不同版本的linux有不同的实现。
3.sack
上面的问题抛出来了,linux后面怎么解决呢,引入sack,这个字段的值会放在tcp头的选项字段上,就是发生上面例子中的情况下,后面接收端每次收到请求都会回复一个ack和sack,这两个值中间的部分就是当前接收端缺失的数据,即ack<=x 还有一种情况就是发送端发送的数据在网络中延时了,并没有丢失,那么在发送端进行重传后,这个延时的数据又到达了,这样就造成重复发送,这种情况下会采用d-sack方式,dsack其实就是利用sack处理重复数据的一种方式。依然是接收端回复ack+sack,只不过sack表示当前重复的这条数据的序列号,ack表示需要接受的序列号,这样发送端就能知道这条数据已经发送过了。 不难发现,可以通过ack和sack比较大小来区别这两种模式,如果ack大于sack就说明是数据重复发送了,如果ack小于sack就说明是数据缺失了。 TCP为保证可靠性使用确认应答机制,理论上来说就是发送端发送一条数据到接收端,接收端收到后回复一个应答数据,一次对话才算结束,然后发送端才会发送下一条数据。 这样的方式无疑是一种效率极低的方式,所以为了实现可靠以及高效,TCP引入滑动窗口和流量控制 TCP推出滑动窗口的概念,滑动窗口就是接收端和发送端为每个socket开辟一块空间,只有在接收端滑动窗口空闲的时候才能处理发送端的数据。 原理:发送端每次发送数据到接收端,接收端都会返回一个滑动窗口大小,表示接收端能接受的最大字节数,发送方接收到这个滑动窗口大小后可以连续发送多个数据包,只要在滑动窗口范围内即可。 发送方的滑动窗口有如下区域: 当已发送的数据得到回复后,滑动窗口右移。 接收端的滑动窗口有如下区域: 如果接收的数据被应用取走,窗口右移。 在滑动窗口范围内发送端连续发送的几个数据包,如果中间有一个包的ack丢失了,不一定需要重新发送,发送端可以通过下一个ack确定丢失的这个ack包需要不需要重发。也就是说有了滑动窗口的概念,当ack回复包丢失后不一定需要重发。 发送端的窗口大小是由接收端决定的。 滑动窗口的概念的提出,使得一次可以发送多个数据包,解决了一次只能处理一个数据包,效率低的问题。但是因为接收端的处理能力是有限的,作为发送端不能源源不断的给接收端发送数据,如果数据流量大了,接收端处理不了,就只能丢弃数据了,所以必须有一种机制可以控制数据的流量。 TCP的流量控制也恰恰是基于滑动窗口的,滑动窗口由接收端确认后发送给发送端,发送端根据窗口大小进行发送数据,就能保证发送的数据在接收端都能被接收处理。 但是基于滑动窗口实现流量控制TCP考虑了这样几个问题: 滑动窗口是socket缓冲区中的一块空间,socket对应的缓冲区也不是一成不变的,所以如果缓冲区变化对滑动窗口的同步就会造成一些影响。比如接收端通知发送端窗口大小为100,但是此时操作系统把socket缓冲区减小了到了50,收到的数据大于50,就会把包丢掉就出现了丢包现象。 还有一个问题,糊涂窗口问题,比如因为接收端处理比较慢,滑动窗口为0,窗口处于关闭状态,一段时间后,窗口出现了可能50个字节空闲空间,这时候就会把窗口=50通知发送端,发送端接收到窗口=50后,就会发送50字节的数据过来,但是要知道不管发送多少数据,tcp都要给数据包上tcp头,tcp头就有20字节,同时还要包ip头,也是20字节,足足40字节,而发送的数据就只有10字节,性价比是极低的。而且还会占用带宽。 tcp在实现基于滑动窗口实现流量控制的时候不得不考虑上面的问题,tcp如何解决呢? tcp不允许同时减少缓冲区大小和窗口大小,如果需要减少缓冲区大小,必须先减少窗口大小,一段时间后再减少缓冲区大小。 要想解决糊涂窗口的问题,就要避免接收端给发送端回复较小的窗口和避免发送端发送小的数据包。 tcp规定接收端在窗口大小 发送端也要解决发送小数据的问题,发送端是通过nagle算法进行延迟处理,即满足以下两个条件中的一个才可以发送: 只要满足这两个中一个就可以发送。但是这个算法一旦开启就会造成一些数据本身就很小的包不能及时发送,所以这个算法开启要慎重考虑。 tcp依靠上面的机制实现流量控制,具体的流程就是接收端每次都会给发送端回复窗口大小,当接收端很忙的时候,可能窗口就会变小到0,就会通知发送端窗口关闭,此时发送端就不会再发送数据给接收端,当接收端窗口变大后,就会主动回复发送端窗口大小。 但是这个回复可能会丢失,那么这是就会出现互相等待的问题,可以理解为死锁,tcp的解决方案是定义一个时钟,就是一个定时器,当到达一定时间接收端还没有通知窗口的话,发送端就会发送探测报文,一般每30-60秒发送一次,发送三次(这里不同实现可能不一样,可以配置),如果回复了窗口就开始发送数据,如果回复的窗口依然是0就重置时钟重新计时,如果最终都没有打开窗口,发送端可能会发送rst包给服务端终止连接。 滑动窗口和流量控制保证了接收端繁忙的时候,数据不会因为无处安放而丢弃。 但是网络是共享的,网络也会存在很繁忙的情况,如果网络拥堵,也会造成丢包,tcp在没有收到ack的时候就会重传,使得网络更加拥堵,丢失数据会更多,就会使得整个网络环境更加糟糕。 tcp为解决网络拥堵带来的数据丢失问题,提出了拥塞控制。 在拥塞控制机制中的两个概念:拥塞窗口和拥塞算法 首先来看怎样才算拥堵,tcp认为只要是出现超时重传就算拥堵了。 tcp在发送数据的时候,发送数据的大小受到滑动窗口和拥塞窗口的限制,也就是发送端能发送的最大数据量是拥塞窗口和滑动窗口的的最小值。 (1) 慢启动: 在刚刚建立连接的时候,滑动窗口和拥塞窗口一致,接下来我们假设滑动窗口和拥塞窗口初始值为100字节,后面的说明都以此假设为基础。 慢启动,就是在初始值的基础上,发送端向接收端发送100字节数据包(这里不要考虑数据包的个数,直接以总字节数来说明),当所有的ack返回后,拥塞窗口就会在100的基础上加100。再循环一次就是在200的基础上加200,再一次就是加800,这种算法的拥塞窗口是指数级增长的,并且增长速度很快,因此总要有个限制的,这个限制叫做拥塞门限值,这个值默认65535个字节。当拥塞窗口的值达到这个值后就进入拥塞避免阶段。 (2) 拥塞避免: 当拥塞窗口的值达到拥塞门限值后,就会进入拥塞避免阶段,在这个阶段,比如当前拥塞窗口的值达到了65535个字节,如果这个65535个字节都发送出去了,当所有的ack返回后,就是在65535个字节的基础上加65535个字节,再一次就再加65535个字节,再一次还是加65535个字节,也就不再是指数级增长了,而是线性增长,说白了就是增长的速度降下来了。但是即便是这样,拥塞窗口值也处于一个增长状态。那什么时候是个头呢,答案是出现超时重传的时候。 (3) 拥塞发生: 当发生重传的时候就意味着拥塞现象发生了,拥塞发生是因为重传造成的,重传分为超时重传和快速重传,当发生超时重传的时候说明网络确实很糟糕了,tcp的做法是将拥塞门限值设置为此时拥塞窗口值的一半,同时将拥塞窗口值设置为1.从而再次进入慢启动阶段。而如果发生的是快速重传,说明网络也不是很糟糕,拥塞窗口值会设置为此时拥塞窗口值的一半,拥塞门限值也会设置为拥塞窗口值的一半,此时如果什么都不做就会进入拥塞避免阶段,但是对于这种情况,tcp会进入快速恢复算法。 (4) 快速恢复: 就是在当前在当前拥塞门限值的基础上加3来表示拥塞窗口的大小,接下来重发失败的数据,收到恢复后就加1,接下来发送新的数据并进入拥塞避免阶段。4.d-sack
二、滑动窗口
三、流量控制
四、拥塞控制
拥塞算法