在之前的文章中,我们已经介绍了TCP的5个机制:确认应答、超时重传、连接管理、滑动窗口和流量控制,接下来我们来学习TCP的另外五个机制。
6、拥塞控制
虽然TCP为了高效可靠地提供网络传输,已经有了前面的5个机制。但是上述的机制都是建立在发送方和接收方的机制。别忘了,我们数据的传输还需要经过若干的路由器和交换机的封装分用(当前的网络环境是复杂的),如果在刚开始阶段就发送大量数据,就可能会引发大量的问题。
试想一下,如果接收方的接收速度非常快,此时我们发送方的窗口大小非常大,但是在发送方到接收方的通信路径已经“堵车”了,此时发送方发送消息就会出现丢包。
针对上面的情况,TCP的解决思路:把中间路径经过的设备,视为是一个整体,然后通过“实验”的方式,找到一个比较合适的传输速率(调整窗口大小)。
如果按照某个窗口大小发送数据之后,出现丢包了,就视为是中间路径存在拥堵,就减小窗口大小;如果没出现丢包,就说明中间路径没有出现拥堵就会继续增大窗口大小。
上述的方案,一方面把整个问题简化了,另一方面,也能够很好地适应当前网络环境的复杂性,中间这些节点,啥时候出现拥堵,啥时候没出现拥堵,那都是“随机”的。按照上述策略,就可以让发送数据进行动态变化。
那么窗口大小到底是流量控制还是我们这里的拥塞控制说了算呢?
总的原则:流量控制和拥塞控制,谁给出的窗口大小更小,谁就说了算。
拥塞控制的具体流程
1、慢启动:刚开始传输数据的时候,网络传输的环境未知,此时的窗口大小是很小的(此时的窗口大小是由拥塞控制说了算),传输的速率也是很小的。
2、指数增长:如果上述传输数据没有丢包,此时说明网络环境还是很通畅的,此时,就要增大窗口大小了(通过不断*2来进行指数增长)。因为使用的是慢启动,一开始,窗口大小会非常非常小,通过指数增长的方式就可以让窗口大小快速变大,保证了传输速率。
3、线性增长:指数增长并不会一直持续下去,达到一定的阈值之后就会变为线性增长。线性增长能够使当下的窗口,持久保持一个比较高的速率,并且也不容易一下就出现大量丢包的情况。
4、动态调整:线性增长也是一直在增长,一段时间之后,传输的速度可能太快,此时还是会引起丢包。一旦出现丢包,就把拥塞窗口重置成比较小的值了,然后又会重新回到慢启动过程(又要重新进行指数增长),而且会根据刚才的丢包情况,更改线性增长的阈值。
情况如下:
这里存在两种版本,TCP Tahos版本和TCP Reno版本,虚线版本是经典的版本,就是当线性增长出现丢包的情况的时候,此时拥塞窗口的大小,就会回到非常小的值,重新指数增长和线性增长。而在新的版本中,拥塞窗口的大小会回到丢包时的1/2,然后进行线性增长。
7、延时应答
延时应答机制是一种基于滑动窗口,尽可能提升效率的机制。
接收方收到数据之后并不会立即返回ack,而是稍等一下,等一下再返回ack,等了这一会,相当于给接收方的应用程序这里,腾出更多时间来消费数据(此时返回的窗口大小就会更大些,下次传输的数据就会更多些)。
我们前面讲,通过滑动窗口来传输数据,如果ack丢了,其实并不会有太大的影响。延时应答也可以按照“ack丢了”的方式来进行处理。
我们可以每隔几个数据,再返回一个ack了(每隔几个数据,就能起到延时应答的效果了)。另外也能减少ack传输的数量,减少开销。
这里的延时应答不仅仅与数据有关,和时间也是有关的,如果延时时间达到了一定程度了,即使数据没到,也会返回ack了。
8、捎带应答
捎带应答,是基于延时应答引入的机制,也是用来提高传输效率。
修改窗口大小是提高效率的有效途径。捎带应答,就是走另一条路,尽可能地把能合并地数据包进行合并,从而起到提高效率地效果。
我们知道:在web开发地很多情况下,客户端和服务器在应用层上都是“一问一答”的,意味着客户端给服务器发了一个request,客户端也会返回一个response,那么此时ack就可以搭乘顺风车(response)一起返回给客户端了。
如下图所示:
ack本身也不携带载荷,只是把报头的ack标志位设置为1,并且确认序号、窗口大小这几个属性,对于response报文没有影响,也不会冲突,因此可以将ack和response一起传输过去。
注意:这里的捎带应答和三次握手还是有本质上的区别的。
三次握手,是一个建立连接的过程,它所发送的是一个没有载荷的数据报,没有任何的业务逻辑。而这里的捎带应答,发送的是有载荷的数据报,是有业务逻辑在里面的。
而且很多时候,客户端和服务器之间都是长连接的,要进行若干次请求和响应的。在捎带应答的加持之下,后续的每次请求响应都是可能触发捎带应答的,都可能把request(response)和ack合二为一的。(注意:这里是有可能触发,具体是否触发,主要取决于下一个request/response来的快不快,如果下一个request/response来得很快。在延时应答的时间之内,就可以触发合并,如果下一个数据来得慢,就无法触发了)。
9、面向字节流
我们创建一个TCP的socket时,就会同时在内核中创建一个发送缓冲区和接收缓冲区:
调用write的时候,数据会先写入发送缓冲区中。如果发送的字节数太长,就会被拆分成多个TCP数据报发送,如果发送的字节数太短,就会在缓冲区里面等待,等数据的数量累积得差不多了再发出去。接收数据得时候,数据也是从网卡驱动程序到达内核的接收缓冲区。然后接收方在应用层调用read方法从接收缓冲区拿到数据。另一方面,对于TCP的一个连接,既有发送缓冲区,也有接收缓冲区,那么对于这一个连接,既可以读数据,又可以写数据——这也印证了TCP是全双工特性。
由于缓冲区的存在和TCP面向字节流的特性,TCP程序的读和些都不需要意义匹配。例如:写100字节数据的时候,完全不需要考虑写的时候,是怎么写的,既可以一次 write100个字节,也可以一次write 1 个字节,重复100次。
这就会导致一个“粘包问题”!!!此处的包,指的是TCP载荷的应用层数据包。
在TCP报头中,只有一个序号这样的字段来进行数据的分隔,站在传输层的角度,TCP是一个一个报文传输过来的,按照序号排列好放在缓冲区中,站在应用层的角度,看到的只是一串连续的字节数据,write和read的时候并没有办法区分当前的数据从哪到哪是一个完整的数据包,这也就导致了“粘包问题”。
举个例子:
假定,有3个TCP数据报携带的都是完整的应用层数据包(因为可能出现一个TCP数据报携带半个或者多个应用层数据包的情况)
当发送方发送数据给接受方之后,接收方的接收缓冲区和传输层角度,这几个数据都是按照序号拍好的顺序:
站在应用层的角度,应用程序,调用read读取数据,由于TCP是面向字节流的,读取过程非常灵活,一次可以读出一个a,也可以一次读出aa,可以一次读出aaa,也可以一次读出aaab……多个应用层数据包之间就混淆不清了,这种情况就被称为“粘包问题”。
“粘包问题”并不是TCP所独有的问题,只要是面向字节流传输,都会有这样的问题,解决这个问题的关键,就是要明确“包之间的边界” 。
1、通过特殊符号,作为分隔符。只要一见到分隔符,应用程序就视为一个包结束了。我们在之前写回显服务器的时候,就是使用了分割符作为边界(空白符+Scanner)。
2、指定出包的长度。比如在包开始的位置,加上一个特殊空间来表示整个数据的长度。
上述的问题,都应该是我们在设计应用层协议的时候把相关问题考虑进去,提前设计好,进行解决的。
相应的,UDP传输就没有这个问题。UDP传输的基本单位是UDP数据报,在UDP这一层就已经分开了,只要约定好每个UDP数据报都只携带一个应用层数据报,就不需要额外的手段来进行区分开了~
UDP的接收缓冲区并不是一个队列的结构,而是类似链表的结构
UDP 的接收缓冲区类似于一个链表,每个链表的结点都是一个 UDP 数据报,通过代码来读取的时候,一次取一个,也就是一个应用层数据包了。
补充:粘包问题,是 TCP 面向字节流引起的,但 TCP 本身是不会有机制负责解决,需要我们程序员知道 TCP 存在粘包问题,通过代码,写应用层逻辑的时候,自己去解决。我们前面提到过的各种常用的应用层协议的格式,xml,json,protobuffer,都能够很好的处理粘包问题。
10、异常情况
异常情况是一些比丢包更严重的特殊情况,这些情况会直接导致我们的网络通信出现严重故障,此时TCP又会如何处理呢?
1、其中有一方出现了进程崩溃
进程无论是正常正常结束,还是异常崩溃,都会触发带回收文件资源,关闭文件这样的操作(操作系统完成),就会触发四次挥手。TCP连接的生命周期,可以比进程更长一些,虽然进程已经退出了,但是TCP连接还在,仍然可以继续进行四次挥手。
2、其中有一方出现了关机(按照正常流程关机)
当有主机,触发了关机操作,就会先强制终止所有进程,终止进程自然就会触发四次挥手。关机之后,四次挥手不一定挥得完,系统就关闭了。如果挥得快,能够顺利挥完,本端和对端就能正确删除掉保存的连接信息。(四次挥手的核心任务)如果挥得不快,至少也能把第一个fin发送给对方,至少能告诉对方,我这边要结束了。对端在收到fin之后。对端就要进入释放连接的过程了,返回ack,返回fin,对端此时如果已经关机,这里发的fin也就不会有ack了,fin没有收到ack之后,势必会进入重传,当重传几次,发现仍然没有ack,此时就会单方面删除链接信息。
3、其中一方出现断电(直接拔电源)
直接断电这个情况,机器瞬间关机,此时肯定是来不及发送fin的,这里分两种情况:
a、断电的是接收方
发送方就会突然发现,没有ack了,此时就需要重传,重传几次后,发现还是不行,就会尝试“复位”连接(相当于清除TCP中的各种临时数据,重新开始),这里的复位操作,需要用到TCP的一个“复位报文段”,它也是六个关键字之一——RST,通过这个报文,直接复位,既往不咎,过往就翻篇了。如果RST也没有返回ack,发送方就会单方面放弃连接。
b、断电的是发送方
这个情况下接收方就需要区分,发送方到底是挂了呢,还是暂时没发送数据。这个时候,接收方在等待一段时间之后,没有收到对方的消息,就会触发“心跳包”来询问下对方的情况(心跳包是一种不带应用层数据包的特殊数据报:1、周期性发送 2、如果没有心跳,就会认为对端是挂了),如果对端没心跳了,此时本端也会尝试复位,如果复位失败,就也会单方面释放连接。
4、网线断开
这种情况就是 3 情况a、b的结合。