经典软件测试面试题:浏览器输入 URL 背后发生了什么?(图解版)

概述过程

—-

分为两个大部分,一部分是**生成HTTP请求**,另外一部分是**浏览器委托协议栈**进行通信。

大体分为如下步骤:

1.  解析url

2.  生成HTTP请求

3.  向DNS服务器查询WEB服务器的IP地址

4.  全世界DNS服务器的接力

5.  委托协议栈发送信息

6.  创建套接字

7.  连接服务器

8.  收发数据

9.  断开连接

**由于知识点有点多,请各位大佬细听小弟娓娓道来。**

生成HTTP请求

——–

### 解析url

我们的探索之旅从在浏览器中输入网址开始,在介绍浏览器的工作方式之前,让我们先来介绍一下网址,准确来说应该叫URL。

实际上除了“http:”,网址还可以以其他一些文字开头,例如 “ftp:” “file:” “mailto:”等。之所以有各种各样的URL,是因为通常浏览器是用来访问Web服务器的,但它也可以用来在FTP服务器上下载和上传文件,同时也具备电子邮件客户端的功能。

可以说**浏览器是一个具备多种客户端功能的综合性客户端软件**。

因此它需要一些东西来判断应该使用其中哪种功能来访问相应的数据,而各种不同的URL就是用来确定网址所使用的不同方法,通常我们称之为**协议类型**

所以说浏览器要做的第一步工作就是**对URL进行解析**

### HTTP

解析完URL之后,我们就知道应该要访问的目标在哪里了。接下来,浏览器会使用HTTP协议来访问Web服务器,在此之前我们先复习一下HTTP协议。

HTTP协议定义了客户端和服务器之间交互的**消息内容**和**步骤**。

首先,客户端会向服务器发送请求消息,请求消息中包含的内容是**对谁**和**进行何种操作**两个部分。

*   其中对谁的部分称为URI,这里可以写各种访问目标,而这些访问目标统称为URI(统一资源标识符)。

*   另一部分称之为方法,表示**需要让Web服务器完成怎样的工作**。通过下面这张表大家应该能够理解通过方法可以执行怎样的操作。

除此以外,HTTP消息中还有一些用来表示附加信息的头字段。客户端向Web服务器发送数据时,会先发送头部字段,然后再发送数据。

收到请求消息之后,Web服务器会对其中的内容进行解析,根据这些要求来完成自己的工作,然后将结果存放在响应消息中。

在响应消息的开头有一个 **状态码** 和 **解释信息**,它用来表示操作的执行结果。

后面就是头字段和网页数据。响应消息会被发送回客户端,当收到之后,浏览器会从消息中读出所需的数据并显示渲染。到这里,HTTP的整个工作就以完成。

其中最常用的一个是**GET方法**,一般当我们访问Web服务器获取网页数据时,使用的就是GET方法。

另外一个就是**POST方法**,我们在表单中填写数据并将其发送给Web服务器时就会使用这个POST方法,当我们在网上商城填写收货地址和姓名,或者是在网上填写问卷时,都会遇到带有输入框的网页,而这些可以**输入信息**的部分就是表单。

使用POST方法时,URI会指向Web服务器中运行的一个应用程序的文件名,典型的例子包括“index.cgi”“index.php”等。然后,在请求消息中,除了方法和URI之外,还要加上传递给应用程序和脚本的数据,也就是表单中填入的数据。当服务器收到消息后,Web服务器会将请求消息中的数据发送给URI指定的应用程序。最后,Web服务器从应用程序接收输出的结果,会将它存放到响应消息中并返回给客户端。

之后会有对HTTP进行详细整理,在此不做赘述。

#### 生成HTTP请求消息

理解了HTTP的基本知识之后,我们回到对浏览器本身的探索中。对URL进行解析之后,浏览器确定了Web服务器和文件名,接下来就是根据这些信息来生成HTTP请求消息。

首先,请求消息的第一行称为**请求行**。

这里的重点是开头的方法,可以告诉Web服务器它应该进行怎样的操作。第一行的末尾需要写上HTTP的版本号,这是为了表示该消息是基于哪个版本的HTTP 规格编写的。

第二行开始为消息头。里面存放了除了请求行外的额外详细信息。如日期、客户端支持的数据类型、语言、压缩格式、客户端和服务器的软件名称和版本、数据有效期和最后更新时间等。

写完消息头之后,还需要添加一个完全没有内容的空行,然后写上需要发送的数据。这部分称之为消息体。消息体结束之后,整个消息也就随之结束。

#### 发送请求后会收到响应

当我们将上述请求消息发送出去之后,Web服务器会返回响应消息,响应消息的格式以及基本思路和请求消息是相同的,差别只在第一行上。

在响应消息中,第一行的内容为状态码和响应短语,用来表示请求的执行结果是成功还是出错。状态码和响应短语表示的内容一致,但它们的用途不同。状态码是一个数字,它主要用来向程序告知执行的结果,响应短语则是一段文字,用来向人们告知执行的结果。

### 向DNS服务器查询Web服务器的IP地址

#### IP地址基础

生成HTTP消息之后,接下来我们需要委托操作系统将消息发送给Web服务器。首先我们要查询网址中服务器域名对应的IP地址。(通信必须要提供IP地址),简单回顾一下IP地址。

互联网和公司内部的局域网都是基于TCP/IP的思路来设计的,所以我们先来了解 TCP/IP的基本思路。

TCP/IP的结构如图所示,就是由一些小的子网,通过路由器连接成大的网络。这些子网可以看成使用集线器连接起来的几台计算机,我们将他们看成一个单位,称之为**子网**。通过路由器连接起来形成一个**网络**。

在网络中,所有的设备都会被分配一个地址。这个地址就相当于现实中某条路上 的“××号××室”。其中“号”对应的号码是分配给整个子网的,而“室”对应的号码是分配给子网中的计算机的,这就是网络中的地址。“号”对应的号码称为网络号,“室”对应的号码称为主机号,这个地址的整体称为**IP地址**。

通过IP地址我们可以判断出访问**对象服务器的位置**,从而将消息发送到服务器。前面这些就是TCP/IP中IP地址的基本思路。了解之后让我们再来看一下实际的IP地址。

实际的IP地址是一串32比特的数字,按照8比特(1字节)为一组分成4组,分别用十进制表示然后再用圆点隔开。

这就是我们平常经常见到的IP地址格式,但仅凭这些我们无法区分哪部分是网络号,哪部分是主机号。在IP地址的规则中,网络号和主机号连起来总共是32比特,但这两部分 的具体结构是不固定的。

在组建网络时,用户可以自行决定它们之间的分配关系, 因此,我们还需要另外的附加信息来表示IP地址的内部结构。这一附加信息称为**子网掩码**。

子网掩码的格式如图所示,是一串与IP地址长度相同的32比特数字,其左边一半都是1,右边一半都是0。其中,子网掩码为1的部分表示**网络号**,子网掩码为0的部分表示**主机号**。将子网掩码按照和IP地址一样的方式以每8比特为单位用圆点分组后写在IP地址的右侧,这就是上图(b)的方法。

#### 域名和IP地址并用的理由

TCP/IP网络是通过IP地址来确定通信对象的,不知道IP地址就无法将消息发送给对方,所以在委托操作系统发送消息时,必须要先查询好对方的IP地址。

既然如此,那么在网址中不写服务器的名字,直接写IP地址不就好了吗?实际上,如果用IP地址来代替服务器名称也是能够**正常工作**的。然而,就像你很难记住电话号码一样,要记住一串由数字组成的IP地址也非常困难。因此,相比IP地址来说,网址中还是使用服务器名称比较好记忆。

既然如此,那干脆不要用IP地址而是用名称来确定通信对象不就好了吗?

从运行效率上来看,这并不能算是一个好主意。互联网中存在无数的路由器, 它们之间相互配合,根据IP地址来判断应该把数据传送到什么地方。,使用IP地 址只需要处理4字节的数字,而域名则需要处理几十个到255个字节的字符,这增加 了路由器的负担,传送数据也会花费更长的时间。同时路由器的速度是有极限的,而互联网内部流动的数据量已然让路由器疲于应付了,因此我们不应该再采用效率更低的设计。

于是,现在我们使用的方案是让人来使用名称,让路由器来使用IP地址。为了填补两者之间的障碍,**DNS服务**孕育而生。

#### Socket库提供查询IP地址的功能

向DNS服务器发出查询,并接收DNS服务器返回的响应消息。对于DNS服务器,我们的计算机上一定有相应的DNS客户端,而相当于DNS客户端的部分称为DNS解析器,通过DNS查询IP地址的操作称为域名解析。

解析器实际上是一段程序,它包含在操作系统的Socket库中,在介绍解析器之前,我们先来简单了解一下Socket库。

Socket库其中包含的程序组件可以让其他的应用程序调用操作系统的网络功能,而解析器就是这 个库中的其中一种程序组件。解析器的用法非常简单,Socket库中的程序都是标准组件,只要从应用程序中进行调用就可以了。

调用解析器后,解析器会向DNS服务器发送查询消息,然后DNS服务器会返回响应消息。响应消息中包含查询到的IP地址,解析器会取出IP地址,并将其写入浏览器指定的内存地址中。只要运行图中的这一行程序,就可以完成IP地址的查询。接下来,浏览器在向Web服务器发送消息时,只 要从该内存地址取出IP地址,并将它与HTTP请求消息一起交给操作系统就可以了。

#### DNS解释器内部原理

浏览器调用解析器时,程序的控制流程就会转移到解析器的内部。

通过让多个程序按顺序执行操作,数据就被发送出去了。顺带一提,向DNS服务器发送消息时,我们当然也需要知道DNS服务器的IP地址。只不过这个IP地址是作为TCP/IP的一个设置项目事先设置好的,不需要再去查询了。不同的操作系统中TCP/IP的设置方法也有差异,解析器会根据设置的DNS服务器IP地址来发送消息。

#### 解析过程

上述图片是查找www.google.com的IP地址过程。

首先在本地域名服务器中查询IP地址,如果没有找到的情况下,本地域名服务器会向根域名服务器发送一个请求,如果根域名服务器也不存在该域名时,本地域名会向com顶级域名服务器发送一个请求,依次类推下去。直到最后本地域名服务器得到google的IP地址并把它缓存到本地,供下次查询使用。

从上述过程中,可以看出网址的解析是一个从右向左的过程: com -> google.com -> www.google.com。

但是你是否发现少了点什么,根域名服务器的解析过程呢?事实上,真正的网址是www.google.com.,并不是我多打了一个.,这个.对应的就是根域名服务器,默认情况下所有的网址的最后一位都是.,既然是默认情况下,为了方便用户,通常都会省略,浏览器在请求DNS的时候会自动加上,所有网址真正的解析过程为: . -> .com -> google.com. -> www.google.com.。

#### 通过缓存加快DNS服务器的响应

有时候并不需要从最上级的根域开始查找,因为DNS服务器有一个缓存功能, 可以记住之前查询过的域名。如果要查询的域名和相关信息已经在缓存中,那么就 可以直接返回响应,接下来的查询可以从缓存的位置开始向下进行。相比每次都从 根域找起来说,缓存可以减少查询所需的时间。

并且,当要查询的域名不存在时,“不存在”这一响应结果也会被缓存。这样,当 下次查询这个不存在的域名时,也可以快速响应。

这个缓存机制中有一点需要注意,那就是信息被缓存后,原本的注册信息可能会发 生改变,这时缓存中的信息就有可能是不正确的。因此,DNS服务器中保存的信息都 设置有一个有效期,当缓存中的信息超过有效期后,数据就会从缓存中删除。而 且,在对查询进行响应时,DNS服务器也会告知客户端这一响应的结果是来自缓存中 还是来自负责管理该域名的DNS服务器。

委托协议栈发送消息

———

知道了IP地址之后,就可以委托操作系统内部的协议栈向这个目标IP地址发送消息。

向操作系统内部的协议栈发出委托时,需要按照指定的顺序来调用Socket库中的程 序组件。首先我们解释一下什么是协议栈以及套接字。

### 协议栈

操作系统中的网络控制软件称之为协议栈,表面上是看不出来的,比较抽象。

分为4个部分,之间的上下关系是遵循一定规则。

最上层是**网络应用层程序**,其中包含浏览器、电子邮件客户端、Web服务器等,接下来是Socket库,其中包含DNS解析器

接下来就是**操作系统内部**TCP、UDP他们接受应用程序委托执行收发数据的操作。IP协议负责控制网络包收发操作,将网络包分割成一个一个网络包,并进行传输。在IP协议中存在着ARP协议和ICMP协议,ARP协议用于根据IP地址查询相应的以太网MAC地址

IP层下面就是**网卡驱动程序**,负责控制网卡硬件,最下面的**网卡**则负责实际的收发操作。

### 套接字

在协议栈内部,有一块内存空间存放着控制信息的内存空间,这里记录了控制信息,通常包含通信双方的IP地址、端口号、通信状态等。协议栈需要通过这些控制信息判断下一步的行动,这些控制信息通常称之为套接字。

了解协议栈和套接字之后,我们来看一下调用Socket库的具体过程。

### 创建套接字

通过调用socket函数,首先分配一个套接字所需要的内存空间,然后写入初始化状态。

### 连接服务器

初始化后,浏览器会调用connect函数,随后的协议会将本地的套接字与服务器的套接字进行连接。

#### 具体细节(TCP三次握手)

通过调用connect函数,通过TCP头部中的发送方和接收方端口号可以找到要连接的套接字,将头部的控制位SYN比特设置为1,表示可以连接。同时设置适当的序号和窗口大小。

找到服务器的套接字后,套接字中会写入相应的控制信息,将状态更改为**正在连接**。

服务器开始返回响应,在TCP头部中写入对方的IP地址、端口号,以及SYN比特。同时将ACK控制位设置为1.表示已经接收到相应的网络包。(在网络传输过程中,经常会发生错误网络包因此丢失。因此双方在通信时必须相互确认网络包是否已经送达而设置ACK比特就是用来进行这一确认的)

网络包返回到客户端后,通过IP模块到达TCP模块,并通过TCP头部的信息确认连接服务器的操作是否成功。当确认成功后,会向套接字中写入服务器的IP地址、端口号信息,将状态更改为连接完毕。同时响应服务器,将控制位的ACK比特设置为1并发回服务器。

当服务器收到这个返回包后,连接操作完成,现在的套接字随时可以进入收发数据的状态。

### 收发数据

#### 将HTTP请求消息交给协议栈

通过调用write函数,把要发送的数据交给协议栈。协议栈收到数据后并不是马上发送出去,而是将数据存放在内部的**发送缓存区**中,并等待应用程序的下一段数据。

这样做的目的是:

优化网络效率。如果收到数据就发送,就可能导致发送大量的小包数据。

关于累计多少数据才能发送跟下面几个要素来进行判断:

    第一个要素是判断每个网络包能容纳的数据长度。通过MTU参数(网络包中最大长度,通常是1500字节)减去头部(TCP头部 IP头部)的长度,得到网络包中能够容纳的最大数据长度,称之为MSS。当发送缓冲区的数据长度超过或接近MSS时再发送数据,这样可以避免发送大量小包数据问题。

    复制代码

    第二个要素是时间。如果应用程序发送数据频率不高,每次都会等到缓冲长度接近MSS时才发送数据,会导致发送延迟。为此协议栈内部有一个计时器,当经过一定时间后,就会把网络包发送。

    复制代码

如果仅靠协议栈来判断发送的时机可能会导致一些问题,因此协议栈给应用程序保留了控制发送时机的能力。例如:

浏览器这种会话型应用程序在向服务器发送数据时,通常是使用直接发送的选项。比如指定“不等待填满缓冲区直接发送”。从提高浏览器的响应速度,降低延迟。

#### 拆分较大数据

通常HTTP请求消息不会太长,一个网络包就能装下。但如果是要提交表单数据,长度可能会超过一个网络包所能够容纳的数据量。例如博客、论坛、评论等场景。

这种情况下,发送缓冲区数据长度就会超过MSS,所以数据会以MSS为单位进行拆分,将拆分出来的每块数据放进单独的网络包中。

当要发送这些网络包时,会在数据包前面打上TCP头部,并且根据套接字中记录的控制信息,标记IP地址和端口号后转交给IP模块进行发送操作。

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

因为TCP协议是提供可靠的字节流服务,所以具备确认对方是否接收到网络包,以及当对方没有收到时进行重发功能,因此在发送网络包后,还需要进行确认操作。

##### 原理

TCP协议在进行拆分数据时,会计算好每一块数据相当于从头开始的第几个字节,将计算好的字节数写在TCP头部中,这正是“序号”字段的作用。发送数据的长度是根据网络包的长度减去头部长度就可以计算出来,所以不写入TCP头部中。

根据序号和数据长度就可以准确知道数据是从第几个字节开始,长度为多少。并且接收方还可以能确认收到的网络包是否有遗漏。

如果没有遗漏,接收方会计算现在一共收到多少字节,然后将这个数值写入TCP头部中的ACK号,回传给发送方。返回的ACK号的操作称为确认响应。通过这样的方式,发送方就能够确认对方到底收到了多少数据。

“序号”是通过随机计算出一个初始值,避免数据被攻击。因此在建立通信的时候(三次握手时)需要将开始收发数据之前的随机初始值通知通信对象。在将SYN设置为1时,还需要同时设置序号字段的随机初始值。SYN(synchronize)的本质是通过告诉初始序号使得通信双发保持步调一致,以便完成后续的数据收发检查。

TCP采用这种补救措施来确保对方是否接收到数据,在得到对方确认之间,所有的数据将存放在发送缓冲区中,当对方没有返回某个包对应的ACK号,就会重新发送这些包。

但如果发生网络中断,服务器宕机等问题,无论TCP怎样重发数据肯定是徒劳的。因此在TCP尝试几次无效重传后会强制结束通信,并向应用程序报错。

#### TCP补救机制以及优化策略

##### 根据网络包平均返回时间调整ACK号超时时间

当网络传输繁忙时就会发生拥塞,ACK号的返回会变慢。这时我们就要将等待的时间设置的稍微长一点,避免发生重传数据包之后前面的ACK号才姗姗来迟。

因为真实环境不可预测,所以将等待时间设置为一个固定的值并不是一件好办法,因此TCP采用了动态调整等待时间的策略。简单来说就是TCP会检测ACK号的返回时间,当ACK号返回的变慢,则会延迟超时时间。当ACK返回变快时,相应的会缩短超时时间。

##### 使用窗口有效管理ACK号

每发送一个包就等待一个ACK号的方式是最简单最容易里的方式。但等待ACK号这段时间,如果不做其他事情,实在是太浪费效率了。因此TCP采用滑动窗口方式来管理数据发送和ACK号的操作。简单来说就是发送一个数据包后,不等待ACK号返回,而是直接发送下一个数据包,这样就有效利用起来等待ACK号这段时间。

虽然这样做能够减少等待ACK号所浪费的时间,但是如果不等待返回ACK号就连续发送包,会出现发送包频率超过接收方处理能力的情况。

为了避免接收缓冲区的数据溢出,可以通过接收方告诉发送方我能够接受多少数据(TCP头部中的窗口字段能够将自己能够接受的数据量告诉对方),然后发送方根据这个值对数据发送操作进行控制,这就是滑动窗口的基本思路。

##### ACK与窗口的合并

要提高收发数据的效率,还需要考虑返回ACK号和更新窗口的时机。首先分别分析一下更新窗口和返回ACK号的时机。

*   更新窗口的时机:

当接收到的数据填入缓冲区时,没必要向发送方更新窗口大小。因为发送方在每次发送数据时,减掉已经发送的数据长度就可以自行计算出当前窗口的剩余长度。因此更新窗口大小的时机是接收方从缓冲区取出数据传递给应用程序的时候,这个操作的时机是发送方无法得知的。

因此当接收方将数据传输给应用程序时,导致缓冲区容量增加,就需要告知发送方。

*   更新ACK的时机:

当接收方收到数据时,如果确认没有问题,就应该向发送方返回ACK号,因此我们认为应该收到数据之后就马上进行这一操作

结合这两个因素来分析,当发送方的数据到达后,立刻返回一个ACK号,当数据传递给应用程序后,更新窗口大小。若此以来,每收到一个包就需要分别发送ACK号和窗口更新的两个单独数据包,会导致网络效率下降。

因此,接收方在发送ACK号和更新窗口时,并不会马上把包发送出去,而是等待一段时间,将两个通知合并成一个包进行回传。

举例来说:

当等待发送ACK号时,正好需要更新窗口,这时就可以将数据合并成一个包,减少包的数量。

当需要连续发送多个ACK号时,也可减少包的数量。因为ACK号表示的是已经接受到的数据量,因此只发送最后一个ACK号就可以,中间的可以全部省略。

同理当需要发送多个窗口更新时也可以减少包的数量,因为窗口大小表示缓冲区剩余空间,因此只发送最后一个窗口大小,省略中间过程。

### 接收HTTP响应消息

具体操作在发送消息的时候已经说明,在这里简单总结一下接受HTTP响应的过程。

浏览器委托协议栈发送请求消息之后,会调用read函数来获取响应消息。控制流程会通过read函数转移到协议栈,协议栈执行接下里的操作。

首先协议栈会检查数据块和TCP头部信息,判断数据是否有丢失,如果没有问题返回ACK号。同时协议栈将数据暂存到接收缓冲区中,并将数据按顺序连接起来还原出原始的数据,最后将数据交给应用程序。

### 从服务器断开(TCP四次挥手)

收发数据结束的时间点应该是应用程序判断所有数据已经发送完毕,这时候数据发送完毕的一放会发起断开过程,但不同的应用程序也会选择不同的断开时机。

假设以服务器一方发起断开过程

服务器会调用Socket库中的close函数,生成包含断开信息的TCP头部,将控制位中的FIN比特设置为1.同时套接中会记录断开操作的相关信息。

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

只要接受服务器返回的所有数据,客户端的操作也就随之结束了。因此客户端应用程序会调用close函数来结束收发操作。同服务器端一样生成FIN比特为1的TCP包委托IP模块发送给服务器。

服务器收到后会返回一个ACK号,到现在,整个客户端和服务器的通信全部结束。

当浏览器拿到响应到的数据后,会… [浏览器是如何渲染出页面的?](https://juejin.im/post/5decb43c518825124316816a)

彩蛋…

—–

感谢各位大佬听我叨叨完,文章是参考《网络是怎样连接》,当然各位大佬可能有些早已阅读过,或者啃过经典大教材等等..快圣诞节了,也不知道送大家点什么好..把我收藏的电子书送给大家吧(目前在看的)..还有各种学习资源。

虽然纸质书看起来舒服,但是电子书胜在便携还有做笔记时候你懂的!

如果有需要的请私信我…记得点赞哈!资源就不截图了,免得…**侵权**…

至此,从输入url到浏览器的渲染就整理结束了,下一篇文还是把学过的**HTTP**整理整理..

避免狗熊掰棒子。

下回见,再见各位大佬!