从输入URL到页面显示,中间发生了什么?

351 阅读10分钟

每个故事都需要特定的故事背景才能更深入的体会,而本文讲述的故事,背景就是故事发生的地方——浏览器(Chrome)。

1 Chrome的多进程架构

浏览器的的功能决定了其需要的模块,主要包括网络、渲染引擎等。在实际浏览器工作场景中,存在几个无法避免的问题:

  • 稳定性问题:复杂JavaScript代码可能引起的渲染引擎模块崩溃;
  • 流畅性问题:页面可能存在的内存泄漏;
  • 安全性问题:网络获取资源可能存在的恶意性;
  • ... 上述这些问题难以被彻底解决,所以为了避免因为某个无法避免的问题带来整个浏览器的卡顿甚至崩溃,Chrome引入了多进程架构。多进程架构的好处在于进程间的相互隔离,单个进程的卡顿或崩溃不会影响浏览器中的其他内容。多进程的引入使得安全问题的解决方式更加优美,仅需要给安全敏感进程套上一层沙箱,就可以在保证安全的前提下,最大限度地降低对浏览器性能的影响。常用的进程及其功能包括:
  • 浏览器主进程:界面展示、用户交互、子进程管理等;
  • 渲染进程:将HTML、CSS、JavaScript渲染为用户可见可交互的网页;
  • 网络进程:页面网络资源加载;

接下来要讲述的故事也将围绕进程展开,故事的主线是浏览器主进程,支线是主进程管理的子进程。先来输入URL(Uniform Resource Locator,统一资源定位系统)开始这个故事。

2 网络进程

在浏览器地址栏中输入之后,浏览器会先生成完整的URL再通过进程间通信交给网络进程处理,由网络进程发起真正的URL请求。浏览器地址栏的输入可能存在两种需要处理情况:

  • 符合URL规则的输入:浏览器补充协议头(https、http、file等),合成完整的URL;
  • 不符合URL规则的输入:浏览器基于默认的搜索引擎,将输入作为搜索关键字,合成完整的URL; 网络进程收到了浏览器主进程交给它的任务,第一反应是先看能不能“偷懒”,先查找本地缓存中是否有浏览器主进程请求的资源,如果实在没有,它才会不情愿的进入网络请求流程。 但网络进程先查找本地缓存的“偷懒”行为,带来了实实在在的好处:
  • 对服务器端:缓解了服务器的处理压力;
  • 对浏览器端:提高了浏览器的加载速度; 接下来正式进入了网络请求的流程。在日常生活中,记住一个人的名字可能很简单,但要记住一个人的住址可能很困难。网络世界中也是一样,一个网站的域名更好记忆,但这个网站的IP(Internet Protocol,网际互连协议)地址并不易于人们记忆。好在DNS(Domain Name System,域名系统)替网络进程做了一套映射,网络进程可以请求DNS获取到域名对应的IP地址。当然,网络进程还是会一如既往的能偷懒就偷懒,DNS数据同样可以被缓存!解析过的域名,浏览器就会缓存解析的结果。

有了IP地址,网络进程就可以找到服务器在哪了,同时网络进程也会告诉服务器自己住在哪里。但在网络世界中,想要获得数据并不像现实世界中发个快递那么容易。数据是被拆分成一个个小数据包来“运输”的,这可是个大工程。因此,要获取浏览器请求的资源,还需要建立一条数据的“物资保障通道”,这条通道就是TCP(Transmission Control Protocol,传输控制协议)连接。建立TCP连接需要进行三次握手。这里有个奇怪的问题,为什么建立TCP连接偏偏需要进行三次握手,而不是两次或者四次?

2.1 建立TCP连接

建立连接三次握手的过程可以概括为“请求->应答->应答之应答”,这么做的主要原因是因为网络通路本身的不可靠性。因此,建立TCP连接需要处理以下几种情况:

  1. 请求包丢失;
  2. 请求包超时;
  3. 服务器无响应; 当源主机A(浏览器一侧) 发出请求后,如果没有收到目的主机B(服务器一侧) 的应答,是无法判断无应答的原因的,A能做的只有重发请求。基于重发请求这一点,再来看如何解决上述三种情况:
  4. 若之前的请求包丢失,重发时B收到了A的请求包,B只需要返回应答包给A即可。看上去在情况(1)下只需要“请求->应答”两次握手就可以完成任务,但网络通路的情况不止一种,再让我们加入情况(2)。
  5. 若又发生了请求包超时,但超时的请求包最终到达了B,就会让B误解为A又要建立一个连接,此时,如果仅通过两次握手来建立连接,超时但到达的请求包就会建立多余且不必要的通路。因此,需要加入“应答之应答”,即第三次握手,具体过程为:B向A发送应答,该应答要向A确认是否要建立连接。 当B再次收到A发来的“应答之应答”,说明A确实要建立这样一条通路,此时,TCP连接才算完成。
  6. 若出现服务器无响应,只需要给A一个重发时限,若在该时限内重发一直无响应,则认为B不想建立连接。 上述内容中解释了为什么两次握手不可以,但其实“应答之应答”也可能存在超时但到达的问题。需要特别注意,超时却到达的问题是无法避免的,无论是四次四十次四百次握手,都无法保证100%可靠。因此,在实践中使用三次握手建立TCP连接。

2.2 HTTP请求

TCP连接建立好后,“物资保障通道”就搭成了,接下来轮到HTTP协议来搬运数据包了。浏览器会向服务器发送请求行请求头,来说明自己的需要。终于HTTP请求在TCP建立好的连接上,被送到了服务器,服务器收到请求后,就可以返回数据给浏览器了。

QQ截图20220218171212.png

这里我们使用curl命令行工具来查看掘金服务器返回的数据,其中第二行是服务器的响应行,这里除了HTTP的协议版本,还有一个三位数字的状态码,状态码告诉浏览器请求处理的结果如何,此处状态码200就表示处理成功。

响应行下面是响应头。服务器返回请求数据后,可以通过Connection: keep-alive来保持TCP连接,毕竟建立一次连接耗时耗力,但为了说明整个流程,我们只好先断开TCP连接。

2.3 断开TCP连接

断开TCP连接需要四次挥手。现在,我们的源主机A和目的主机B准备要断开连接了。先考虑理想的状况(假设和平分手场景):

  1. A告诉B,“我想要的都有了,不用再给我什么了。”;
  2. B收到A说的话,告诉A,“好吧,我知道了。”(到这里,A已经摆明态度,停止对B的付出数据包);
  3. B释怀,告诉A,“那我就不再做多余的事了,再见A。”(B也放下了执念,停止对A的付出数据包);
  4. A礼貌回复,“再见B。” 和平分手是一种理想状况,但在分手断开连接的过程中,无论是A先跑还是B先跑,只要等待足够长的一段时间收不到任何回复,心里就明白发生了什么,此时就会彻底断开TCP连接。

到这里,网络进程已经完成了它的任务,拿到了请求的网络资源,并告诉浏览器主进程它忙活完了。接下来,浏览器主进程就会安排网络进程与渲染进程通过管道交接得到的数据,当这些数据全部交接完毕后,浏览器主进程就会更新页面和界面状态,渲染进程就要开始忙活了。

3 渲染进程

把HTML、CSS、JavaScript这些冰冷的代码渲染成生动的页面,并不是一件容易的事情。由于渲染的机制复杂,执行时会把整个渲染的流程分割为若干子阶段,接下来的故事,将按子阶段的顺序依次展开。

3.1 构建DOM树

浏览器是无法直接理解和使用HTML的,因此需要把HTML做一个转换,于是有了DOM树。DOM树将HTML中的内容,以树结构一一对应地保存在内存中,HTML中的标签成为了树上的结点,以便JavaScript来对其进行间接的查询或修改操作。 浏览器理解了页面HTML后,接下来就是给HTML添加样式的CSS了。

3.2 样式计算

与HTML一样,浏览器同样无法直接理解CSS样式,因此还需要对CSS做一个转换,于是又有了styleSheets,该结构在同样具备了被JavaScript进行查询或修改的操作。有了这些基础,我们可以真正的开始样式计算了。

  1. 计算DOM树结点样式:计算基于CSS的继承规则与层叠规则
    • 继承规则:部分属性(例如字体大小)存在父结点到子结点的继承;
    • 层叠规则:由选择器优先级决定层叠的样式;
  2. 属性值标准化:将CSS中相对单位转换为绝对单位(例如:2em -> 32px),使得渲染进程可以专注于样式计算。

3.3 布局

渲染进程已经拥有了DOM树和DOM树中元素的样式。接下来,需要将这些内容按照计划的布局,摆放在页面上。这里需要完成的两项工作为:

  1. 构建布局树:布局树中仅包含可见的DOM结点;
  2. 布局计算:计算布局树结点的坐标位置;

3.4 分层

知道如何摆放之后,还不能着急绘制页面,渲染进程需要给布局树中的结点们分配图层,构造出一棵图层树。这是因为网页在实践中常常是一个三维空间(比直觉上多一个z轴),真实的网页分层就像是在白纸上贴贴画,贴画之间可以彼此覆盖。这里需要说明一下,渲染进程是如何给布局树中的结点们分配图层的:

  1. 拥有层叠上下文的元素会拥有自己的图层;
  2. 被可视区域剪裁的内容会拥有自己的图层;

3.5 图层绘制

任何复杂图形一定是由简单图形组合产生的,渲染进程的逻辑也是一样。渲染引擎将要绘制的图形进行拆分,得到一个个小的绘制指令,通过小绘制指令的组合,来拼出复杂的图层样式。

3.6 栅格化

图层绘制出的图像是一种三维的矢量图形格式,这种格式无法作为网页被直接显示,需要通过栅格化转换为二维位图。一个页面可能会很大,但浏览器视口的大小是相对固定的,因此,出于对浏览器性能的考虑,视口附近的图层会被划分为图块,栅格化时仅处理视口附近的图块,并将得到的页面绘制到内存中,显示在页面上。

参考资料