当你打开浏览器,在地址栏输入一个网址(如https://www.baidu.com),按下回车,浏览器很快就会为你加载出你想要的页面。那么你有没有想过,从你按下回车,到看到最终的页面,浏览器都做了哪些工作(不管你认为它是简单还是复杂,它可能都比你想象的复杂的多!)?
下面我们就从浏览器完整加载和渲染一个网页的过程入手,一步步介绍浏览器的运行机制。
域名系统(英文:Domain Name System,缩写:DNS)是互联网的一项服务。它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网(来自百度百科)。
为什么需要域名系统?
这就要先说一下我们如何在整个互联网中找到一台主机(你可以先简单理解为一台电脑,实际上这并不准确)了。
根据IP协议(这里指IP v4)的规定,我们使用一个32位长的由二进制数字(0和1)构成的字符串来唯一标识一台主机。为了书写简便,我们以8位为一组,这样就得到4组二进制数,每组的8位二进制转换为十进制后,就是一个0-255之间的数字。因此,这个32的0和1构成的字符串就可以写成4个0-255之间的数字,我们用.连接起来,就得到了形如192.168.1.1这样的一个地址,这个地址我们就称为IP地址。
按照协议,我们必须提供一台主机的IP地址,才可以在整个网络中找到该主机(如百度的服务器)。但是对于每个互联网用户来说,IP地址不仅冗长,而且毫无含义,因此难以记忆。为了解决这个问题,互联网的奠基者设计了域名系统。
所谓域名系统,本质上就是一个用来维护IP地址和网址对应关系的分布式服务系统。比如现在百度希望用户通过 www.baidu.com来访问自己的网站,那么百度就可以向专门的域名注册机构去申请这个网址,并提交自己的IP地址,这样该机构就会在专有的DNS服务器上保存这个对应关系(实际过程远比这复杂得多,如多级域名的管理等)。现在你只需要访问DNS服务器,告诉它你想访问www.baidu.com这个网站(终于不用记住那个难记的IP地址了),你就可以得到百度服务器的实际IP地址,从而访问百度的服务器了。
域名的注册与浏览器无关,但是域名的解析(将网址解析为IP地址)却与浏览器息息相关。
比如我们现在在地址栏输入了百度首页的网址:
那么浏览器想要访问这个网站,就必须先查出这个网址对应的IP地址。查询过程主要分以下几步:
搜索浏览器的DNS缓存,查询是否有该网址的缓存记录。浏览器会将之前较短时间内访问过的网站的IP地址保存在专有的缓存区中,目的是减少下次查询所需的时间,但缓存时间较短,数量有限。
如果搜索失败,那么浏览器会向本地DNS域名服务器发起域名解析请求(使用UDP网络协议),去最近的DNS域名服务器查询该网址的IP。
如果上述查询仍然失败(本地DNS服务器不可能保存所有网络主机的IP地址),浏览器就会通过运营商直接向根域名服务器发起查询请求。根域名服务器会根据网址中的一级域名(如上述网址的com,就是一级域名),查询出其对应的服务器地址,并将该地址返回给运营商(之后的解析将全部由运营商代理完成)。
运营商根据一级域名的DNS服务器地址,访问该服务器(如com对应的DNS服务器),查询二级域名对应的服务器地址,得到二级域名(如baidu.com)对应的DNS服务器地址。
经过上述查询,运营商已经查到了该网址的注册商(百度)的服务器地址,运营商向该服务器发起查询请求,就可以得到最终的IP地址。
运营商将查到的IP地址返回回来。至此,浏览器就拿到了上述网址对应的IP地址。
注意:并不是所有的网址解析都需要经过上述所有步骤,一旦在某一步拿到了完整的IP地址,解析过程将立即结束。
现在浏览器终于拿到了访问网站所需的IP地址!接下来就是真正的访问过程了。
这个步骤主要是当前主机与服务器之间建立TCP连接。
浏览器得到了服务器的IP地址后,就需要与服务器之间建立一个通信链路,所有的数据都将通过这条通信链路来传递。为了在网络中正确地传输数据,互联网的建设者需要考虑大量的问题,比较典型的如数据丢失处理、网间路由跳转、数据流的编码与解码等等等等。为此web标准化组织制定了大量的网络协议,用于保证数据能在网络间正确地传输。
目前web中广泛采用的是TCP/IP协议族,其中TCP是一个传输层协议,用于保障两台主机之间的通信(IP协议主要用于处理数据在网络之间的路由跳转,如路由器就是一个典型的运行IP协议的设备,由于偏向于底层,这里不再详述)。两台主机建立TCP连接需要经过一个称为三次握手的过程(以我们的主机连接服务器为例):
经过这三步(也称为三次握手),我们的主机就与服务器建立了一个可靠的TCP连接,这个连接会一直持续到双方四次挥手(过程与建立连接类似)断开连接结束。在连接建立期间,由于TCP协议提供的保障,我们在两台主机之间发送的所有信息都将被正确传输(主要依赖TCP强大的纠错机制,以及下层多个网络协议提供的服务)。
现在我们的主机与服务器之间已经建立了一条通信链路,我们可以在这条链路上收发数据。浏览器将基于这条链路,通过http协议(应用层协议,是协议族最高层的协议之一)实现与服务器的消息通信。这里我们不再对http协议展开讲解(可能需要单独的文章介绍)。我们可以先这样简单地理解http协议:浏览器将需要发送的数据,按照一定的格式封装起来,然后添加一系列关于这些数据的描述,最后打成包交给TCP协议去进行网间传递。服务器收到这个数据包,按照同样的规则打开,读取其中的数据和相关描述,这样就借助http协议完成了一次通信。服务器向主机发送消息也是同样的机制。
下面我们来看一下,浏览器是如何在TCP通信链路的基础上,向服务器请求网页文件的。
在讲解浏览器请求资源的过程之前,我们需要先了解一下浏览器结构。
Chromium浏览器的结构如下(图片引自罗升阳的网页加载系列文章,以下关于浏览器结构的理解均参考自该系列文章,详情请见 https://blog.csdn.net/luoshengyang/article/details/50414848 ):
从下往上来看,Chromium总共分以下几层(引用自上述博客):
WebKit:网页渲染引擎层,定义在命令空间WebCore中。Port部分用来集成平台相关服务,例如资源加载和绘图服务。WebKit是一个平台无关的网页渲染引擎,但是用在具体的平台上时,需要由平台提供一些平台相关的实现,才能让WebKit跑起来。
WebKit glue:WebKit嵌入层,用来将WebKit类型转化为Chromium类型,定义在命令空间blink中。Chromium不直接访问WebKit接口,而是通过WebKit glue接口间接访问。WebKit glue的对象命名有一个特点,均是以Web为前缀。
Renderer/Renderer host:多进程嵌入层,定义在命令空间content中。其中,Renderer运行在Render进程中,Renderer host运行在Browser进程中。
WebContents:允许将一个HTML网页以多进程方式渲染到一个区域中,定义在命令空间content中。
Browser:代表一个浏览器窗口,它可以包含多个WebContents。
Tab Helpers:附加在WebContents上,用来增加WebContents的功能,例如显示InfoBar。
这里的Webkit(一个开源项目)和Webkit glue(对webkit的封装层)我们称为Webkit层,提供了诸如HTML引擎、JavaScript引擎(webkit默认的js引擎是JavaScript Core,Chrome就是将这里的引擎替换成了其自主研发的V8引擎)等一系列的底层引擎;Renderer/Renderer host和WebContents称为Content层,主要负责网页的渲染;Brower和Tab Helpers称为浏览器层,主要负责与服务器的通信等。
下面我们就来看一下,浏览器是如何下载网页的。
Chromium在加载一个网页前,会在Browser进程中创建一个Frame Tree,这个Frame Tree代表了我们接下来要加载的网页。如果该页面内部嵌有子网页(iframe),Browser进程就会在这个Frame Tree下面添加子节点,用于描述子网页。
当Frame Tree创建完毕之后,Browser进程就会通过一个专用的IPC通道向Content层发送消息通知Render进程。然后Render进程将进行一系列的初始化工作,最主要的是创建一个Render Tree,Render进程之后会通过该树完成网页的渲染。Render Tree初始化完毕后,会通过上述IPC通道告知Browser层,此时Browser进程和Render进程都已准备就绪,可以准备进行网页下载了!
Browser进程根据地址栏中的网址,解析出需要加载的文件路径。
注意:完整的网址应该包括协议类型、域名/IP地址、端口号、要加载的文件地址,如https://www.baidu.com:80/xxx/xxx.html 。协议类型和域名/IP不再解释。端口号是主机中一个进程的标识,同一个服务器可能同时向外提供多个服务,每个服务我们用端口号来区别。文件地址指的是我们需要加载的网页相对于该网站根目录的路径。
但是我们并不需要输入如此多的信息,省略的协议类型浏览器会自动补全;端口号默认是80端口;而对于文件地址,每个服务都会对外提供一个默认入口文件(通常命名为index.html),我们省略了文件地址时,就会加载这个默认的文件。所以我们通常只需要输入域名或者IP地址即可。
Browser进程现在会封装出一个http请求,向服务器请求这个文件,请求中会携带大量的参数,这里不再赘述。服务器收到该请求后,找到对应的文件,将其返回给浏览器。浏览器会将下载到的文件临时保存在Browser层的一块缓冲区中,并通过IPC通道向Render进程发送消息,通知其读取该文件。
HTML文件的解析并不是Render进程自身完成的,而是交给了更加底层的Webkit层。
Webkit层的HTML引擎拿到html文件之后,首先对文件进行标记化。由于数据在网络中传输只能是0、1的形式,虽然数据经过主机处理,会被转化为一系列的字符串,但这些字符串并不能直接操作。引擎需要先将这些字符一个个读取出来,转换为html标记。比如有一段字符串是这样的:
"123
"
html引擎会把这个字符串提取出来,识别为一个HTML标记,用于后续构建DOM树使用。
将一个标签标记化后,HTML引擎就会将其挂到一棵DOM树上。这棵树有一个默认的根节点,就是Document节点。随后,引擎将从html标记开始添加,并将其作为根节点的唯一子节点。html节点下面会有head节点和body节点,这两个元素又将成为html节点的两个直接子元素。如对于下面的HTML页面:
<!DOCTYPE html>
<html>
<head>
<title>标题</title>
</head>
<body>
<p>123</p>
</body>
</html>
经过HTML引擎的处理,文件会被处理成下面的结构:
现在你可以通过JavaScript提供的DOM接口,来访问这棵树上的节点,执行相关DOM操作。
在构建DOM的过程中,我们可能会遇到一些特殊的标签,如