前言:网络知识非常的重要,如果你不是做程序的,那么一些网络常识还是得知道的;而做程序的,就更不用说了,不仅需要了解一些网络知识,还是知道其原理,如果不了解原理,不敢说他不是程序员,但是总缺了点意思,就像去北京没去过长城一样。
网络原理系列文章:
因为网络原理不是三言两语可以讲完,如果读者很忙,可以直接拉到最底下,看总结,知道个大概,再回头细读此文章。感谢关注。废话不多说,直接进入主题。在上篇我们已经讲了TCP收发数据的前两步,接下来是最后两步。
将HTTP消息传给协议栈
上篇讲到控制流程从 connect 回到应用程序之后,就到了数据收发阶段。 数据收发数据是从应用程序调用write将要发送的数据交给协议栈开始的,协议栈收到数据后执行发送操作,这一操作包含如下要点。
首先,协议栈并不关心应用程序传来的数据是什么内容。应用程序调用write时会指定发送数据的长度,在协议栈看来,要发送的数据数据就是一定长度的二进制字节序列而已。
其次并不是一收到数据就马上发送出去,而是会将数据存放在内部的发送缓冲区中,并且继续等下一段数据。不过应用程序交给协议栈发送的数据长度是由应用程序本身决定,有些应用程序会一次性传递所有的数据,有些程序则会逐字节或者逐行传递数据。
总之,一次将多少数据交给协议栈是由应用程序决定的,协议栈没有这个控制行为。
协议栈之所以不一收到数据就发出去,是因为那样可能会发送大量的小包,导致网络效率下降。至于积累多少数据才发送,有以下两个要素判断。
第一,每个网络包能容纳的数据长度。协议栈会根据一个叫做MTU的参数来进行判断。MTU表示一个网络包的最大长度,在以太网中一般是1500字节。MTU包含了头部的总长度,所以MTU减去头部长度才是一个网络包所能容纳的最大数据长度,这一长度叫做MSS。当协议栈收到的长度大于或者接近MSS时发送出去,就很好的解决大量小包的问题。
MTU表示一个网络包的最大长度,在以太网中一般是1500字节。MTU包含了头部的总长度,MTU = MSS + 头部,所以MSS是一个网络包所能容纳的最大数据长度。
第二,等待时间。当应用程序发送数据频率不高的时候,协议栈收到的数据要接近MSS,可能要等非常久,而造成发送延迟,所以在这种情况下,即时缓冲区的数据没接到MSS,都发送出去。协议栈内部里面有计时器,经过一定时间,就会把网络包发送出去。
协议栈内部里面有计时器,经过一定时间,就会把网络包发送出去。
读者可以发现,其实这两个判断要素是相互矛盾的。如果长度优先,网络效率会提高,但可能因为等待而产生发送延迟;相反,时间优先,则会降低网络效率,但延迟时间减少。所以这两个要素要综合考虑,以达到平衡。这个平衡由协议栈的开发者来决定,所以不同种类和版本的操作系统在相关操作上也就存在差异。当然应用程序在发送数据时,可以指定发送选项,比如说让网络包直接发送,不用存在缓冲区了。
对较大数据进行拆分
HTTP请求消息一般不会很长,一个网络就可装下,但如果要发送一张图片或者发送一篇长文呢,发送缓冲区的数据肯定超过MSS的长度。这时,我们除了不等到后面的数据,还要对现有数据进行拆分,拆分的每块数据会放进每个单独的网络包。
上一篇也讲过,发送数据前,要在每一块数据添加TCP头部,并根据套接字中包含的通信对象的信息(发送方和接收方的端口号),然后交给IP模块处理发送操作,IP模块会在每个网络包前面添加IP头部和以太网头部,具体操作,后面再讲。
网络错误检测和补偿机制
网络以及其他环境很复杂,收发数据时,难免会在发送中出现错误,所以需要检测和补偿机制。
网络包发往服务器,需要确认对方是否收到网络包,对方没收到时及时重发。那么确认原理是什么?
TCP模块在拆分数据时,会算好每一块数据相当于从头开始的第几个字节,接下来在发送此块数据,会将算好的字节数写在TCP头部中,上一篇中说到的seq作用就在这里。然后告知接收方数据长度,但是数据长度不是通过TCP头部传输,因为接收方可以通过整个网络包的长度减去头部长度得出。所以,我们可以知道发送的数据是从第几个字节开始,长度是多少。
通过上面两个数值,接收方还可以检查收到的网络包有没遗漏。比如:上次接收到第1120字节,如果接下来收到序号是第1121的包,则表示没有遗漏。收到第2200字节,则有包遗漏了。如果确认没有遗漏,接收方会将到目前为止接收到的数据长度加起来,计算出一共已经收到了多少个字节,然后将这个数值写入TCP头部的ACK号中发送给发送方(TCP的seq和ack号计算方法),返回ACK号这一操作称作确认响应。
有个需要注意的是,seq序号不是从1开始,因为从1开始,很容易被猜到,被攻击者发动攻击。所以seq序号初始值是用随机数算出来,开始收发数据前需要告知通信对象序号初始值。上文讲到连接过程中,有一个将SYN控制位设为1并发送给服务器的操作,就是在这一步将序号的初始值告知对方的。实际上,在将SYN设为1的同时,还需要同时设置序号字段的值,而这里的值就是初始值。
通过seq序号和ACK号可以确认数据,我们前面只考虑了单向传输,但TCP数据收发是双向的,所以客户端向服务器发送数据,服务器也会向客户端发送。所以收发双方都需要计算序号,并且在连接过程中相互告诉对方自己计算的序号初始值。
上图表示了实际的工作过程。首先,客户端在连接时需要计算出序号初始值并告知服务器(①)。接下来,服务器会通过初始值计算出ACK号并返回给客户端(②)。初始值有可能在通信中丢失,所以服务器需要返回ACK号给客户端作为确认。因为数据传输是双向,服务器也需要告知客户端它计算出来的序号初始值,并将其发给客户端(②)。接下来,客户端也会计算出ACK号告知服务器,已经收到了其发来的初始值(③)。到此,连接操作工作完成。接下来到收发操作工作,数据收发工作可以双向同时进行。客户端向服务器发送请求,序号也会跟随数据一起发送(④),服务器收到数据返回ACK号(⑤)。同理,服务器向客户端发送数据(⑥⑦)。
在得到对方确认之前,发送过的网络包都会保存在缓冲区中,如果出现丢包现象,也就是通信对象没有返回ACK,协议栈中的TCP模块重新发送这些包。
通过“seq”和“ACK”可以确认对方是否收到网络包。
返回ACK号的等待时间(也叫超时时间),当网络繁忙时会发生拥塞,这时需要把等待时间设置长点,否则重发包了,上次需要返回的ACK号才来,这样会导致本来就拥塞的网络更加要命。如果设置等待时间过长,也不行,重传包会有很大延迟。这又要找一个时间平衡,真难!所以TCP采用了动态调整等待时间的方法。这个等待时间根据ACK号返回所需的时间来判断的。具体来说,TCP会在发送数据的过程中,不断的测量ACK号的返回时间,如果ACK号返回很慢,则延长等待时间,相反,如果返回很快,则缩短等待时间。
采用滑动窗口来管理数据发送和ACK操作
每发送一个网络包,就等到一个ACK号返回,这个很容易理解,但是在等待ACK返回这段时间,如果什么都不做,就非常浪费。为了减少浪费,TCP采用滑动窗口管理数据发送和ACK号的操作。所谓滑动窗口,就是在发送一个包,不等待ACK号返回,直接发送后续的一系列包。
但是这样有可能出现以下问题,在不返回ACK号的时候,就连续发送包,可能导致发送包的频率超过接收方处理能力的情况。具体来说,接收方TCP接收到包,会先将数据存放到接收缓冲区中。然后,接收方需要计算ACK号,将数据块组装起来还原成原本的数据并传递给应用程序,如果该操作未完成,又有下一个包到来,同样是存入接收缓冲区中,如果包到来速率比将数据块组装数据并传给应用程序速率快,缓冲区数据就会越积越多,最后溢出,接收方就收不到后面的包了。所以,接收方需要告诉发送方自己最多能接收多少数据,然后发送方根据这个值对数据发送进行控制,这个最大值称为窗口大小。这就是滑动窗口方式的基本思路。
能够接收的最大数据量称为窗口大小,它属于TCP调优的一个重要参数
ACK与窗口包的合并
前面说过窗口大小就是最大接收量,当接收的数据存入缓冲区中,没必要马上向发送方更新窗口大小,更新窗口大小时机应该是接收方从缓冲区中取出数据传递给应用程序的时候,因为这时,缓冲区中数据减少,剩余的空间变大,理应告诉发送方。
接收方收到数据,确认内容没有问题,就应该向发送方返回ACK号。假设ACK包是一个包,而更新窗口大小又是另外一个包,这样可能会收到一个包的情况下,接收方需要向发送方返回两个包。这样一来,接收方发给发送方的包就太多了,导致网络效率下降。
所以,如果在等待发送ACK的时候,刚好也要更新窗口大小,就可以把这两个包合并成一个包发送,从而减少的包的数量。当需要连续发送多个ACK号,也可以减少包的数量,这是因为ACK号表示的是已经收到的数据量,也就是说,它是告诉发送方目前已接收的数据最后位置在哪里,因为当需要连续发送ACK号时,只要发送最后一个ACK号就可以了。同理,当需要连续发送多个窗口更新也可以减少包的数量。
接收HTTP响应消息
客户端委托协议栈发送请求后,等待服务端返回的消息,调用read程序来获取响应消息。和发送数据一样,接收数据也需要将数据暂存到接收缓冲区中。具体操作如下,协议栈尝试从接收缓冲区取出数据并传递给应用程序,但这个时候可能响应消息还没返回,所以接收操作就没法继续。那么,协议栈会将应用程序的委托,也就是从缓冲区取数据的工作暂时挂起,等响应消息到达再继续接收操作。注意,这里只是挂起这项工作,协议栈并没有停止工作,还会处理好多其他的工作。
应用程序在发送数据和接收数据都依赖协议栈。
协议栈接收数据会先将数据放入缓冲区,然后将数据块按顺序连接,还原成原始数据,最后将数据交给应用程序。具体来说,协议栈会将接收方的数据复制到应用程序指定的内存地址中,然后将控制流程交给应用程序,同时,协议栈还要找到合适时机告诉发送方更新窗口大小。
接收完成与服务器断开
应用程序接收数据,其判断数据被全部接收完成,则这个时间就是收发数据结束的时间。协议栈在设计上允许通信双方的任意一方先发起断开过程。大部分程序向服务器发送请求消息,服务器再返回响应消息,这时收发数据的过程就全部结束了,服务器一方会先发起断开过程。也有一些程序是发完数据就先发起断开过程。
协议栈在设计上允许通信双方的任意一方先发起断开过程,具体哪方先断开,由那方的程序决定。
我们以常见的服务器断开讲解。首先,服务器一方的程序会调用Socket库的 close 程序。然后,服务器的协议栈会生成包含断开信息的 TCP 头部,具体来说就是将控制位的 FIN 比特设为1。接下来,协议栈会委托IP模块向客户端发送数据。同时,服务器的套接字中也会记录下断开操作的相关信息。
客户端收到服务器发来的 FIN 为 1 的TCP头部时(①),客户端协议栈会将自己的套接字标记进入断开操作状态。然后,为了告知服务器已经收到 FIN 的包,客户端会向服务器返回一个 ACK 号(②)。这些操作完成后,就等待应用程序来取数据了。
过了一会,应用程序就回来调用 read 来读取数据。这时,协议栈不会向应用程序传递数据,而是会告知应用程序来自服务器的数据已经全部收到,客户端收到全部数据,也会调用 close 结束数据收发操作,这时客户端的协议栈也会和服务器一样,生成一个FIN比特为1的TCP包,然后委托IP模块发送给服务器(③)。隔一段时间,服务器就会返回ACK号(④)。到此,客户端和服务器的通信全部结束。
删除连接管道
有没有记到前面说过,通信双方在连接阶段中间类似有一条管道,准备连接时,我们建立,现在收发数据结束,我们理应要删除它,其实也就是删除这条虚拟管道的两方套接字。
通信结束之后,我们要删除套接字,不过,套接字不会立即被删除,而是会等待一段时间之后再被删除。等待一段时间是为了防止误操作,引起误操作的原因很多,比如说: 1、客户端发送FIN 2、服务器返回ACK号 3、服务器发送FIN 4、客户端发送ACK号
如果最后客户端返回的ACK号丢失了,服务器没有接受到ACK号,它可能会重新发送一次FIN。如果这个时候,客户端的套接字已经删除,那么套接字中保存的开工至信息也跟着消失,套接字对应的端口号就会被释放出来。这时,如果别的应用程序创建套接字,新套接字刚好被分配了同一个端口号,而服务器重发的FIN正好到达,这个时候,FIN就会错误的跑到新套接字里面,新套接字就开始执行断开操作了。所以不马上删除套接字,就是由于这样。
客户端的端口号是从空闲的端口号中随意选择的。
等待多长时间才删除套接字,这得看包重传的操作方式。网络包丢失之后会进行重传,这操作一般要持续几分钟。如果重传了几分钟之后依然无效,则停止重传。所以一般等待几分钟之后再删除套接字。
总结
TCP收发数据的整体流程分为以下三个部分。
收发数据三个步骤开始前的操作是创建套接字,应用程序调用Socket库的一个程序组件socket程序申请创建套接字,之后协议栈去执行操作。
一、连接操作。创建完套接字,就准备连接通信对象。首先,客户端会生成一个SYN为1的TCP包并发给服务器。这个TCP包的头部包含了客户端向服务器发送数据时使用的seq(初始序号),以及服务器发送数据给客户端需要用到的窗口大小。这个包到达服务器后,服务器会返回一个SYN为1的TCP包,这个TCP包同样包含着序号和窗口大小,此外还包含表示已经收到客户端发来的TCP包的ACK号。过段时间,客户端会返回ACK号,表示已经收到服务器发送的TCP包。
二、收发操作。不同应用程序可能会有些异同。一般。客户端会向服务器发送请求消息。TCP会将数据拆分成很多个网络包分别发送出去。每个包的TCP头部都包含这序号,表示当前发送的是第几个字节数据。服务器收到包后,会返回ACK号,一定时间后也会返回更新窗口大小的包。当然通信是双向的,服务器也会向客户端发送数据,也是类似的流程。
三、断开操作。一般,服务器会先发起断开过程。服务器先发一个 FIN 为1的TCP包给客户端,客户端返回 ACK号作为确认收到。客户端收到全部数据,也会生成一个 FIN 比特为1的TCP包,发送给服务器,服务器也返回ACK号,等待一段时间后,套接字会被删除。到此,客户端和服务器的通信全部结束。
参考文献:
网络是怎样连接的
欢迎关注技术公众号「程序员大咖秀」