https://www.jianshu.com/p/a819a777d33c
连接建立起来后,也就是TCP建链后也就进入数据传输阶段。数据收发操作是从应用程序调用write将要发送的数据交给协议栈开始的,协议栈收到数据后执行发送操作。
首先,协议栈并不关心应用程序传来的数据是什么内容。应用程序在调用write时会指定发送数据的长度,在协议栈看来,要发送的数据就是一定长度的二进制字节序列而已。
其次,协议栈并不是一收到数据就马上发送出去,而是会将数据存放在内部的发送缓冲区中,并等待应用程序的下一段数据。
应用程序交给协议栈发送的数据长度是由应用程序本身来决定的,不同的应用程序在实现上有所不同,有些程序会一次性传递所有的数据,有些程序则会逐字节或者逐行传递数据。总之,一次将多少数据交给协议栈是由应用程序自行决定的,协议栈并不能控制这一行为。
在这样的情况下,如果一收到数据就马上发送出去,就可能会发送大量的小包,导致网络效率下降,因此需要在数据积累到一定量时再发送出去。至于要积累多少数据才能发送,不同种类和版本的操作系统会有所不同,不能一概而论,但都是根据下面几个要素来判断的。
第一个判断要素是每个网络包能容纳的数据长度,协议栈会根据MTU(最大传输单元Maximum Transmission Unit)参数来进行判断。MTU表示一个网络包的最大长度,在以太网中一般是1500字节。MTU是包含头部的总长度,因此需要从MTU减去头部的长度,然后得到的长度就是一个网络包中所能容纳的最大数据长度,这一长度叫作MSS(最大分段大小Maximum Segment Size)。当从应用程序收到的数据长度超过或者接近MSS时再发送出去,就可以避免发送大量小包的问题了。
MSS:Maximum Segment Size,最大分段大小。TCP和IP的头部加起来一般是40字节,因此MTU减去这个长度就是MSS。例如,在以太网中,MTU为1500,因此MSS就是1460。TCP/IP可以使用一些可选参数(protocol option),如加密等,这时头部的长度会增加,那么MSS就会随着头部长度增加而相应缩短。
另一个判断要素是时间。当应用程序发送数据的频率不高的时候,如果每次都等到长度接近MSS时再发送,可能会因为等待时间太长而造成发送延迟,这种情况下,即便缓冲区中的数据长度没有达到MSS,也应该果断发送出去。为此,协议栈的内部有一个计时器,当经过一定时间之后,就会把网络包发送出去。
判断这两个要素,其实是互相矛盾的。如果长度优先,那么网络的效率会提高,但可能会因为等待填满缓冲区而产生延迟;相反地,如果时间优先,那么延迟时间会变少,但又会降低网络的效率。因此,发送操作时需要综合考虑这两个要素以达到平衡。不过,TCP协议规格中并没有告诉我们怎样才能平衡,因此实际如何判断是由协议栈的开发者来决定的,也正是由于这个原因,不同种类和版本的操作系统在相关操作上也就存在差异。
应用程序在发送数据时可以指定一些选项,如果指定“不等待填满缓冲区直接发送”,则协议栈就会直接发送数据。像浏览器这种会话型的应用程序在向服务器发送数据时,等待填满缓冲区导致延迟会产生很大影响,因此一般会使用直接发送的选项。
HTTP请求消息一般不会很长,一个网络包就能装得下,但如果其中要提交表单数据,长度就可能超过一个网络包所能容纳的数据量,比如在博客或者论坛上发表一篇长文,这种情况下,发送缓冲区中的数据就会超过MSS的长度,发送缓冲区中的数据会被以MSS长度为单位进行拆分,拆分出来的每块数据会被放进单独的网络包中。
根据发送缓冲区中的数据拆分情况,当需要发送这些数据时,就在每一块数据前面加上TCP头部,并根据套接字中记录的控制信息标记发送方和接收方的端口号,然后交给IP模块来执行发送数据的操作。
TCP模块在拆分数据时,会先算好每一块数据相当于从头开始的第几个字节,接下来在发送这一块数据时,将算好的字节数写在TCP头部中,“序号”字段就是派在这个用场上的。然后,发送数据的长度也需要告知接收方,这个并不是放在TCP头部里面的,而是通过整个网络包的长度减去头部的长度得到数据的长度。
通过这些信息,接收方检查收到的网络包有没有遗漏。假设上次接收到第1460字节,接下来收到序号为1461的包,说明中间没有遗漏;但如果收到的包序号为2921,那就说明中间有包遗漏了。像这样,如果确认没有遗漏,接收方会将到目前为止接收到的数据长度加起来,计算出一共已经收到了多少个字节,然后将这个数值写入TCP头部的ACK号中发送给发送方。
在实际的通信中,序号并不是从1开始的,而是需要用随机数计算出一个初始值,这是因为如果序号都从1开始,通信过程就会非常容易预测,有人会利用这一点来发动攻击。开始收发数据之前将初始值告知通信对象,将SYN控制位设为1并发送给服务器的操作,就是在这一步将序号的初始值告知对方的。
前面只考虑了单向的数据传输,但TCP数据收发是双向的,在客户端向服务器发送数据的同时,服务器也会向客户端发送数据,怎么解决将初始值告知对端呢?
首先客户端先计算出一个序号,然后将序号和数据一起发送给服务器,服务器收到之后会计算ACK号并返回给客户端;相反地,服务器也需要先计算出另一个序号,然后将序号和数据一起发送给客户端,客户端收到之后计算ACK号并返回给服务器。
TCP采用确认对方是否收到了数据的方式,在得到对方确认之前,发送过的包都会保存在发送缓冲区中。如果对方没有返回某些包对应的ACK号,那么就重新发送这些包。
因此,网卡、集线器、路由器都没有错误补偿机制,一旦检测到错误就直接丢弃相应的包。应用程序也一样,因为采用TCP传输,即便发生一些错误对方最终也能够收到正确的数据,所以应用程序只管自顾自地发送这些数据就好了。不过,如果发生网络中断、服务器宕机等问题,那么无论TCP怎样重传都不管用。这种情况下,无论如何尝试都是徒劳,因此TCP会在尝试几次重传无效之后强制结束通信,并向应用程序报错。
当网络传输繁忙时就会发生拥塞,ACK号的返回会变慢,这时我们就必须将等待时间设置得稍微长一点,否则可能会发生已经重传了包之后,前面的ACK号才姗姗来迟的情况。因为ACK号的返回变慢大多是由于网络拥塞引起的,如果此时再出现很多多余的重传,对于本来就很拥塞的网络来说无疑是雪上加霜。
等待时长设置多大合适呢?TCP采用了动态调整等待时间的方法,这个等待时间根据ACK号返回所需的时间来判断的。具体来说,TCP会在发送数据的过程中持续测量ACK号的返回时间,如果ACK号返回变慢,则相应延长等待时间;相对地,如果ACK号马上就能返回,则相应缩短等待时间。
每发送一个包就等待一个ACK号的方式是最简单也最容易理解的,但在等待ACK号的这段时间中,如果什么都不做那实在太浪费了。为了减少这样的浪费,TCP采用滑动窗口方式来管理数据发送和ACK号的操作。所谓滑动窗口,在发送一个包之后,不等待ACK号返回,直接发送后续的一系列包。这样一来,等待ACK号的这段时间就被有效利用起来了。
在一来一回方式中,接收方完成接收操作后返回ACK号,然后发送方收到ACK号之后才继续发送下一个包,因此不会出现发送的包太多接收方处理不过来的情况。但如果不等返回ACK号就连续发送包,就有可能会出现发送包的频率超过接收方处理能力的情况。
当接收方的TCP收到包后,会先将数据存放到接收缓冲区中。然后,接收方需要计算ACK号,将数据块组装起来还原成原本的数据并传递给应用程序,如果这些操作还没完成下一个包就到了也不用担心,因为下一个包也会被暂存在接收缓冲区中。如果数据到达的速率比处理这些数据并传递给应用程序的速率还要快,那么接收缓冲区中的数据就会越堆越多,最后就会溢出。缓冲区溢出之后,后面的数据就进不来了,因此接收方就收不到后面的包了,这就和中途出错的结果是一样的,也就意味着超出了接收方处理能力。
首先接收方需要告诉发送方自己最多能接收多少数据,然后发送方根据这个值对数据发送操作进行控制,这就是滑动窗口方式的基本思路。
能够接收的最大数据量称为窗口大小,它是TCP调优参数中非常有名的一个。
要提高收发数据的效率,还需要考虑另一个问题,那就是返回ACK号和更新窗口的时机。
首先,什么时候需要更新窗口大小呢?当收到的数据刚刚开始填入缓冲区时,没必要每次都向发送方更新窗口大小,因为只要发送方在每次发送数据时减掉已发送的数据长度可以计算出当前窗口的剩余长度。因此,更新窗口大小的时机应该是接收方从缓冲区中取出数据传递给应用程序的时候。这个操作是接收方应用程序发出请求时才会进行的,而发送方不知道什么时候会进行这样的操作,因此当接收方将数据传递给应用程序,导致接收缓冲区剩余容量增加时,就需要告知发送方,这就是更新窗口大小的时机。
ACK号又是什么情况呢?当接收方收到数据时,如果确认内容没有问题,就向发送方返回ACK号。
首先,发送方的数据到达接收方,在接收操作完成之后就需要向发送方返回ACK号,而再经过一段时间,当数据传递给应用程序之后才需要更新窗口大小。但如果根据这样的设计来实现,每收到一个包,就需要向发送方分别发送ACK号和窗口更新这两个单独的包[插图]。这样一来,接收方发给发送方的包就太多了,导致网络效率下降。
首先,浏览器在委托协议栈发送请求消息之后,会调用read程序来获取响应消息。然后,控制流程会通过read转移到协议栈,协议栈尝试从接收缓冲区中取出数据并传递给应用程序,但这个时候请求消息刚刚发送出去,响应消息可能还没返回。响应消息的返回还需要等待一段时间,因此这时接收缓冲区中并没有数据,那么接收数据的操作也就无法继续。这时,协议栈会将应用程序的委托,也就是从接收缓冲区中取出数据并传递给应用程序的工作暂时挂起,等服务器返回的响应消息到达之后再继续执行接收操作。
本文摘取自周自恒翻译的户根勤编写的《网络是怎样连接的》。