网络那些事儿2-数据途经网卡发生的事儿2

数据途经网卡发生的事儿2

上一节讲到协议栈连接操作已经完成,进入到了数据的收发阶段,这一节我们讲一下数据的收发和协议栈的断开操作。

数据收发

当控制流程从connect回到应用程序后,接下来就是数据的收发阶段,应用程序调用socket的write函数讲要发送的数据交给协议栈,协议栈接收到数据后执行发送操作。

协议栈在执行发送数据的过程中不关心发送的具体内容,应用程序在调用write时会指定数据的长度,对协议栈来说要发送的数据就是一定长度的二进制序列(010100010……)。Write函数被调用时协议栈不是立即执行发送操作,而是会先把数据暂存到缓冲区,然后等待应用程序的下一段数据。这样做的目的是为了提高网络效率,因为如果每次应用程序交给协议栈的数据都是立即被发送出去,不同应用程序发送的数据长度是不同的,这样就会导致要发送大量不同长度的网络包,导致网络效率下降。所以协议栈设计了一个缓存区,缓存区数据达到一定的长度后再一次性发送出去。协议栈缓存区的大小在不同操作系统中是不一样的,主要根据一下这几个要素来判断。

1.每个网络包能容纳的数据长度。协议栈中有一个叫MTU(最大传输单元)的定义,表示一个网络包的最大传输单元,去掉网络包中的头部信息,剩下的就是网络包中能够容纳的最大传输数据长度,叫MSS(最大分段大小),当协议栈从应用程序收到的数据超过或者接近MSS时再发送出去,就能够有效避免发送大量的小包的问题了。
网络包组成

2.缓冲区等待时间。应用程序要发送的数据会首先存放到缓冲区,这里会有另外一个问题,如果应用程序的数据很长时间都填充不满缓冲区,难道协议栈也要一直等待缓冲区被填满才发送吗?显然不是这样的,等待时间太长会造成发送延迟,这种情况下即使缓冲区没有达到MSS,也应该要发送出去,这里协议栈定义了一个定时器(通常时毫秒级的定时器),定时器时间一到即使缓冲区达不到MSS长度也会将数据发送出去。

这两个相互矛盾,如果缓冲区长度优先的化,那么网络传输效率肯定会受到影响,但是如果时间优先,那又会造成发送数据包数量增加,降低网络传输效率,开发者需要根据实际情况设置合理值来平衡这两个要素,正式这个原因,不同操作系统在相关操作上存在一些差异。协议栈给应用程序保留了控制发送时间的接口,应用程序在发送数据时可以指定一些选项(不等待缓冲区填满就发送数据),比如浏览器这种会话在向服务器发送数据时,就采用的直接发送选项,因为等待缓冲区填满会导致延迟产生比较大的影响。

数据拆分

MTU的长度是有限的,MSS也是有限的,这就存在一个问题就是,如果要发送的数据比较大,长度超过MSS大小,这种情况下协议栈怎么处理呢?这就涉及到网络包的分片了。

如果要发送的数据大于MSS的长度,那缓冲区中的数据会被以MSS长度为单位进行拆分,拆分出来的每块数据都会被放到单独的网络包中,每个网络中都会加上TCP的头部信息,并在套接字中加入标记发送方和接收方的端口号,然后交给IP模块进行数据的发送操作。分片过程如下图所示。
分片过程

到这里网络包已经装载好了,开始发往服务器了,但是数据发送还没结束,因为还要确认网络包是否到达了服务器,我们说TCP是可靠连接,这就是原因。TCP能够确认对方是否收到了网络包,以及当对方没有收到网络包时进行重发操作。确认操作的原理是这样的,TCP拆分数据包时会提前计算好每一块数据相当于从头开始的第几个字节,发送数据时候将计算好的该字节数填入到头部中的“序号”这个字段中,发送的每个网络包数据的长度也会告知接收方,不过这个长度不是直接告诉接收方的,需要接收方根据整个网络包的长度减去头部的长度得到这个数据。

通过上面两个数据,接收方就可以确定自己是否接收到了完整的数据,比如,上次接收到第1460字节的数据,那么接下来就应该接收到序号为1461字节开头的数据包,这样才能说明中间没有遗漏。但是如果接收到的时2921,那么说明中间的网络包肯定是遗漏掉了。如果最后确认没有遗漏的网络包,接收方就会根据目前位置接收到的数据,将其数据长度加起来,计算出一共接收到了多少字节的数据,然后将这个数值写入TCP头部的ACK中返回给发送方,这就是确认响应,发送方就能确认对方到底接受了多少数据。
发送过程

刚才举的例子是单项传输的情况,其实TCP是双向传输的,客户端给服务端发送数据的同时,服务端也在给客户端发送数据,这种情况下,直接按照刚才的流程,从服务端发送数据到客户端就行了,服务端会先计算出一个序号,然后将序号和数据一起发送给客户端,客户端接收到后计算ACK并返回给服务端。

实际情况中,客户端先计算出序号值发送给服务端,服务端返回一个ACK和服务端计算除的序号值,客户端接收到后根据ACK能够确认务端接收到了多少数据,另外客户端接收到服务端发来的序号值后会返回给服务端一个ACK信号,然后服务端接收这个ACK信号后,接下来就进入到实际的数据收发阶段了,数据收发是双向同时进行的,先由客户端发送请求,序号跟随数据一起被发送,服务端接收到数据后返回ACK。

TCP就是通过这种方式确认数据接收方是否接收到了数据,在得到对方确认前,数据都是存放到发送缓冲区的,如果对方在一定的时间范围内没有返回确认响应,就重发这些包。这一机制保证了即使在网络中存在错误,我们也可以通过重传机制保证数据能够发送出去,直到对方成功接收。也正是协议栈拥有这种保证机制,网卡,路由器,集线器中都没有相似的错误补偿机制,一旦检测到错误就丢弃网络包。应用程序中也是只管发送数据就行,不用考虑其他问题。但是如果网络线路被切断,或者服务端宕机的话TCP重传在多次也无用,因此TCP还有一个机制是重传一定次数后会结束通信,并报告错误。

其实网络的错误检测和补偿机制非常复杂,等待ACK信号返回的时间就需要动态调整,因为如果遇到网络拥堵的情况,ACK信号返回的就比较慢,这种情况下我们的等待时间如果设置得比较小就会造成数据包重传的情况,可能在重新发送数据后我们才收到ACK信号,这样就会造成不必要的重传,导致网络更加拥堵,雪上加霜。如果等待时间设置的太大就会造成不必要的等待时间,这里协议栈是通过根据ACK信号返回的时间动态调整等待时间来平衡这个问题的,具体来说就是TCP在数据发送过程中持续监测ACK信号的返回时间,如果ACK返回得比较慢,就增加等待时间,反之减少等待时间。

还有一个问题是,没发送一个包就等待一个ACK信号的话效率太低,我们在等待ACK信号的这段时间什么也不做的话,浪费计算机和网络资源,所以TCP采用了滑动窗口的方式管理数据的发送和ACK响应。这种模式下在发送完一个数据包后不等待其ACK返回就接着发送另一个网络包。这种情况下,如果数据包发送频率较快,接收方的缓冲区已经被填满怎么办呢?丢弃掉后后来的包,直到缓冲区由空间接收数据包为止。这种情况下,TCP是这样避免的,接收方会告诉发送方自己最多能接收到少数据,发送方根据这个值来对数据进行发送控制,这就是滑动窗口的基本思路。
滑动窗口

还有一个问题,什么时候更新窗口大小合适呢?当数据刚填入缓冲区时,是没必要更新窗口的大小的,因为发送方可以根据接收方缓存区大小减去发送的数据长度就能直到接收方的缓冲区还有多大,所以更新窗口大小的时机应该是接收方缓冲区接收方的应用程序取走一部分数据时进行更新,并通知发送方,因为发送方是不知道这个动作什么时候发生的,必须主动通知发送方,接收方应用程序取走缓冲区数据的时候就是通知发送方更新窗口大小的时候。

另外ACK信号什么时候返回呢?如果接收方在接收到数据后立即返回ACK给发送方,而更新滑动窗口大小的操作也需要发送数据包给发送方,这样网络中就存在这两种单独的包,数据包的收发太多,会导致网络效率下降。这里TCP的做法是接收方在发送ACK和更新窗口的包时,不立即发送出去,而是等待一段时间,在等待过程中,如果有其他的通知操作,就连同ACK和更新窗口的包一同发送。

数据接收

浏览器委托协议栈通过HTTP请求将数据发送给服务端后,接下就是等待服务端的响应,通过socket的read函数来接收数据,read将控制流程交给协议栈,协议栈通过读取缓冲区的数据交给应用程序来完成read操作。这里有一个问题就是刚发送完请求,服务端还没有把数据返回过来,这是缓冲区是空的,响应消息可能还要等待一段时间才能返回,这是协议栈会将浏览器读取消息的委托暂时挂起,等待服务端返回响应消息后再继续执行read的相关操作。

总结

这里简单总结一下协议栈接收数据的操作,首先,协议栈会检查收到的数据块的TCP头部的内容,判断数据是否有丢失,如果没有问题就返回ACK信号,然后协议栈将数据暂存到缓冲区,并将数据块按顺序连接起来还原出原始的数据吗,最后将数据交给应用程序。具体来说就是协议栈会将接收到的数据复制到应用程序指定的内存地址中,将数据交给应用程序后,协议栈还需要寻找合适的时机向发送方发送窗口更新和ACK响应。