探索协议栈和网卡——收发数据

1、将HTTP请求消息交给协议栈:

上一篇文章讲到了Socket库中connect组件委托协议栈实现连接服务器的操作,那么接下来就进入到了数据收发阶段,也就是Socket库中对应的write组件和read组件所实现的功能,以及委托到协议栈涉及的操作。

从Socket库的角度看,应用程序调用write时会指定发送数据的长度,所以在协议栈看来,要发送的数据就是一定长度的二进制字节序列。其次,协议栈的收发不是立即操作的,而是先放入内部缓存区中等待应用程序的下一段数据。其实这还是需要根据程序的特点来标定的,有些应用程序会一次性传递所有数据,而有些应用程序则逐字或逐行传递数据,所以协议栈往往会在数据积累到一定量时才会发送出去。

决定这个量的大小的因素主要有两个:

第一个因素是每个网络包能够容纳的最大数据长度MSS(Maximum Segment Size 最大分段大小),在说MSS之前我们先来了解另一个名词MTU(Maximum Transmission Unit 最大传输单元),MTU表示一个网络包的最大长度,在以太网中一般为1500字节。MTU是包含头部的总长度,而MSS则是去除头部之后的数据长度。具体如下图(起始帧分界符 Start Frame Delimiter SFD、帧校验序列 Frame Check Sequence FCS):

另一个因素则是时间,这个其实很好理解,假设应用程序发送数据频率不高的时候,协议栈不可能一直等到缓冲区的数据长度接近MSS才发送,不然可能需要等待很长的时间,这样就会赞成发送的延迟。这个时间会由协议栈的内部的计时器去计算判断。

其实这两个因素是相互矛盾的,长度越长那么很可能会因为等待填满缓存区的时间就会越长,网络延迟就会越高;相反地,如果时间优先,那么可能发送包的数据就会相对较少,网络效率就会很低。所以需要掌握一定的平衡,而这种平衡实际由操作系统的开发者来决定。所以有些时候,可能就不需要等待缓冲区而使用直接发送的选项。

2、对较大的数据进行拆分:

当需要发送的消息超过MSS的时候,缓冲区中的数据会被以MSS长度为单位进行拆分,拆分出来的每块数据会被放进单独的网络包中。具体情况如下图:

3、使用ACK号确认网络包已收到

网络包装好之后就会发送到服务器了,服务器在接收到网络包后会进行拆分并确认响应,确认的原理如下:

TCP模块在拆分数据包时会先算好每一块数据相当于从头开始的第几字节,之前讲到的序号就是排在这个用途上,它记录的就是这个数据开始部分在总数据的哪个位置,然后再根据这个数据包判断出这个数据的大小,这样就知道了目前接收到的这个数据是从整个数据的哪个位置开始,长度是多少了。

接着接收方在接收到发送过来的数据包之后会将响应数据包中的ACK号值设置为发送过来的数据包中的序号(假设为1)与长度(假设为1460)的和,也就是告诉发送方我已经接收到了1461字节之前的数据了。当然接收方并不会马上做出响应,它会将到目前为止接收到的数据长度加起来,计算出一共收到了多少字节。还有一点值得注意,那就是第一个数据包发送过来的序号不一定为1,它会是一个随机数,这样就不容易遭受到恶意的攻击。那么,接收端是如何知道开始的序号是多少呢?其实,在进行收发数据之前的连接阶段客户端发送过来的第一个SYN包中就包含了开始序号,它会告知接收方客户端的第一个初始序号是什么。

当然收发数据其实是双向的,在客户端向服务器发送数据的同时,服务器也会向客户端发送数据,所以服务器也需要告知客户端自己的初始序号。因此双方需要在连接过程中互相告知自己计算的序号初始值。

前面讲到,TCP会将数据放在缓冲区等到达到MSS或者一定的时间发送出去,同样的TCP只有在接收到某些包对应的ACK号之后才会将发送过的包从缓存区中清除,否则会重新发送,当然也不会一直进行重发,TCP会在尝试几次重传无效之后强制结束通信,并向应用程序报错。

4、根据网络包平均往返时间调整ACK号等待时间:

网络是一个庞大公开的体系,所以网络情况也是无法琢磨,所以返回ACK号的等待时间也是需要不断的调整的。当网络传输繁忙时就会发生拥塞,ACK号的返回就会变慢,这个时候必须将等待时间设置得稍微长一点,否则可能会发生重传了包之后前面的ACK号才姗姗来迟的情况。

所以TCP采用了动态调整等待时间的方法,它是根据ACK号返回所需的时间来判断的。TCP会持续的测量ACK号的返回时间,然后对等待时间进行延长或者缩短。

5、使用窗口有效管理ACK号:

之前说到接收方在接收到数据包后会等待一定的时间,然后计算出一共接收到多少个字节并响应相应的ACK号。同样的发送方也并不是每发送一个数据包后等待对应的ACK号响应之后再发送另一个数据包的。TCP会采用滑动窗口的方式来管理数据发送和ACK号的操作,窗口其实可以理解为接收方目前可以接收的数据的大小(接收方和发送方一样也存在一个存放数据包的缓冲区,当接受到的数据包过多时就会发生溢出的情况,这样对于后面发送的数据包就无法接收),无论是在连接阶段还是在收发阶段TCP数据包都会发送对应的窗口大小,它是为了告知发送方目前可以一次性发送的最大的数据量。

6、ACK和窗口的合并:

当然窗口也并不是每时每刻都需要立刻告知发送方的,与ACK号一样,接收方会接收一定的数据包并放入缓冲区中,直到应用程序将数据从缓冲区中取出之后,接收方的窗口大小才会发生变化,而这个变化的过程是不需要每时每刻都发送给发送方的。所以,ACK号和窗口一样,都可以省略中间过程,只要发送最终的结果就可以了。

7、接收HTTP响应消息:

最后的操作就是调用read程序来获取响应消息。控制流程会通过read转移到协议栈,然后协议栈会执行接下来的操作。

首先,协议栈尝试从接收缓冲区中取出数据并传递给应用程序,但这个时候请求消息刚刚发送出去,响应消息可能还没有返回,因此此时接收缓冲区中并没有数据,那么接收数据的操作也就无法继续。这个时候从接收缓冲区中取出数据并传递给应用程序的工作暂时挂起(协议栈会继续执行其他的工作),等服务器返回的响应消息到达之后再继续执行接收操作。

在接收到响应消息之后,首先,协议栈会检查收到的数据块和TCP头部的内容,判断是否有数据丢失,如果没有返回ACK号。然后,协议栈将数据块暂时存到接收缓冲区中,并将数据块按顺序连接起来还原出原始的数据,最后将数据交到应用程序。协议栈还需要找到合适的时机向发送方发送窗口更新。

猜你喜欢

转载自blog.csdn.net/qq_38386085/article/details/90380091