一、TCP可靠性传输
参考小林图解网络
1、重传机制
1.1、超时重传
超时重传,就是在发送数据时,设定一个定时器,当超过指定的时间后,没有收到对方的 ACK 确认应答报文,就会重发该数据。
TCP 会在以下两种情况发生超时重传:
数据包丢失
确认应答丢失
超时重传时间 RTO是一个动态变化的值,其值应该略大于报文往返 RTT 的值。RFC6298建议使用下式计算超市重传时间RTO:
即出现超时重传时,新的RTO为2倍旧的RTO。
超时触发重传存在的问题是,超时周期可能相对较长。可以用快速重传机制来解决超时重发的时间等待。
1.2、快速重传
快速重传机制,不以时间为驱动,而是以数据驱动重传。当收到三个相同的 ACK 报文时,会在超时重传时间 RTO过期之前,重传丢失的报文段。
如下图,发送端依次发送了seq1,seq2,seq3,seq4,seq5,
1、seq1送达,接收端确认接收seq1,回复ACK;
2、seq2因某些原因丢失,未送达;
3、seq3、seq4、seq5依次送达,但因为接收端未收到seq2,因此接收端还是会发送三个对seq2确认的ACK;
4、接收端收到三个连续的seq2确认ACK,会在超时重传时间 RTO过期之前,重传seq2;
5、最后接收端成功收到seq2,此时5个数据包都成功收到,于是发送确认seq5的ACK。
但是如果seq2、seq3都丢失,接收端都是回复确认seq2的ACK。那么重传的时候,是重传一个,还是重传所有的呢?
为了解决不知道该重传哪些 TCP 报文,于是就有 SACK 方法。
1.3、SACK
SACK( Selective Acknowledgment), 选择性确认。简单来说,可以对每次发的数据包都加上序号,这样接收端就可以判断当前的数据是否有接收过,从而决定其去留。
这种方式需要在 TCP 头部「选项」字段里加一个 SACK 的东西,它可以将已收到的数据的信息发送给发送方,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据。
如下图,发送方收到了三次同样的 ACK 确认报文,于是就会触发快速重发机制,通过 SACK 信息发现只有 200~299 这段数据丢失,则重发时,就只选择了这个 TCP 段进行重复。
1.4、Duplicate SACK
Duplicate SACK 又称 D-SACK,其主要使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。即可以为ACK加上编号。则每个ACK的相互作用就不会互串了。
2、滑动窗口
TCP 是每发送一个数据,都要进行一次确认应答。当上一个数据包收到了应答了, 再发送下一个。简单来说,就是一来一回。虽然模式简单,但是缺点是效率比较低的。
为解决这个问题,TCP 引入了窗口,其中窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值。
举个例子,窗口相当于货车。发送端每次发送一货车的物品,并记录在本子上。接收端收到货之后,
1、如果清点无漏,就回复确认消息。发送端收到确认消息之后,就把之前的发货记录划掉,继续发货;
2、如果清点完发现有某个商品漏了,就会根据重传机制,让发送端重发此商品。等到全部接收再回复确认。
这样,可以一次性发一批货。不用发一个等一个,费时。
专业点,窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。
注意点
1、窗口大小由哪一方决定?
TCP 头里有一个字段叫 Window,也就是窗口大小。
这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据。所以,通常窗口的大小是由接收方的窗口大小来决定的。
2、接收窗口和发送窗口的大小是相等的吗?
并不是完全相等,接收窗口的大小是约等于发送窗口的大小的。比如接收端处理数据很快,那么接收窗口很快就会被空出来。
3、流量控制
3.1 滑动窗口与流量控制
一般来说,我们总是希望数据传输得更快一些。但如果发送方把数据发送得过快,接收方就可能来不及接收处理,导致触发重传机制,从而造成数据的丢失和流量的浪费。
所谓流量控制就是让发送方根据接收方的实际接收能力来控制发送速率,要让接收方来得及接收。利用滑动窗口机制可以很方便地在TCP连接上实现对发送方的流量控制
3.2窗口关闭
TCP 通过让接收方指明希望从发送方接收的数据大小(窗口大小)来进行流量控制。
如果窗口大小为 0 时,就会阻止发送方给接收方传递数据,直到窗口变为非 0 为止,这就是窗口关闭。
但TCP 是如何解决窗口关闭时,潜在的死锁现象呢?
TCP 为每个连接设有一个持续定时器,只要 TCP 连接一方收到对方的零窗口通知,就启动持续计时器。
如果持续计时器超时,就会发送窗口探测 ( Window probe ) 报文,而对方在确认这个探测报文时,给出自己现在的接收窗口大小。
4、拥塞控制
流量控制是避免发送方的数据填满接收方的缓存,但是一般来说,计算机网络都处在一个共享的环境。因此也有可能会因为其他主机之间的通信使得网络拥堵。
在网络出现拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是一重传就会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包。出现拥塞而不进行控制,整个网络的吞吐量将随输入负荷的增大而下降
拥塞控制主要四个算法:
慢启动、拥塞避免、拥塞发生、快速恢复
4.1拥塞窗口
拥塞窗口 cwnd是发送方维护的一个的状态变量,其值取决于网络的拥塞程度,且是动态变化的。
拥塞窗口cwnd,与发送窗口 swnd 和接收窗口 rwnd 的关系是:swnd = min(cwnd, rwnd),也就是拥塞窗口和接收窗口中的最小值。
拥塞窗口 cwnd维护的规则:只要网络中没有出现拥塞,cwnd 就会增大;但网络中出现了拥塞,cwnd 就减少;
判断出现网络拥塞的依据:发送方没有按时收到 ACK 应答报文,也就是发生了超时重传,就会认为网络出现了拥塞。
4.2 慢启动
TCP 在刚建立连接完成后,首先是有个慢启动的过程,这个慢启动的意思就是一点一点的提高发送数据包的数量,而不是一开始就发大量的数据。当发送方每收到一个 ACK,拥塞窗口 cwnd 的大小就会加 1。
并且维护一个慢开始门限ssthresh状态变量:
1、当cwnd < ssthresh时,使用慢开始算法;
2、当cwnd >ssthresh时,停止使用慢开始算法而改用拥塞避免算法
3、当cwnd =ssthresh时,既可使用慢开始算法,也可使用拥塞避免算法
4.3 拥塞避免
当拥塞窗口 cwnd 超过慢启动门限 ssthresh 就会进入拥塞避免算法,其规则是每当收到一个 ACK 时,cwnd 增加 1/cwnd。
拥塞避免算法就是将原本慢启动算法的指数增长变成了线性增长,还是增长阶段,但是增长速度缓慢了一些。
随着时间推移,网络就会慢慢进入了拥塞的状况了,于是就会出现丢包现象,这时就触发了重传机制,也就进入了拥塞发生算法。
4.4 拥塞发生
当网络出现拥塞时,会发生数据包重传,重传机制主要有两种:超时重传、快速重传
超时重传
1、将ssthresh值更新为发生拥塞时cwnd值得一半
2、将cwnd值减少为1,并重新开始执行满开始算法
对于超时重传,如果遇到个别报文段会在网络中丢失,但实际上网络并未发生拥塞,这将导致发送方超时重传,并误认为网络发生了拥塞;并且发送方把拥塞窗口cwnd又设置为最小值1,并错误地启动慢开始算法,因而降低了传输效率。
快速重传
所谓快重传,就是使发送方尽快进行重传,而不是等超时重传计时器超时再重传。
1、要求接收方不要等待自己发送数据时才进行捎带确认,而是要立即发送确认
2、即使收到了失序的报文段也要立即发出对已收到的报文段的重复确认
3、发送方一旦收到3个连续的重复确认,就将相应的报文段立即重传,而不是等该报文段的超时重传计时器超时再重传。
TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,则 ssthresh 和 cwnd 变化如下:
1、cwnd = cwnd/2 ,也就是设置为原来的一半;
2、ssthresh = cwnd;
3、进入快速恢复算法
4.5 快速恢复
发送方一旦收到3个重复确认,就知道现在只是丢失了个别的报文段。于是不启动慢开始算法,而执行快恢复算法。
1、拥塞窗口 cwnd = ssthresh + 3 ( 3 的意思是确认有 3 个数据包被收到了);
2、重传丢失的数据包;
3、如果再收到重复的 ACK,那么 cwnd 增加 1;
4、如果收到新数据的 ACK 后,把 cwnd 设置为第一步中的 ssthresh 的值。
为什么拥塞窗口 cwnd = ssthresh + 3 ?
因为既然发送方收到3个重复的确认,就表明有3个数据报文段已经离开了网络;
这3个报文段不再消耗网络资源而是停留在接收方的接收缓存中;
可见现在网络中不是堆积了报文段而是减少了3个报文段。因此可以适当把拥塞窗口扩大些
为什么收到新数据的 ACK 后,把 cwnd 设置为第一步中的 ssthresh 的值?
原因是该 ACK 确认了新的数据,说明从 duplicated ACK 时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态;
二、UDP可靠性传输
对于大部分的应用使用TCP既可以满足工程的需求,又可提供可靠的数据传输服务,并具备流量控制和拥塞控制机制。但对一些实时性要求比较高的场景,如实时通信、游戏、视频流等,则比较适合UDP。
1、音视频通话(网络延时,tcp不可以控制重传,延时太大,udp可以控制重传时间);
2 、游戏开发(实时性操作:王者荣耀;传输位置,延迟会造成卡顿)
3 、DNS查询(一问一答;一个包就可以,丢包直接重发就行)
4 、物联网设备监控,用电池(活跃状态耗电,睡眠,发送数据量不大)
5 、心跳机制 监测设备在不在线心跳包
UDP的可靠性传输与TCP中的一些策略类似,在上面了解TCP可靠性传输的基础上学习,将事半功倍。
1、主要策略
UDP如何做到可靠性传输,有以下几个策略:
1、ACK机制:当接收方接收到数据时,即回复ACK进行确认
2、重传机制
3、序号机制:发送方给每个数据包加上序号,让接收方收到乱序包之后方便重排
4、重排机制:接收方收到乱序包之后,根据序号重排
5、窗口机制
2、重传机制
ARQ协议(Automatic Repeat-reQuest),即自动重传请求,是传输层的错误纠正协
议之一,它通过使用确认和超时两个机制,在不可靠的网络上实现可靠的信息
传输。
ARQ协议主要有3种模式:
- 即停-等待协议SW(stop-and-wait)
- 回退n帧协议GBN(go-back-n)
- 选择重传协议SR(selective repeat)
2.1 即停-等待协议
1、每发送一帧数据后需要接收到对方的回复之后才发送下一帧数据。
2、为避免因接收方收不到数据分组,而不发送ACK或NAK,导致发送方一直处于等待接收方ACK或NAK的状态。可以在发送方发送完一个数据分组时启动一个超时计时器。若到了超时计时器所设置的重传时间而发送方仍收不到接收方的任何ACK或NAK,则重传原来的数据分组,即超时重传。
3、为了让接收方能够判断所收到的数据分组是否是重复的,需要给数据分组编号。
4、为了让发送方能够判断所收到的ACK分组是否是重复的,需要给ACK分组编号。
2.2 回退n帧协议
发送方给接收方发送5、6、7、0、1,但是5号数据丢失,导致接收方接收数据时,数据与接收窗口的序号不匹配。
发送方收到重复的确认,就知道之前所发送的数据分组出现了差错,于是可以不等超时计时器超时就立刻重传!
需要注意的是,回退N帧协议可以采用累计确认方式。比如发送0、1、2、3、4,接收方成功接收并返回确认值ACK4,表示4号及之前的数据被成功接收。
另外,回退N帧协议的接收窗口尺寸Wr只能等于1,因此接收方只能按序接收正确到达的数据分组。
2.3选择重传协议
选择重传协议核心是,先接收已收到且无误码的数据。再告诉发送端,哪一个报文丢失,重传丢失报文即可。当成功接收全部数据,才能向前滑动窗口。
3、流量控制和拥塞控制
参考tcp
4、UDP编程模型
TCP面向连接的流式传输,一次可以只收一部分数据。
而UDP 报文传输,一次只能接收一个包,且recvfrom()一次需要读取完整的报文。因此若MTU=1500字节,而UDP 发送的数据包过大,需要拆包发送。
5、KCP协议
KCP(Kernel Congestion Control Protocol)是一种高速可靠性传输协议,它在UDP协议的基础上进行了优化和改进,可以有效地解决网络丢包、拥塞等问题,提高数据传输效率。严格意义上讲,KCP并不是一种网络传输协议,它是为UDP写的可靠传输算法。
5.1 KCP流程
第一步,就是创建一个kcp实例,然后进行初始化,相当于一个句柄。 ikcpcb* ikcp_create(IUINT32 conv, void *user) 第二步,设置发送回调函数,底层用哪种socket都没问题,只要能把数据发送出去,建议使用UDP,比较简单。 void ikcp_output(ikcpcb *kcp, int (*output)(const char *buf, int len, ikcpcb *kcp, void *user)) 第三步,更新KCP状态。KCP运行于用户空间,所以需要手动去更新每个实例的状态,其实主要就是检测哪些数据包该重传了。 void ikcp_update(ikcpcb *kcp, IUINT32 current) 第四步,发送数据。调用ikcp_send之后,KCP最后会使用上面设置的ikcp_output函数来发送数据(KCP自己并不关心如何发送数据)。 int ikcp_send(ikcpcb *kcp, const char *buffer, int len) 第五步,预接收数据。在应用主动调用recvfrom读取udp数据,,然后再调用ikcp_input将裸数据交给KCP,这些数据有可能是KCP控制报文,并不是我们要的数据。 int ikcp_input(ikcpcb *kcp, const char *data, long size) 第六步,接收数据。此时收到的数据才是真正的数据,重组操作在调用ikcp_recv之前就完成了。 int ikcp_recv(ikcpcb *kcp, char *buffer, int len) 第七步,释放一个kcp对象 void ikcp_release(ikcpcb *kcp)