• 网络是怎样连接的--TCP大致控制流程


    5.1 协议栈内部结构

    协议栈的内部结构中上下关系是有一定规则的,上面的部分会向下面的部分委派工作,下面的部分接受委派的工作并实际执行;当然,这一上下关系只是一个总体的规则,其中也有一部分上下关系不明确,或者上下关系相反的情况,所以也不必过于纠结。

    在这里插入图片描述

    • 图中最上面的部分是网络应用程序,也就是浏览器、Web服务器等程序,它们会将收发数据等工作委派给下层的部分来完成。应用程序的下面是Socket库,其中包括解析器,解析器用来向DNS服务器发出查询(可以通过此文最下面链接进行 查找对此的讲解).

    再下面就是操作系统内部,其中包括协议栈:

    • 协议栈的上半部分有两块,分别是负责用TCP协议收发数据的部分和负责用UDP协议收发数据的部分,它们会接受应用程序的委托执行收发数据的操作。

      浏览器、邮件等一般应用程序收发数据时用TCP;DNS查询等收发较短的控制数据时用UDP .

    • 协议栈的下半部分是用IP协议控制网络包收发操作的部分。在互联网上传送数据时,数据会被切分成一个一个的网络包,而将网络包发送给通信对象的操作就是由IP来负责的。此外,IP中还包括ICMP协议和ARP协议。ICMP(Internet Control Message Protocol)用于告知网络包传送过程中产生的错误以及各种控制消息,ARP(Address Resolution Protocol)用于根据IP地址查询相应的以太网MAC地址。

    • IP下面的网卡驱动程序负责控制网卡硬件,而最下面的网卡则负责完成实际的收发操作,也就是对网线中的信号执行发送和接收的操作。

    5.2 套接字作用

    在协议栈内部有一块用于存放控制信息的内存空间,这里记录了用于控制通信操作的控制信息,例如通信对象的IP地址、端口号、通信操作的进行状态等。本来套接字就只是一个概念而已,但为了理解一般说存放控制信息的内存空间就是套接字的实体。

    协议栈在执行操作时需要参阅这些控制信息,例如,在发送数据时,需要看一看套接字中的通信对象IP地址和端口号,以便向指定的IP地址和端口发送数据。在发送数据之后,可能因为数据丢失,永远等不来服务器的响应,套接字就必须要根据记录的发送数据以后经过了多长时间这些信息按照需要执行重发操作。

    上面说的只是其中一个例子。套接字中记录了用于控制通信操作的各种控制信息,协议栈便是根据套接字中记录的这些控制信息来工作的,这就是套接字的作用.

    例如: 下图在Windows命令行中的每一行记录都可以算作某个套接字的一种控制信息;

    比如第6行的控制信息就表示: 正在使用IP地址为10.11.111.1,端口号为54698,PID为35136的程序正在和IP地址108.177.97.188和端口号5228的对象进行通信; 而像第一行到第四行这种表示通信还未开始正在等待连接;

    5.3 创建套接字

    如图所示,应用程序调用socket申请创建套接字,协议栈根据应用程序的申请执行创建套接字的操作。

    在这个过程中,协议栈首先会分配用于存放一个套接字所需的内存空间,由于套接字刚刚创建,数据收发操作还没有开始,因此需要在套接字的内存空间中写入表示这一初始状态的控制信息。到这里,创建套接字的操作就完成了。

    接下来,协议栈就将表示这个套接字的描述符告知应用程序,当应用程序向协议栈进行收发数据委托时就提供这个描述符;

    由于套接字中记录了通信双方的信息以及通信处于怎样的状态,协议栈就能够根据描述符找到目标套接字获取所有的相关信息,这样一来,应用程序就不需要每次都告诉协议栈应该和谁进行通信了。

    5.4 两类控制信息

    套接字刚刚创建完成的时候,里面并没有存放任何数据,也不知道通信的对象是谁,在这个状态下,如果应用程序要求发送数据,协议栈将不知道数据应该发送给谁;

    但是浏览器可以根据网址来查询服务器的IP地址,而且根据规则也知道应该使用80号端口,因此,我们需要把服务器的IP地址和端口号等信息告知协议栈,这便是连接操作的目的之一。

    之前我们讲过,连接实际上是通信双方交换控制信息,在套接字中记录这些必要信息并准备数据收发的一连串操作,像上面提到的客户端将IP地址和端口号告知服务器这样的过程就属于交换控制信息的一个具体的例子。连接操作中所交换的控制信息是根据通信规则来确定的,只要根据规则执行连接操作,双方就可以得到必要的信息从而完成数据收发的准备

    此外,当执行数据收发操作时,我们还需要一块用来临时存放要收发的数据的内存空间,这块内存空间称为缓冲区,它也是在连接操作的过程中分配的。

    所以: 连接所要做的事情就是 通信双方交换必要的控制信息完成数据收发的准备,同时准备一块用于收发数据的内存空间

    而上面所说的交互控制信息则记录在TCP协议的头部,内容如下:
    在这里插入图片描述

    控制信息还有另外一类,就是和协议栈本身程序融为一体的信息,因此,“协议栈具体需要哪些信息”会根据协议栈本身的实现方式不同而不同,但这并没有什么问题。因为协议栈中的控制信息通信的程序对方是看不见的,只要在通信时按照规则将必要的信息写入头部,客户端和服务器之间的通信就能够得以成立。例如,Windows和Linux操作系统的内部结构不同,协议栈的实现方式不同,必要的控制信息也就不同,但两种系统之间依然能够互相通信。正如前面所说,协议栈的实现不同,因此我们无法具体说明协议栈里到底保存了哪些控制信息,但可以用命令来显示一些重要的套接字控制信息,如最上面的Windows中cmd命令行里面的信息,这些信息无论何种操作系统的协议栈都是共通的,通过理解这些重要信息,就能够理解协议栈的工作方式了。

    5.5 连接操作的实际过程

    我们已经了解了连接操作的含义,下面来看一下具体的操作过程。这个过程是从应用程序调用Socket库的connect开始的。

    在这里插入图片描述

    上面的调用提供了服务器的IP地址和端口号(浏览器向DNS服务器查询得到),这些信息会传递给协议栈中的TCP模块。然后,TCP模块会与该IP地址对应的对象,也就是与服务器的TCP模块交换控制信息,这一交互过程包括下面几个步骤。

    首先,客户端先创建一个包含表示开始数据收发操作的控制信息的头部,并把发送方和接收方的端口号等重要信息填入,到这里,客户端(发送方)的套接字就能准确连接服务器(接收方)的套接字,然后,我们将头部中的控制位的SYN比特设置为1,可以认为它表示连接。此外还需要设置适当的序号和窗口大小,这一点我们会稍后详细讲解.

    当TCP头部创建好之后,接下来TCP模块会将信息传递给IP模块并委托它进行发送,IP模块执行网络包发送操作后,网络包就会通过网络到达服务器,然后服务器上的IP模块会将接收到的数据传递给TCP模块,服务器的TCP模块根据TCP头部中的信息找到端口号对应的套接字,当找到对应的套接字之后,套接字中会写入相应的信息,并将状态改为正在连接。

    上述操作完成后,服务器的TCP模块会返回响应,这个过程和客户端一样,需要在TCP头部中设置发送方和接收方端口号以及SYN比特,此外,在返回响应时还需要将ACK控制位设为1,这表示已经接收到相应的网络包。网络中经常会发生错误,网络包也会发生丢失,因此双方在通信时必须相互确认网络包是否已经送达,而设置ACK比特就是用来进行这一确认的。然后服务器TCP模块会将TCP头部传递给IP模块,并委托IP模块向客户端返回响应。然后,网络包就会返回到客户端,通过IP模块到达TCP模块,并通过TCP头部的信息确认连接服务器的操作是否成功。如果SYN为1则表示连接成功,这时会向套接字中写入服务器的IP地址、端口号等信息,同时还会将状态改为连接完毕。到这里,客户端的操作就已经完成,但其实还剩下最后一个步骤。刚才服务器返回响应时将ACK比特设置为1,相应地,客户端也需要将ACK比特设置为1并发回服务器,告诉服务器刚才的响应包已经收到。当这个服务器收到这个返回包之后,连接操作才算全部完成。

    现在,套接字就已经进入随时可以收发数据的状态了,大家可以认为这时有一根管子把两个套接字连接了起来。当然,实际上并不存在这么一根管子,不过这样想比较容易理解,网络业界也习惯这样来描述。这根管子,我们称之为连接。只要数据传输过程在持续,也就是在调用close断开之前,连接是一直存在的。建立连接之后,协议栈的连接操作就结束了,也就是说connect已经执行完毕,控制流程被交回到应用程序。

    5.6 收发数据

    5.6.1将http请求消息发给协议栈

    当控制流程从connect回到应用程序之后,接下来就进入数据收发阶段了。数据收发操作是从应用程序调用write将要发送的数据交给协议栈开始的,协议栈收到数据后执行发送操作,这一操作包含如下要点。

    • 首先,协议栈并不关心应用程序传来的数据是什么内容。
    • 其次,协议栈并不是一收到数据就马上发送出去,而是会将数据存放在内部的发送缓冲区中,并等待应用程序的下一段数据。

    这样做是有道理的。应用程序交给协议栈发送的数据长度是由应用程序本身来决定的,不同的应用程序在实现上有所不同,有些程序会一次性传递所有的数据,有些程序则会逐字节或者逐行传递数据,协议栈并不能控制这一行为。在这样的情况下,如果一收到数据就马上发送出去,就可能会发送大量的小包,导致网络效率下降,因此需要在数据积累到一定量时再发送出去。至于要积累多少数据才能发送,不同种类和版本的操作系统会有所不同,不能一概而论,但都是根据下面几个要素来判断的。
    在这里插入图片描述

    第一个判断要素是每个网络包能容纳的数据长度 . 协议栈会根据一个叫作MTU(maximum transmition unit)的参数来进行判断。

    MTU: 表示一个网络包的最大长度,在以太网中一般是1500字节。

    MSS: MTU除去头部之后,一个网络包所能容纳的TCP数据的最大长度

    当从应用程序收到的数据长度超过或者接近MSS时再发送出去,就可以避免发送大量小包的问题了。

    另一个判断要素是时间。当应用程序发送数据的频率不高的时候,如果每次都等到长度接近MSS时再发送,可能会因为等待时间太长而造成发送延迟,这种情况下,即便缓冲区中的数据长度没有达到MSS,也应该果断发送出去。为此,协议栈的内部有一个计时器,当经过一定时间之后,就会把网络包发送出去.

    判断要素就是这两个,但它们其实是互相矛盾的。如果长度优先,那么网络的效率会提高,但可能会因为等待填满缓冲区而产生延迟;相反地,如果时间优先,那么延迟时间会变少,但又会降低网络的效率。

    因此,在进行发送操作时需要综合考虑这两个要素以达到平衡,但TCP协议规格中并没有告诉我们怎样才能平衡,因此实际如何判断是由协议栈的开发者来决定的,应用程序在发送数据时可以指定一些选项:

    • 指定“不等待填满缓冲区直接发送”,则协议栈就会按照要求直接发送数据。像浏览器这种会话型的应用程序在向服务器发送数据时,等待填满缓冲区导致延迟会产生很大影响,因此一般会使用直接发送的选项
    • 指定"等待缓冲区数据达到MSS再发送",则协议栈救护按照要求等达到MSS长度再发送数据. 比如上传大型文档数据时,就会用到;

    5.6.2 注意对较大数据进行拆分

    HTTP请求消息一般不会很长,一个网络包就能装得下,但如果其中要提交表单数据,长度就可能超过一个网络包所能容纳的数据量,比如在博客或者论坛上发表一篇长文就属于这种情况。

    这种情况下,发送缓冲区中的数据就会超过MSS的长度,这时我们当然不需要继续等待后面的数据了,发送缓冲区中的数据会被以MSS长度为单位进行拆分,拆分出来的每块数据会被放进单独的网络包中。

    根据发送缓冲区中的数据拆分的情况,当判断需要发送这些数据时,就在每一块数据前面加上TCP头部,并根据套接字中记录的控制信息标记发送方和接收方的端口号,然后交给IP模块来执行发送数据的操作

    在这里插入图片描述

    5.6.3 序号和ACK

    在对数据进行拆分重装以后,就可以完成数据发送的准备了,不过在发送数据以后,还需要进行一些确认操作;

    确认原理:

    首先,TCP模块在拆分数据时,会先算好每一块数据相当于从头开始的第几个字节,接下来在发送这一块数据时,将算好的字节数写在TCP头部中的序号字段(第二行)。接收方可以用MSS方法获取数据长度。有了上面两个数值,我们就可以知道发送的数据是从第几个字节开始,长度是多少了。

    在这里插入图片描述

    通过这些信息,接收方还能够检查收到的网络包有没有遗漏。例如,假设上次接收到了1460字节,那么接下来如果收到序号为1461的包,说明中间没有遗漏,如果收到的包序号为2921,则说明有数据包遗漏。最后,如果确认没有遗漏,接收方会将到目前为止接收到的数据长度加起来,计算出一共已经收到了多少个字节,然后将这个数值写入TCP头部的ACK号中发送给发送方

    在这里插入图片描述

    在实际通信时候,序号字段并不是都从1开始,而是通过一个随机数计算出初始值,因为如果不这样子就容易被人预测攻击;而这个序号字段则是在第一次客户端向服务端发起请求连接时候,就将该值写入了序列字段,进而告知服务器;

    我们知道通信是双向的,既然客户端给服务端发送数据前,在连接阶段会通过序列号告知服务器,相反的,服务器也会在该阶段同时计算自己的序列号初值,并在进行ack确认连接时候一并告知客户端自己的序列初值

    在这里插入图片描述

    通过这一机制序列号和ack的使用,我们可以确认接收方有没有收到某个包,如果没有收到则重新发送,这样一来,无论网络中发生任何错误,我们都可以发现并采取补救措施(重传网络包)。

    5.6.4 等待超时时间

    当网络传输繁忙时就会发生拥塞,ACK号的返回会变慢,这时我们就必须将等待时间设置得稍微长一点,否则可能会发生已经重传了包之后,前面的ACK号才姗姗来迟的情况,在本来就比较拥塞的情况下,因此如果此时再出现很多多余的重传,对于本来就很拥塞的网络来说无疑是雪上加霜。

    那么等待时间是不是越长越好呢?也不是。如果等待时间过长,那么包的重传就会出现很大的延迟,也会导致网络速度变慢。

    因此,TCP采用了动态调整等待时间的方法,这个等待时间是根据ACK号返回所需的时间来判断的。具体来说,TCP会在发送数据的过程中持续测量ACK号的返回时间,如果ACK号返回变慢,则相应延长等待时间;相对地,如果ACK号马上就能返回,则相应缩短等待时间

    5.6.5 窗口机制提升效率

    在这里插入图片描述

    如图(a)所示,每发送一个包就等待一个ACK号的方式是最简单也最容易理解的,但在等待ACK号的这段时间中,如果什么都不做那实在太浪费了,为了减少这样的浪费,TCP采用图2(b)这样的滑动窗口方式来管理数据发送和ACK号的操作。所谓滑动窗口,就是在发送一个包之后,不等待ACK号返回,而是直接发送后续的一系列包。这样一来,等待ACK号的这段时间就被有效利用起来了

    在这里插入图片描述

    但是这种不等返回ACK号就连续发送包的情况,就有可能会出现发送包的频率超过接收方处理能力的情况;

    例如当接收方的TCP收到包后,会先将数据存放到接收缓冲区中。然后,接收方计算ACK号,将数据块组装起来还原成原本的数据并传递给应用程序,如果这些操作还没完成下一个包就到了也不用担心,因为下一个包也会被暂存在接收缓冲区中。如果数据到达的速率比处理这些数据并传递给应用程序的速率还要快,那么接收缓冲区中的数据就会溢出,也就意味着超出了接收方处理能力。
    在这里插入图片描述

    我们可以让接收方将数据暂存到接收缓冲区中并执行接收操作,当接收操作完成后,接收缓冲区中的空间会被释放出来,也就可以接收更多的数据了,这时接收方会通过TCP头部中的窗口字段将自己能接收的数据量告知发送方。这样一来,发送方就不会发送过多的数据,导致超出接收方的处理能力了

    5.6.6 ACK与窗口合并

    什么时候需要更新窗口大小呢?

    我们的理解是当收到的数据刚刚开始填入缓冲区时,便向发送方更新窗口大小,但是前者发送方只要在每次发送数据时减掉已发送的数据长度就可以自行计算自己出当前窗口的剩余长度,然后发送小于其数量的数据,就不会造成服务方处理过载,因此,更新窗口大小的时机应该是接收方从缓冲区中取出数据传递给应用程序的时候,这个操作是接收方应用程序发出请求时才会进行的,而发送方不知道什么时候会进行这样的操作,因此当接收方将数据传递给应用程序,导致接收缓冲区剩余容量增加时,就需要告知发送方,这就是更新窗口大小的时机。

    什么时候返回ACK值呢?

    我们的理解是只要确定了数据没有任何问题,就可以返回;

    如果将前面两个因素结合起来看,首先,发送方的数据到达接收方,在接收操作完成之后就需要向发送方返回ACK号,而再经过一段时间,当数据传递给应用程序之后才需要更新窗口大小。但如果根据这样的设计来实现,每收到一个包,就需要向发送方分别发送ACK号和窗口更新这两个单独的包。这样一来,接收方发给发送方的包就太多了,导致网络效率下降

    因此,接收方在发送ACK号和窗口更新时,并不会马上把包发送出去,而是会等待一段时间,在这个过程中很有可能会出现其他的通知操作(比如又有新的包来临或者窗口又更新了),这样就可以把两种通知合并在一个包里面发送了,提升了效率;

    当然,发送的数据肯定以最后一个新来通知为准,比如在等待期间又来了3个包,那么ack就会按照最后一次确认的值和窗口更新值一起发送;

    5.7 断开连接

    只要有一方完成收发数据以后,就会发起断开过程,这里我们以服务器一方发起断开过程为例来进行讲解。

    首先,服务器一方的应用程序会调用Socket库的close程序。然后,服务器的协议栈会生成包含断开信息的TCP头部,具体来说就是将控制位中的FIN比特设为1。接下来,协议栈会委托IP模块向客户端发送数据。同时,服务器的套接字中也会记录下断开操作的相关信息.

    在这里插入图片描述

    接下来轮到客户端了。当收到服务器发来的FIN为1的TCP头部时,客户端的协议栈会将自己的套接字标记为进入断开操作状态。然后,为了告知服务器已收到FIN为1的包,客户端会向服务器返回一个ACK;

    5.8 删除套接字

    和服务器的通信结束之后,用来通信的套接字也就不会再使用了,这时我们就可以删除这个套接字了。不过,套接字并不会立即被删除,而是会等待一段时间之后再被删除。等待这段时间是为了防止误操作,例如客户端先发起断开,则断开的操作顺序如下。

    (1)客户端发送FIN

    (2)服务器返回ACK号

    (3)服务器发送FIN

    (4)客户端返回ACK号

    如果最后客户端返回的ACK号丢失了,结果会如何呢?这时,服务器没有接收到ACK号,可能会重发一次FIN。如果这时客户端的套接字已经删除了,那么套接字中保存的控制信息也就跟着消失了,套接字对应的端口号就会被释放出来。这时,如果别的应用程序要创建套接字,新套接字碰巧又被分配了同一个端口号,而服务器重发的FIN正好到达,于是这个FIN就会错误地跑到新套接字里面,新套接字就开始执行断开操作了。

    5.9 总结

    数据收发操作的第一步是创建套接字。一般来说,服务器一方的应用程序在启动时就会创建好套接字并进入等待连接的状态。客户端则一般是在用户触发特定动作,需要访问服务器的时候创建套接字。在这个阶段,还没有开始传输网络包。

    创建套接字之后,客户端会向服务器发起连接操作。首先,客户端会生成一个SYN为1,初始序号以及服务器向客户端发送数据时需要用到的窗口大小的TCP包并发送给服务器

    当这个包到达服务器之后,服务器会返回一个SYN为1的TCP包,这个包的头部中也包含了序号和窗口大小,此外还包含表示确认已收到包的ACK号。当这个包到达客户端时,客户端会向服务器返回一个包含表示确认的ACK号的TCP包。

    在这里插入图片描述

    到这里,连接操作就完成了,双方进入数据收发阶段。

    数据收发阶段的操作根据应用程序的不同而有一些差异,以Web为例,首先客户端会向服务器发送请求消息。TCP会将请求消息切分成一定大小的块,并在每一块前面加上TCP头部,然后发送给服务器。TCP头部中包含序号,它表示当前发送的是第几个字节的数据。当服务器收到数据时,会向客户端返回ACK号。在最初的阶段,服务器只是不断接收数据,随着数据收发的进行,数据不断传递给应用程序,接收缓冲区就会被逐步释放。这时,服务器需要将新的窗口大小告知客户端。当服务器收到客户端的请求消息后,会向客户端返回响应消息,这个过程和刚才的过程正好相反

    在这里插入图片描述

    服务器的响应消息发送完毕之后,数据收发操作就结束了,这时就会开始执行断开操作

    在这里插入图片描述

    ============================上一章========================

  • 相关阅读:
    docker network 组件内网
    git的基本使用2
    [HDLBits] Exams/m2014 q4k
    Vue循环渲染 v-for和v-if,key
    数据库DML
    【附源码】Python计算机毕业设计视频分类管理系统
    网络类型(通信分类)
    挖矿木马基础知识
    xstream运用,JAVA对象转xml,xml转JAVA对象
    深度学习——第1章 深度学习的概念及神经网络的工作原理
  • 原文地址:https://blog.csdn.net/m0_51723227/article/details/128004129