以下是TCP协议传输数据过程中的一些特性
一、滑动窗口
这里的滑动窗口和 oj题算法里的滑动窗口性质是一样的,不过这里的滑动窗口是为了提高网络传输信息的速度。
如图,它是类似窗口这种,传输数据的时候是按窗口这种为单位进行传输的,这样传输的速度就会比一个一个数据的传要快。因为是花费一个传输数据的等待时间,但是能传输的数据是一个窗口这样单位的数据量。
TCP这里反复强调数据传输是可靠的,那像上面这种以窗口为单位的方式传输数据,要是中间有丢包的情况,它又会怎么处理呢?
1、ack丢了
如果是ack丢了,其实是没关系的,因为这里有确认序号,如果你接受到3001~4000,因为接受到后,确认序号就会变成4001,如果前面接受到1~3000的数据,然后接受方没有返回ack,但你此时的确认序号就是4001,那有关系吗,接受方已经知道了,而确认序号的4001就是表示4001前的数据它都收到了,下一个要接受的数据是4001。如图:
如上图,虽然中间两个ack传输失败了,但这里的确认序号已经变成了4001,也就是说,4001之前的数据都收到了,接下来要收到的数据是4001了,也就是说,中间两次的ack发不发送给主机A都可以,因为我已经知道了4001之前的数据都收到了。就像你在大街想加一个小姐姐的微信,结果人家小姐姐跟你说,人家都已经有娃了,那里面包含的信息不就是人家小姐姐已经谈过恋爱还结婚了。
2、数据丢了
如图:
如果1~1000的数据没有发送到主机B上,也就意味着主机B不会发送ack报文给主机A了,发送方在反复多次想主机A索要ack后,还是收不到,发送方就会认为1~1000的数据丢包了,就会重传1~1000的数据给主机B。
主机B因为拿不到1001之前的数据,就会反复想发送端索要;接下来就是重传3次后,主机B终于收到1~1000的数据了,主机B就会返回ack给发送方,然后主机B的确认序号就会变成4001,而不是1001;这是因为不管是发送方还是接收方,都会有接受缓冲区,前面1001~4000的数据主机B是收到了,但不会更新确认序号,而是把最新的确认序号放在主机B里的接收缓冲区中。当拿到1~1000的数据后,就会返回ack给发送端,然后更新确认序号,这个新的确认序号就是在接收缓冲区中拿的。
注意:这里的反复索要,次数也是有限的,如果反复多次索要合理的数据,还是不成功,想要的数据应该就是丢了,这里规定的次数也就相当于 “超时时间” 的判定了。
上述的重传的过程,整体效率还是很高的,已经接收到的数据就不不必重传了,这里重传是针对没有接受到的数据(丢包了的)。这里整体的效率没有损失,这种重传就称为快速重传。
这里的滑动窗口、快速重传、确认应答、超时重传是同时存在的,互不干扰;如果滑动窗口的窗口比较小,也就是要传输的数据比较少,就会出现:滑不动的情况,这时为了保证可靠性,就会退化成确认应答,一个一个的数据进行传输,使用超时重传来保证其可靠性,此时判断是否丢包就是看是否到达时间还没返回ack;如果数据很庞大,就会使用快速重传来保证其可靠性,此时判断是否丢包就是看是否有多个ack,索要同一个数据。
二、流量控制(流控)
我们通过滑动窗口可以知道,这里的窗口越大,传输速度就会越快,但是窗口能无限大吗?答案显而易见:不能;以下是原因的解释。
我们知道,发送方发送数据的速度是由滑动窗口的大小决定的,而接收方接受数据的速度是由它自身处理数据的能力决定的,如果发送方发送的数据太多、太快,接收方处理不过来这些数据了,第一时间这些处理不过来的数据会放在接收缓冲区里面,但如果缓冲区里面也满了,这时候,就会出现丢包。所以我们要控制窗口的大小,举个简单的例子,如图:
我们想要让蓄水池不装满水的话,进水口的速度就要和出水口的速度持平,就算快一点点也没事,因为蓄水池有容量空间,可以储存在蓄水池里;但如果进水口速度过快,出水口就那么大点位置,不久后蓄水池就会装满水,这时候出水口排不出去这么多水,进水口那就会溢出浪费了。
我们所说的流量控制也和上面的例子类似,要控制窗口大小,尽量让发送方的发送数据速度和接受速度持平,而为了让发送、接受速度尽可能持平,就要让接收方去影响发送方的速度,这就是流量控制。流量控制是通过下面这个字段反馈给发送方的:
这个字段在普通报文段没有意义,在ack报文中才有意义,也就是接收方接受到数据后,通过ack反馈给发送方,告诉发送方下次发送数据的速度(窗口大小)该如何调整;而接收方通过自己的接受缓冲区剩余空间的大小,作为ack中窗口大小的数值,发送方接受到ack后,就会根据这个数值,来调整自己窗口的大小。大概流程如图:
第一次传输1~1000的数据给主机B,主机B收到后根据自己接收缓冲区剩余大小,在ack中设置窗口大小报文,把ack返回给主机A,主机A就知道主机B的接收缓冲区还剩多少,从而调整主机A传输数据的窗口大小,把1001~4000数据批量的传输给主机B,此时主机B的接受缓冲区就满了,然后主机A这边等一定的时间,来接受主机B发送过来的窗口更新的通知,超过这个时间阈值还没等到主机B发来的通知,此时主机A就会发一个窗口探测包给主机B,索要窗口更新通知,等收到窗口更新通知后发现是2000,此时就主机A就发送4001~6000数据给主机B。
这里16位窗口大小最大就是64k吗?其实不然,子啊TCP报头的选项中,还包含了一个参数:窗口拓展因子;而实际真实要设置的窗口大小是:16位窗口大小 * 2^窗口扩展因子
三、拥塞控制
拥塞控制和流量控制的本质都一样:限制发送方发送数据的速率。不过这里是站在有中间路径的角度看的。
我们知道,网络是非常复杂的,通过网络从A机器发送数据给B机器,中间要经过很多的路由器、交换机,其中这中间的设备情况都不一样,有些设备的负载不大,数据能正常传输,但有些设备可能负载很大,已经到它的极限了,这时候就会出现丢包的情况。如图:
而拥塞控制就要考虑这种情况,然后调整发送方拥塞窗口的大小。
因为这里类似木桶效应,木桶能装多少水,取决于短板;上面的图也是类似的道理,有很多条路径传输数据的速度非常快,但有其中一个路径的设备高负载了,到极限了,只要数据经过这,就会阻塞,也就必须要调整发送方的窗口大小。
因为传输数据要经过中间路径,所以啥时候回堵塞,我们不能知道,是随机的;这里为了调整窗口的大小,就只能 实验 了。
而窗口的调整也是动态变化的;因为我们无法控制啥时候传输数据的时候会堵塞,所以,只要丢包了,我们就认为数据在中间某个节点上阻塞了,从而缩小窗口大小,如果没丢包,就逐渐增大窗口大小;我们称为:动态变化。以下是动态变化的规则。
拥塞窗口动态变化的规则
总的规则:流量控制和拥塞控制谁产生的窗口小,就是谁说的算。以下是拥塞控制产生的窗口大小规则:
1、慢启动;刚开始传输数据的时候,拥塞窗口会设置的非常小,传输的速率也就很小(如果一下子就把窗口设置太大,就可能导致设备的负载过高,从而在传输的过程中阻塞、丢包)。
2、因为刚启动时的窗口非常小,此时为了提高传输效率,就会增大窗口,而这个窗口是随指数式增大的。
3、随着窗口增大到一定程度,就不能继续让它指数式的增大了,因为指数式的增长的太快,窗口一下子增大的太大,就会导致网络拥堵、丢包这种情况;这里会设置一个 阈值 ,到达阈值后,窗口就会成线性式增大。
4、窗口是随着线性式的增大,也不能让它无限的增大,如果增大到一定程度,出现了丢包的情况,就要让窗口大小重新回到一个比较小的值,然后回到慢启动这个过程。并且这里会根据刚才丢包的窗口大小,重新设置限制指数增长的 阈值。
大概流程如图:
如果出现丢包了,就要进入上面说的第四阶段,但这里有两个版本的慢启动,一是旧版的,窗口大小回到非常小的值,经过指数增长再进入线性增长;二是新版的,窗口不会回到非常小的值,也不会回到指数增长阶段,直接是线性增长阶段,指数增长阶段只会出现在刚开始的时候。
整个过程的窗口大小都是动态变化的,这个过程也非常像谈恋爱的过程。
四、延时应答
基于滑动窗口,来提高传输效率的;延时应答就是发送方批量的发送数据给接收方,接收方收到这些数据,并不会一个一个ack的发送给发送方,而是等一段时间,等接收方处理一段时间刚发过来的数据后,才返回一个ack给发送方。为什么呢?
原因就是接收方接收到数据,不是就直接处理这个数据的,而是先放到接收缓冲区中,然后再处理数据,如果等一段时间,接收缓冲区的数据就会变少,因为留有一定的时间把它们给处理了。那返回ack中会带有窗口大小的报文,这个报文就是根据接收缓冲区的剩余空间来设置窗口大小的,因为留有一段时间处理缓冲区的数据,所以接收缓冲区的剩余空间就变大了,窗口大小也会变大,传输数据的速度也会变快。
上面讨论过滑动窗口如果ack丢了的情况,这种情况没关系,因为有确认序号,所以丢了也没事;所以延时应答这里,就设置每处理几个数据,就返回一个ack回去,从而起到延时的效果,当然,这里等处理几个数据的时间也不是无限等的,而是会设置一个时间阈值,如果超过这个时间阈值,不管接收方还剩多少个数据没处理,都会返回ack。
所以延时应答的核心:通过在允许的范围内(延时返回ack),增大滑动窗口的大小(提高数据的传输效率)。
五、捎带应答
基于延时应答,用来提高数据传输效率的。在数据传输中,如果发送方发送数据过来,我接收到了,然后因为延时应答,我可以等一段时间再返回ack回去,但因为有等待这一时间,就可以把ack连同响应,一起返回给发送方。大概流程如图:
这里ack延时的这段时间结束后,服务器刚好把请求计算完,准备返回响应给客户端,这时候就可以把ack和response一起返回回去。当然,如果返回ack和发送请求的时机一样,也可以合并,就成了如图:
因为延时应答,所以只要延时后ack返回的时机和发送数据的时机相同,触发了捎带应答,就可以把它们合并到一起进行发送。所以,基于延时应答+捎带应答,是可以把四次挥手变成3次的:fin fin+ack ack。
捎带应答啥时候触发,具体还得是看你代码具体咋写,下次发送过来的数据快不快,接受方处理数据的速度快不快。
六、面向字节流
这里核心要解决的问题:粘包问题
因为TCP是传输数据的时候是以字节为单位进行传输的,那如果多次传输数据过去,这些数据就会在接收缓冲区中,因为全都是字节,在接收缓冲区中就无法辨哪到哪是一个完整的应用层数据包,这就是 粘包问题。如图:
其中解决粘包问题有两种方法:
(1)通过特殊符号作为分隔符。例如上图说的用空白作为分隔符,如果遇到这个分隔符了,就说明一个包结束了。
(2)指定包的长度。会使用一个特殊的空间,表示一个数据包的长度,如图:
对于 粘包问题,TCP协议是不会解决的,要程序员自己编写代码解决这个问题,可以用上面这两种方法解决。
而UDP协议就不会存在这种问题,因为UDP协议面向的是数据报,发送数据的单位是数据报,就能分隔开不同的数据内容;而UDP的接受缓冲区不是像上面这样的,而是类似数据结构中的链表,把每个的数据报给连接起来,如图:
七、异常情况
异常的严重程度也是有高低之分的,一种是丢包了的异常,如果是丢包了,就直接触发TCP里的超时重传特性就好了;但也还有更严重的情况:网络故障,这该如何解决呢?
以下有不同网络故障的情况,其中也会介绍如何解决:
(1)进程崩溃了
进程无论是正常关闭,还是异常崩溃,都会进行资源回收,也就是关闭文件的效果(系统内完成的),所以,如果是进程崩溃了,进程崩溃的一方肯定会执行到类似socket.close()的效果,也就是说一定能把 fin 给发送出去;而且 TCP连接 的生命周期比进程的生命周期长,这样也就能完成四次挥手的操作,把连接给断开了(双方把保存对端的信息给删除掉)。
(2)其中一方关机了(正常关机)
当有个主机关机,就会强制杀死系统中所有的进程,进程关闭,也就一定会给对端发送 fin,对方接收到,也就会进行四次挥手的流程,但这里四次挥手不一定能挥完:
1、如果四次挥手挥的快,在系统关机前,把最后一个ack发送过去,就能完成四次挥手。
2、如果系统提前关机了,那也没事,至少把fin发送出去了,对端接收到 fin ,就会把ack和fin也返回回来,但是因为已经关机了,最后一个ack肯定不能给对端发送过去;这时候对端很久都没有收到ack,就会触发超时重传,当重传几次后,还没有收到,对端就会单方面把连接给断开了,删除连接信息。
(3)其中一方断电了
直接断电了,肯定就是来不及发送fin了,这里分为两种情况
1、接收方断电了
发送方就会突然发现,ack没了,发送方就会尝试重传,重传几次后,发现还是没有ack返回回来,就会触发 “复位” 连接进行重连,要是重连不成功,发送方就会知道对方挂了,然后进行单方面的断开连接。复位连接需要用到RST复位报文段,如图:第四个
复位连接:清除原来TCP的各种临时数据,重新开始连接,类似过去既往不咎的效果(此时的RST不会有ack的返回)。
剩下两个报文段:URG、PSH的简单介绍
URG:与TCP带外数据有关系,TCP中有些特殊的数据包,携带一些特殊的功能。
PSH:催促对方快点发消息给我。
2、发送方断电了
接收方本身就是在阻塞等待对方发消息过来,如果迟迟没有信息回来,接收方就要试探一下,对方是不是挂了,这里就会使用 “心跳包” 来咨询对方情况。而心跳包也不带应用层的数据的,是一个特殊的数据包,带有周期性的发送给对端,这个周期、频率程序员是可以自己设置的;当发送心跳包后, 没有返回 “心跳” ,就视为对方挂了;如果对端挂了,也就会进行单方面的断开连接。
(4)断网了(网线断开)
结合接收方和发送方都断电的情况。