在浏览器中输入一个URL后到底发生了什么?

387 阅读18分钟

这篇文章来自我们团队的小李同学,总结了面试中常会被问到的一个经典题目:在浏览器中输入一个URL后到底发生了什么?来看看她的回答吧,也欢迎大家关注她@Lorrain(原文地址详见文末)

做前端开发已经五个月了,这期间从头学习了html、css、js、angular等等,每天都在和浏览器页面打交道,但是最近做新组件的过程中发现自己对很多宏观且系统的问题了解得仍然不够。便开始学习极客时间上李兵老师的《浏览器工作原理与实践》,这里就对其中一部分的学习做个总结。

“在浏览器中输入一个URL后到底发生了什么?”这是一道出现频率极高的面试题,工作之前作为学生的我仅仅只能从几个常用协议和计算机网络模型去回答,大致如:DNS解析域名,获取IP地址 --》 建立TCP连接(三次握手、四次挥手) --》 发送HTTP请求 --》 服务器处理请求并返回HTTP报文 --》 浏览器解析并渲染页面。

通过最近的学习,发现这道题的回答其实涉及到的知识点十分庞杂,下面根据最近所学来重新回答这个问题。

在此之前,我们首先来回顾一下什么是进程和线程一个进程就是一个程序的运行实例。详细解释就是,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程。”(此处引用《浏览器工作原理与实践》

线程是进程划分的更小的运行单元,一个进程在执行的过程中可以产生很多线程。也就是说,线程无法单独存在,它是由进程启动和管理的。

进程与线程的区别: 1.进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位 2.各进程之间相互独立不会互相影响,各线程则不一定。在同一进程中的线程会互相影响,他们共享进程中的数据(进程之间数据很难共享),可以对进程的公共数据进行读写操作。 3.进程中的一个线程崩溃的话,会导致整个进程崩溃。但是一个进程崩溃后,在保护模式下不会对其他进程产生影响。

为什么要回顾这个知识点呢?这就涉及到了浏览器的工作原理,当我们使用Chrome打开一个页面时,通过Chrome的任务管理器的窗口可以看到,此时Chrome启动了4个进程,这是为什么呢(此处引用《浏览器工作原理与实践》的图来康康~)

ce7f8cfe212bec0f53360422e3b03a9e.png 早期,浏览器的工作是都运行在同一个进程中的,包括页面渲染、插件等等,这导致单进程浏览器的运行相当地不稳定、不流畅。不安全。(容易崩溃,被恶意代码入侵操作系统等)。因此,现代的浏览器使用的是多进程的架构,这就是打开一个浏览器页面出现了4个进程的原因。 image.png

  • 浏览器进程。主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
  • 渲染进程。核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎Blink和JavaScript引擎V8都是运行在该进程中,默认情况下,Chrome会为每个Tab标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
  • GPU进程。其实,Chrome刚开始发布的时候是没有GPU进程的。而GPU的使用初衷是为了实现3D CSS的效果,只是随后网页、Chrome的UI界面都选择采用GPU来绘制,这使得GPU成为浏览器普遍的需求。最后,Chrome在其多进程架构上也引入了GPU进程。
  • 网络进程。主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
  • 插件进程。主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。(在页面中有插件时会有插件进程) (引用《浏览器工作原理与实践》)

—————————————————分割线————————————————————

由此开始,正式梳理一下输入URL后都发生了什么?

1.首先,用户输入URL地址后,浏览器进程接收到用户输入的URL请求,浏览器进程再将该URL转发给网络进程,在网络进程中真正地发起URL请求

2.由于用户一般的输入都是域名,需要进行DNS解析得到对应的IP地址。找到正确的IP地址会有以下几个步骤

查询浏览器是否有缓存 --》 查看本机系统是否有缓存 --》 查询路由器是否有缓存 --》 查看本地硬盘的host文件是否有对应的ip地址 --》 本地DNS解析器缓存查找 --》 本地DNS服务器查找。

递归查询就是主机所询问的本地域名服务器若不知道域名的IP地址,那么本地域名服务器就以DNS客户的身份向其他DNS服务器继续发出查询请求(而不是由主机来发送),以此类推。

迭代查询特点是,当当前DNS服务器不知道正确的IP地址时,告诉本地域名服务器下一个应该查找的DNS服务器的IP地址,让本地域名服务器再向下一个DNS服务器进行查询,以此类推。

3.根据URL的字段,构建HTTP请求信息,包括请求行、请求头和请求体。

image.png (引用《浏览器工作原理与实践》))

4.为了将请求信息完整的转发到目的主机或服务器上,则需要建立TCP连接(传输层)。TCP的连接建立需要三次握手,其过程可以理解为

客户端:“hi,我请求和你建立连接”

服务器:“同意建立连接”

客户端:“确认建立连接”

(TCP是面向连接的可靠的传输服务,其可靠性主要来源于 数据包校验、保证数据包的正确顺序、丢弃重复数据、确认应答的机制、超时重发机制、流量控制机制、拥塞控制机制,这里可展开的东西太多了orz···) 建立好TCP连接后,原始数据会加上TCP头,TCP头中包括源端口号和目的端口号,于是便形成了TCP数据段。

5。得到TCP数据段后(前面已经得到了正确的IP地址),发送方会把这个 TCP数据段交给IP协议进行再一次封装(加入源IP地址和目的IP地址)。网络层把TCP数据段进一步封装成一个IP数据报(也可能是多个IP数据报,IP协议会进行自动分包的过程)然后把数据报交给数据链路层。

6.数据链路层会进一步把这个数据封装成以太网数据帧, 在构造帧头的时候就需要根据IP映射MAC地址(也就是物理地址),这个构造的过程依赖了ARP协议。然后再把这个数据交给物理层传输。

关于ARP协议: 每台主机都有自己的ARP列表,用于保存IP地址和MAC地址的映射关系。主机首先会检查自己的ARP列表,如果有对应的MAC地址则直接拿来使用,如果没有,就向本地网段广播发送一个ARP请求。网络中的所有主机接收到ARP请求后会根据其中的目的IP地址检查是否与自己的IP地址一致,若不一致则忽略次数据包,若一致,则该主机首先将发送端的MAC地址和IP地址添加到自己的ARP列表中,并给源主机发送一个ARP响应包,告诉源主机自己就是对方要查找的MAC地址。源主机接收到响应数据包后,添加此映射关系进入自己的ARP列表中。

7.物理层将这个数据转换为光电信号继续进行传输。 电信号沿着网线,到达下一个设备(路由器),路由器就会针对收到的数据进行分用,物理层把数据交给了数据链路层,数据链路层把数据交给了网络层,路由器拿到了网络层中的IP数据报,取出其中的 IP地址,查询路由表,找到下一个需要传输的目标,进一步再找到下一个目标的 MAC地址,然后将数据重新封装(把数据交给数据链路层和物理层)。此时的 源MAC 和 目的MAC 已经发生更改。(引用自文章从输入URL到展示出页面,这个过程发生了什么?-爱代码爱编程

8.数据到达接收方(服务器),仍然要进行分用。层层解析,物理层将电信号转换成以太网数据帧,交给数据链路层;数据链路层解析出 IP数据报,交给网络层(都涉及到了CRC校验,如果校验和不对,说明数据错误,直接丢弃)IP 协议再进行解析,获得了一个 TCP 数据段(IP报头中有协议类型);这个解析过程可能还涉及组包的过程,再根据 TCP数据报中的端口号,找到对应的进程,把数据放入对应的 socket(套接字) 的接受缓存区中。(引用自自文章从输入URL到展示出页面,这个过程发生了什么?-爱代码爱编程

9.服务器的应用程序调用对应的socket API,从 TCP接收缓冲区中读取数据,应用程序把这个数据按照 HTTP 协议来解析,获取其中的 URL,根据 URL 指定的路径,知道了要获取数据的根路径。(引用自文章从输入URL到展示出页面,这个过程发生了什么?-爱代码爱编程

10.服务器会对这个根路径进行配置,映射到一个具体的index.html这样的一个HTML文件,服务器读取这个文件,把其中的数据构造成一个 HTTP 响应数据,然后再调用 socket API 进行发送。重复以上过程,依次转发,最后达到用户的主机。(引用自文章从输入URL到展示出页面,这个过程发生了什么?-爱代码爱编程) 服务器HTTP响应数据的格式:

image.png(引用《浏览器工作原理与实践》

11.主机接收到响应数据之后,根据图中所示,不同的Content-Type会有不同的处理流程。如果Content-Type字段的值被浏览器判断为下载类型,那么该请求会被提交给浏览器的下载管理器,同时该URL请求的导航流程就此结束。但如果是HTML,那么浏览器则会继续进行导航流程。由于Chrome的页面渲染是运行在渲染进程中的,所以接下来就需要准备渲染进程了。

12.默认情况下,Chrome会为每个页面分配一个渲染进程,也就是说,每打开一个新页面就会配套创建一个新的渲染进程。但是,同一站点的网页会运行在同一个渲染进程中。

13.渲染进程准备好之后,还不能立即进入文档解析状态,因为此时返回的文档数据还在网络进程(提示:网络进程是用来加载网络资源的)中,并没有提交给渲染进程,所以下一步就进入了提交文档阶段。

14.首先当浏览器进程(用于管理子进程)接收到网络进程的响应头数据之后,便向渲染进程发起“提交文档”的消息; 渲染进程接收到“提交文档”的消息后,会和网络进程建立传输数据的“管道”(进程间通信的一种方法); 等文档数据传输完成之后,渲染进程会返回“确认提交”的消息给浏览器进程浏览器进程在收到“确认提交”的消息后,会更新浏览器界面状态,包括了安全状态、地址栏的URL、前进后退的历史状态,并更新Web页面。

个人理解:在这个过程中浏览器进程承担了管理的作用,文档数据虽然在网络进程中,但是是由浏览器进程对渲染进程来发送消息的,渲染进程接收到消息后才会与网络进程进行数据通信,通信完成后,渲染进程就告诉浏览器进程自己已经接收到文档准备渲染了(也就是“确认提交”的消息),这时候浏览器进程(也用于界面显示)才更新自己的页面,但是其中的内容还没有显示出来。

15.接下来就进入到渲染流程。“按照渲染的时间顺序,流水线可分为如下几个子阶段:解析HTML代码构建DOM树、样式计算、布局阶段、分层、绘制、分块、光栅化和合成。”(注:这部分的内容写的较为详细,篇幅略大)

解析HTML:浏览器是如何读懂HTML代码的呢? HTML的结构不算太复杂,我们日常开发需要的90%的“词”(指编译原理的术语token,表示最小的有意义的单元),种类大约只有标签开始、属性、标签结束、注释、CDATA节点几种。举个例子,如果要将下面这句HTML拆分成词:

<p class="a">text text text</p>

可以把这段代码依次拆成词(token):

  • '<p' “标签开始”的开始;
  • ' class=“a”' 属性;
  • '<' “标签开始”的结束;
  • text text text 文本;
  • '< /p>'标签结束。

根据返回的字符流(HTML文档),使用状态机(这又另是一个知识点···)将其解析为词(token)。HTML官方文档规定了80个状态,大概的过程就是使用 if else 语句来确定读到的字符属于哪个状态。

构建DOM树: 接下来要把这些简单的词变成DOM树,这个过程我们是使用栈来实现的。

  • 栈顶元素就是当前节点;
  • 遇到属性,就添加到当前节点;
  • 遇到文本节点,如果当前节点是文本节点,则跟文本节点合并,否则入栈成为当前节点的子节点;
  • 遇到注释节点,作为当前节点的子节点;
  • 遇到tag start就入栈一个节点,当前节点就是这个节点的父节点;
  • 遇到tag end就出栈一个节点(还可以检查是否匹配)。 (引用自《重学前端》

样式计算 :样式计算的目的是为了计算出DOM节点中每个元素的具体样式。那么浏览器如何理解这些纯文本的CSS样式呢?

当渲染引擎接收到CSS文本时,会执行一个转换操作,将CSS文本转换为浏览器可以理解的结构——styleSheets。(这里可以理解其为一个样式表)

转化为styleSheets后就可以将属性值标准化。所谓属性值标准化可以参考下图:

image.png (引用自《浏览器工作原理与实践》

现在样式的属性已被标准化了,接下来就是计算DOM树中每个节点的样式属性,在计算过程中需要遵守CSS的继承和层叠两个规则。(所谓继承就是子节点会继承父节点的样式)。这个阶段最终输出的内容是每个DOM节点的样式,并被保存在ComputedStyle的结构内。实际上,样式计算和构建DOM树并不是分开的两个步骤,浏览器会尽量流式处理整个过程。构建DOM的过程是:从父到子,从先到后,一个一个节点构造,并且挂载到DOM树上,而CSS的属性也同时被计算出来。

布局阶段 :有了DOM树及其CSS属性,还不足以显示页面,因为还缺少DOM元素的几何信息。计算几何信息的过程叫做布局,首先要构建布局树,再进行布局计算(这个小坑先留着,后面学习到了再详细记录)。

为了构建布局树,浏览器大体上完成了下面这些工作:

  • 遍历DOM树中的所有可见节点,并把这些节点加到布局树中;
  • 不可见的节点会被布局树忽略掉,比如head标签下面的全部内容,和设置dispaly:none的元素。

分层 :因为页面中有很多复杂的效果,如一些复杂的3D变换、页面滚动,或者使用z-indexing做z轴排序等(z轴可以认为是与电脑桌面垂直的一条轴,主要用来做重叠的样式),为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)。浏览器的页面实际上被分成了很多图层,这些图层叠加后合成了最终的页面,图层和布局树的关系如图:

e8a7e60a2a08e05239456284d2aa4061.png (引用自《浏览器工作原理与实践》

图层绘制 :渲染引擎实现图层的绘制我们平时画画类似(总是会先画背景再画物体或人物,最后画其细节),会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表。(所谓绘制指令可以理解为,先画啥,再画啥)

栅格化(raster)操作:绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。(渲染进程中包含多个线程)

当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit) 给合成线程,合成线程会将图层划分为图块(tile) (为了节省开销,通常会优先绘制用户目前可见视口的图层)。

然后合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,就是指将图块转换为位图。(位图的概念相信学过图像处理的同学都不陌生,就是使用像素阵列来表示的图像,每个像素都包含自己的位置和颜色信息。这里的位图就是在内存里建立一张二维表格,把一张图片的每个像素对应的颜色保存进去。) 通常,栅格化过程都会使用GPU来加速生成,使用GPU生成位图的过程叫快速栅格化,或者GPU栅格化,生成的位图被保存在GPU内存中。GPU操作是运行在GPU进程中,如果栅格化操作使用了GPU,那么最终生成位图的操作是在GPU中完成的,这就涉及到了跨进程操作。

合成和显示 :一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。(合成线程存在于渲染进程中)

浏览器进程里面有一个叫viz的组件,用来接收合成线程发过来的DrawQuad命令,然后根据DrawQuad命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。

到这里,经过这一系列的阶段,编写好的HTML、CSS、JavaScript等文件,经过浏览器就会显示出漂亮的页面了。 (引用自《浏览器工作原理与实践》

总结一下渲染流程的部分: (1) 渲染进程将HTML内容转换为能够读懂的DOM树结构。 (2) 渲染引擎将CSS样式表转化为浏览器可以理解的styleSheets,计算出DOM节点的样式。 (3) 创建布局树,并计算元素的布局信息。 (4) 对布局树进行分层,并生成分层树。 (5) 为每个图层生成绘制列表,并将其提交到合成线程。 (6) 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。 (7) 合成线程发送绘制图块命令DrawQuad给浏览器进程。 (8) 浏览器进程根据DrawQuad消息生成页面,并显示到显示器上。

至此,从输入一个URL到最后浏览器显示页面的过程就描述完了。但是这其中涉及的知识点非常繁琐,每一个部分都可以拆分成更细的东西去学习和了解,之后的坑我就慢慢填上吧~ 本文中不够准确的地方还请批评指正~


作者:Lorrain
链接:juejin.cn/post/713387…