宏观视角的浏览器

Chrome架构:仅仅打开了1个页面,为什么有4个进程

单进程浏览器时代

  1. 在了解了进程和线程之后,我们再来一起看下单进程浏览器的架构。顾名思义,单进程浏览器是指浏览器的所有功能模块都是运行在同一个进程里,这些模块包含了网络、插件、JavaScript运行环境、渲染引擎和页面等。其实早在2007年之前,市面上浏览器都是单进程的。单进程浏览器的架构如下图所示<br />![Screen Shot 2021-05-28 at 2.54.04 PM.png](https://cdn.nlark.com/yuque/0/2021/png/12960771/1622184846483-6ea567a5-6bc9-4a7c-9ad2-efafbb1e5372.png#clientId=ua1bf98b0-5cd6-4&from=drop&height=287&id=La3zu&margin=%5Bobject%20Object%5D&name=Screen%20Shot%202021-05-28%20at%202.54.04%20PM.png&originHeight=289&originWidth=766&originalType=binary&ratio=1&size=105904&status=done&style=none&taskId=u36fe3cfa-2d63-457d-bb70-a654e672f37&width=762)<br />如此多的功能模块运行在一个进程里,是导致单进程浏览器不稳定、不流畅和不安全的一个主要因素。下面我就来一一分析下出现这些问题的原因<br />**问题1:不稳定**<br />早期浏览器需要借助于插件来实现诸如Web视频、Web游戏等各种强大的功能,但是插件是最容易出问题的模块,并且还运行在浏览器进程之中,所以一个插件的意外崩溃会引起整个浏览器的崩溃。<br />除了插件之外,渲染引擎模块也是不稳定的,通常一些复杂的JavaScript代码就有可能引起渲染引擎模块的崩溃。和插件一样,渲染引擎的崩溃也会导致整个浏览器的崩溃<br />**问题2:不流畅**<br />从上面的“单进程浏览器架构示意图”可以看出,所有页面的渲染模块、JavaScript执行环境以及插件都是运行在同一个线程中的,这就意味着同一时刻只能有一个模块可以执行。<br />比如,下面这个无限循环的脚本:
  1. function freeze() {
  2. while (1) {
  3. console.log("freeze");
  4. }
  5. }
  6. freeze();

如果让这个脚本运行在一个单进程浏览器的页面里,你感觉会发生什么?
因为这个脚本是无限循环的,所以当其执行时,它会独占整个线程,这样导致其他运行在该线程中的模块就没有机会被执行。因为浏览器中所有的页面都运行在该线程中,所以这些页面都没有机会去执行任务,这样就会导致整个浏览器失去响应,变卡顿。这块内容要继续往深的地方讲就到页面的事件循环系统了,具体相关内容我会在后面的模块中为你深入讲解。
除了上述脚本或者插件会让单进程浏览器变卡顿外,页面的内存泄漏也是单进程变慢的一个重要原因。通常浏览器的内核都是非常复杂的,运行一个复杂点的页面再关闭页面,会存在内存不能完全回收的情况,这样导致的问题是使用时间越长,内存占用越高,浏览器会变得越慢
问题3:不安全
这里依然可以从插件和页面脚本两个方面来解释该原因。
插件可以使用C/C++等代码编写,通过插件可以获取到操作系统的任意资源,当你在页面运行一个插件时也就意味着这个插件能完全操作你的电脑。如果是个恶意插件,那么它就可以释放病毒、窃取你的账号密码,引发安全性问题。
至于页面脚本,它可以通过浏览器的漏洞来获取系统权限,这些脚本获取系统权限之后也可以对你的电脑做一些恶意的事情,同样也会引发安全问题。
以上这些就是当时浏览器的特点,不稳定,不流畅,而且不安全。这是一段不堪回首的过去,也许你没有经历过,不过你可以想象一下这样的场景:当你正在用浏览器打开多个页面时,突然某个页面崩溃了或者失去响应,随之而来的是整个浏览器的崩溃或者无响应,然后你发现你给老板写的邮件页面也随之消失了,这时你的心情会不会和页面一样崩溃呢

多进程浏览器时代

好在现代浏览器已经解决了这些问题,是如何解决的呢?这就得聊聊我们这个“多进程浏览器时代”了

早期多进程架构

你可以先看看下面这张图,这是2008年Chrome发布时的进程架构。Screen Shot 2021-05-28 at 2.57.18 PM.png
从图中可以看出,Chrome的页面是运行在单独的渲染进程中的,同时页面里的插件也是运行在单独的插件进程之中,而进程之间是通过IPC机制进行通信(如图中虚线部分)。
我们先看看如何解决不稳定的问题。由于进程是相互隔离的,所以当一个页面或者插件崩溃时,影响到的仅仅是当前的页面进程或者插件进程,并不会影响到浏览器和其他页面,这就完美地解决了页面或者插件的崩溃会导致整个浏览器崩溃,也就是不稳定的问题。
接下来再来看看不流畅的问题是如何解决的。同样,JavaScript也是运行在渲染进程中的,所以即使JavaScript阻塞了渲染进程,影响到的也只是当前的渲染页面,而并不会影响浏览器和其他页面,因为其他页面的脚本是运行在它们自己的渲染进程中的。所以当我们再在Chrome中运行上面那个死循环的脚本时,没有响应的仅仅是当前的页面。
对于内存泄漏的解决方法那就更简单了,因为当关闭一个页面时,整个渲染进程也会被关闭,之后该进程所占用的内存都会被系统回收,这样就轻松解决了浏览器页面的内存泄漏问题。
最后我们再来看看上面的两个安全问题是怎么解决的。采用多进程架构的额外好处是可以使用安全沙箱,你可以把沙箱看成是操作系统给进程上了一把锁,沙箱里面的程序可以运行,但是不能在你的硬盘上写入任何数据,也不能在敏感位置读取任何数据,例如你的文档和桌面。Chrome把插件进程和渲染进程锁在沙箱里面,这样即使在渲染进程或者插件进程里面执行了恶意程序,恶意程序也无法突破沙箱去获取系统权限。
好了,分析完早期的Chrome浏览器后,相信你已经了解了浏览器采用多进程架构的必要性

目前多进程架构

不过Chrome的发展是滚滚向前的,相较之前,目前的架构又有了很多新的变化。我们先看看最新的Chrome进程架构,你可以参考下图Screen Shot 2021-05-28 at 2.59.00 PM.png
从图中可以看出,最新的Chrome浏览器包括:1个浏览器(Browser)主进程、1个 GPU 进程、1个网络(NetWork)进程、多个渲染进程和多个插件进程
下面我们来逐个分析下这几个进程的功能。

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

讲到这里,现在你应该就可以回答文章开头提到的问题了:仅仅打开了1个页面,为什么有4个进程?因为打开1个页面至少需要1个网络进程、1个浏览器进程、1个GPU进程以及1个渲染进程,共4个;如果打开的页面有运行插件的话,还需要再加上1个插件进程。
不过凡事都有两面性,虽然多进程模型提升了浏览器的稳定性、流畅性和安全性,但同样不可避免地带来了一些问题

  • 更高的资源占用。因为每个进程都会包含公共基础结构的副本(如JavaScript运行环境),这就意味着浏览器会消耗更多的内存资源。
  • 更复杂的体系架构。浏览器各模块之间耦合性高、扩展性差等问题,会导致现在的架构已经很难适应新的需求了

对于上面这两个问题,Chrome团队一直在寻求一种弹性方案,既可以解决资源占用高的问题,也可以解决复杂的体系架构的问题

未来面向服务的架构

为了解决这些问题,在2016年,Chrome官方团队使用“面向服务的架构”(Services Oriented Architecture,简称SOA)的思想设计了新的Chrome架构。也就是说 Chrome 整体架构会朝向现代操作系统所采用的“面向服务的架构” 方向发展,原来的各种模块会被重构成独立的服务(Service),每个服务(Service)都可以在独立的进程中运行,访问服务(Service)必须使用定义好的接口,通过IPC来通信,从而构建一个更内聚、松耦合、易于维护和扩展的系统,更好实现 Chrome 简单、稳定、高速、安全的目标。如果你对面向服务的架构感兴趣,你可以去网上搜索下资料,这里就不过多介绍了。
Chrome最终要把UI、数据库、文件、设备、网络等模块重构为基础服务,类似操作系统底层服务,下面是Chrome“面向服务的架构”的进程模型图Screen Shot 2021-05-28 at 3.00.16 PM.png
目前Chrome正处在老的架构向服务化架构过渡阶段,这将是一个漫长的迭代过程。
Chrome正在逐步构建Chrome基础服务(Chrome Foundation Service),如果你认为Chrome是“便携式操作系统”,那么Chrome基础服务便可以被视为该操作系统的“基础”系统服务层。
同时Chrome还提供灵活的弹性架构,在强大性能设备上会以多进程的方式运行基础服务,但是如果在资源受限的设备上(如下图),Chrome会将很多服务整合到一个进程中,从而节省内存占用。

Screen Shot 2021-05-28 at 3.02.14 PM.png
总结

好了,今天就到这里,下面我来简要梳理并总结今天的内容。
本文我主要是从Chrome进程架构的视角,分析了浏览器的进化史。
最初的浏览器都是单进程的,它们不稳定、不流畅且不安全,之后出现了Chrome,创造性地引入了多进程架构,并解决了这些遗留问题。随后Chrome试图应用到更多业务场景,如移动设备、VR、视频等,为了支持这些场景,Chrome的架构体系变得越来越复杂,这种架构的复杂性倒逼Chrome开发团队必须进行架构的重构,最终Chrome团队选择了面向服务架构(SOA)形式,这也是Chrome团队现阶段的一个主要任务。
鉴于目前架构的复杂性,要完整过渡到面向服务架构,估计还需要好几年时间才能完成。不过Chrome开发是一个渐进的过程,新的特性会一点点加入进来,这也意味着我们随时能看到Chrome新的变化。
总体说来,Chrome是以一个非常快速的速度在进化,越来越多的业务和应用都逐渐转至浏览器来开发,身为开发人员,我们不能坐视不管,而应该紧跟其步伐,收获这波技术红利

TCP协议:如何保证页面文件能被完整送达浏览器

一个数据包的“旅程”

互联网,实际上是一套理念和协议组成的体系架构。其中,协议是一套众所周知的规则和标准,如果各方都同意使用,那么它们之间的通信将变得毫无障碍

1. IP:把数据包送达目的主机

计算机的地址就称为IP地址,访问任何网站实际上只是你的计算机向另外一台计算机请求信息。

Screen Shot 2021-05-29 at 12.14.22 AM.png
下面我们一起来看下一个数据包从主机A到主机B的旅程:

  • 上层将含有“极客时间”的数据包交给网络层;
  • 网络层再将IP头附加到数据包上,组成新的 IP数据包,并交给底层;
  • 底层通过物理网络将数据包传输给主机B;
  • 数据包被传输到主机B的网络层,在这里主机B拆开数据包的IP头信息,并将拆开来的数据部分交给上层;
  • 最终,含有“极客时间”信息的数据包就到达了主机B的上层了


    2. UDP:把数据包送达应用程序

    IP是非常底层的协议,只负责把数据包传送到对方电脑,但是对方电脑并不知道把数据包交给哪个程序,是交给浏览器还是交给王者荣耀?因此,需要基于IP之上开发能和应用打交道的协议,最常见的是“用户数据包协议(User Datagram Protocol)”,简称UDP
    UDP中一个最重要的信息是端口号,端口号其实就是一个数字,每个想访问网络的程序都需要绑定一个端口号。通过端口号UDP就能把指定的数据包发送给指定的程序了,所以IP通过IP地址信息把数据包发送给指定的电脑,而UDP通过端口号把数据包分发给正确的程序。和IP头一样,端口号会被装进UDP头里面,UDP头再和原始数据包合并组成新的UDP数据包。UDP头中除了目的端口,还有源端口号等信息
    为了支持UDP协议,我把前面的三层结构扩充为四层结构,在网络层和上层之间增加了传输层,如下图所示:Screen Shot 2021-05-29 at 12.18.11 AM.png
    下面我们一起来看下一个数据包从主机A旅行到主机B的路线:

  • 上层将含有“极客时间”的数据包交给传输层;

  • 传输层会在数据包前面附加上UDP头,组成新的UDP数据包,再将新的UDP数据包交给网络层;
  • 网络层再将IP头附加到数据包上,组成新的IP数据包,并交给底层;
  • 数据包被传输到主机B的网络层,在这里主机B拆开IP头信息,并将拆开来的数据部分交给传输层;
  • 在传输层,数据包中的UDP头会被拆开,并根据UDP中所提供的端口号,把数据部分交给上层的应用程序;
  • 最终,含有“极客时间”信息的数据包就旅行到了主机B上层应用程序这里

在使用UDP发送数据时,有各种因素会导致数据包出错,虽然UDP可以校验数据是否正确,但是对于错误的数据包,UDP并不提供重发机制,只是丢弃当前的包,而且UDP在发送之后也无法知道是否能达到目的地。
虽说UDP不能保证数据可靠性,但是传输速度却非常快,所以UDP会应用在一些关注速度、但不那么严格要求数据完整性的领域,如在线视频、互动游戏等

3. TCP:把数据完整地送达应用程序

对于浏览器请求,或者邮件这类要求数据传输可靠性(reliability)的应用,如果使用UDP来传输会存在两个问题:

  • 数据包在传输过程中容易丢失;
  • 大文件会被拆分成很多小的数据包来传输,这些小的数据包会经过不同的路由,并在不同的时间到达接收端,而UDP协议并不知道如何组装这些数据包,从而把这些数据包还原成完整的文件

基于这两个问题,我们引入TCP了。TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。相对于UDP,TCP有下面两个特点:

  • 对于数据包丢失的情况,TCP提供重传机制;
  • TCP引入了数据包排序机制,用来保证把乱序的数据包组合成一个完整的文件。

和UDP头一样,TCP头除了包含了目标端口和本机端口号外,还提供了用于排序的序列号,以便接收端通过序号来重排数据包。
下面看看TCP下的单个数据包的传输流程
Screen Shot 2021-05-29 at 12.24.49 AM.png
通过上图你应该可以了解一个数据包是如何通过TCP来传输的。TCP单个数据包的传输流程和UDP流程差不多,不同的地方在于,通过TCP头的信息保证了一块大的数据传输的完整性。
下面我们再看下完整的TCP连接过程,通过这个过程你可以明白TCP是如何保证重传机制和数据包的排序功能的。
Screen Shot 2021-05-29 at 12.26.23 AM.png

  • 首先,建立连接阶段。这个阶段是通过“三次握手”来建立客户端和服务器之间的连接。TCP 提供面向连接的通信传输。面向连接是指在数据通信开始之前先做好两端之间的准备工作。所谓三次握手,是指在建立一个TCP连接时,客户端和服务器总共要发送三个数据包以确认连接的建立。
  • 其次,传输数据阶段。在该阶段,接收端需要对每个数据包进行确认操作,也就是接收端在接收到数据包之后,需要发送确认数据包给发送端。所以当发送端发送了一个数据包之后,在规定时间内没有接收到接收端反馈的确认消息,则判断为数据包丢失,并触发发送端的重发机制。同样,一个大的文件在传输过程中会被拆分成很多小的数据包,这些数据包到达接收端后,接收端会按照TCP头中的序号为其排序,从而保证组成完整的数据。
  • 最后,断开连接阶段。数据传输完毕之后,就要终止连接了,涉及到最后一个阶段“四次挥手”来保证双方都能断开连接

到这里你应该就明白了,TCP为了保证数据传输的可靠性,牺牲了数据包的传输速度,因为“三次握手”和“数据包校验机制”等把传输过程中的数据包的数量提高了一倍。


总结

好了,这一节就到这里,下面我来做一个简单的总结。

  • 互联网中的数据是通过数据包来传输的,数据包在传输过程中容易丢失或出错。
  • IP负责把数据包送达目的主机。
  • UDP负责把数据包送达具体应用。
  • 而TCP保证了数据完整地传输,它的连接可分为三个阶段:建立连接、传输数据和断开连接。
  • 其实了解TCP协议,是为了全方位了解HTTP,包括其实际功能和局限性,之后才会更加深刻地理解为什么要推出HTTP/2,以及为什么要推出QUIC协议,也就是未来的HTTP/3。这是一个由浅入深、循序渐进的过程,我希望你能稳扎稳打,学好这每一步、每一个协议,后面“水到自然渠成”

HTTP请求流程:为什么很多站点第二次打开速度会很快


一个TCP连接过程包括了建立连接、传输数据和断开连接三个阶段。
而HTTP协议,正是建立在TCP连接基础之上的。HTTP是一种允许浏览器向服务器获取资源的协议,是Web的基础,通常由浏览器发起请求,用来获取不同类型的文件,例如HTML文件、CSS文件、JavaScript文件、图片、视频等。此外,HTTP也是浏览器使用最广的协议,所以要想学好浏览器,就要先深入了解HTTP。
不知道你是否有过下面这些疑问:

  1. 为什么通常在第一次访问一个站点时,打开速度很慢,当再次访问这个站点时,速度就很快了?
  2. 当登录过一个网站之后,下次再访问该站点,就已经处于登录状态了,这是怎么做到的呢?

浏览器端发起HTTP请求流程

如果你在浏览器地址栏里键入地址:http://baidu.com, 那么接下来,浏览器会完成哪些动作呢?下面我们就一步一步详细“追踪”下

1. 构建请求

首先,浏览器构建请求行信息(如下所示),构建好后,浏览器准备发起网络请求

GET /index.html HTTP1.1

2. 查找缓存

在真正发起网络请求之前,浏览器会先在浏览器缓存中查询是否有要请求的文件。其中,浏览器缓存是一种在本地保存资源副本,以供下次请求时直接使用的技术。
当浏览器发现请求的资源已经在浏览器缓存中存有副本,它会拦截请求,返回该资源的副本,并直接结束请求,而不会再去源服务器重新下载。这样做的好处有:

  • 缓解服务器端压力,提升性能(获取资源的耗时更短了);
  • 对于网站来说,缓存是实现快速资源加载的重要组成部分。

当然,如果缓存查找失败,就会进入网络请求过程了。


3. 准备IP地址和端口

我们需要先看看HTTP和TCP的关系。因为浏览器使用HTTP协议作为应用层协议,用来封装请求的文本信息;并使用TCP/IP作传输层协议将它发到网络上,所以在HTTP工作开始之前,浏览器需要通过TCP与服务器建立连接。也就是说HTTP的内容是通过TCP的传输数据阶段来实现的,你可以结合下图更好地理解这二者的关系。
Screen Shot 2021-05-29 at 12.36.33 AM.png
那接下来你可以思考这么“一连串”问题:

  • HTTP网络请求的第一步是做什么呢?结合上图看,是和服务器建立TCP连接。
  • 那建立连接的信息都有了吗?上一篇文章中,我们讲到建立TCP连接的第一步就是需要准备IP地址和端口号。
  • 那怎么获取IP地址和端口号呢?这得看看我们现在有什么,我们有一个URL地址,那么是否可以利用URL地址来获取IP和端口信息呢?

在上一篇文章中,我们介绍过数据包都是通过IP地址传输给接收方的。由于IP地址是数字标识,比如极客时间网站的IP是39.106.233.176, 难以记忆,但使用极客时间的域名(time.geekbang.org)就好记多了,所以基于这个需求又出现了一个服务,负责把域名和IP地址做一一映射关系。这套域名映射为IP的系统就叫做“域名系统”,简称DNS(Domain Name System)。
所以,这样一路推导下来,你会发现在第一步浏览器会请求DNS返回域名对应的IP。当然浏览器还提供了DNS数据缓存服务,如果某个域名已经解析过了,那么浏览器会缓存解析的结果,以供下次查询时直接使用,这样也会减少一次网络请求拿到IP之后,接下来就需要获取端口号了。通常情况下,如果URL没有特别指明端口号,那么HTTP协议默认是80端口。

等待TCP队列

现在已经把端口和IP地址都准备好了,那么下一步是不是可以建立TCP连接了呢?
答案依然是“不行”。Chrome有个机制,同一个域名同时最多只能建立6个TCP连接,如果在同一个域名下同时有10个请求发生,那么其中4个请求会进入排队等待状态,直至进行中的请求完成。
当然,如果当前请求数量少于6,会直接进入下一步,建立TCP连接。

建立TCP连接

排队等待结束之后,终于可以快乐地和服务器握手了,在HTTP工作开始之前,浏览器通过TCP与服务器建立连接。而TCP的工作方式,我在上一篇文章中已经做过详细介绍了,如果有必要,你可以自行回顾下,这里我就不再重复讲述了。

发送HTTP请求

一旦建立了TCP连接,浏览器就可以和服务器进行通信了。而HTTP中的数据正是在这个通信过程中传输的。你可以结合下图来理解,浏览器是如何发送请求信息给服务器的。
Screen Shot 2021-05-29 at 12.42.38 AM.png
在浏览器发送请求行命令之后,还要以请求头形式发送其他一些信息,把浏览器的一些基础信息告诉服务器。比如包含了浏览器所使用的操作系统、浏览器内核等信息,以及当前请求的域名信息、浏览器端的Cookie信息,等等

服务器端处理HTTP请求流程

1. 返回请求

一旦服务器处理结束,便可以返回数据给浏览器了。你可以通过工具软件curl来查看返回请求数据,具体使用方法是在命令行中输入以下命令:
curl -i https://time.geekbang.org/
注意这里加上了-i是为了返回响应行、响应头和响应体的数据,返回的结果如下图所示,你可以结合这些数据来理解服务器是如何响应浏览器的。
Screen Shot 2021-05-29 at 12.49.52 AM.png
首先服务器会返回响应行,包括协议版本和状态码。
但并不是所有的请求都可以被服务器处理的,那么一些无法处理或者处理出错的信息,怎么办呢?服务器会通过请求行的状态码来告诉浏览器它的处理结果,比如:

  • 最常用的状态码是200,表示处理成功;
  • 如果没有找到页面,则会返回404-

随后,正如浏览器会随同请求发送请求头一样,服务器也会随同响应向浏览器发送响应头。响应头包含了服务器自身的一些信息,比如服务器生成返回数据的时间、返回的数据类型(JSON、HTML、流媒体等类型),以及服务器要在客户端保存的Cookie等信息。
发送完响应头后,服务器就可以继续发送响应体的数据,通常,响应体就包含了HTML的实际内容。
以上这些就是服务器响应浏览器的具体过程


2. 断开连接

通常情况下,一旦服务器向客户端返回了请求数据,它就要关闭 TCP 连接。不过如果浏览器或者服务器在其头信息中加入了:
Connection:Keep-Alive
那么TCP连接在发送后将仍然保持打开状态,这样浏览器就可以继续通过同一个TCP连接发送请求。保持TCP连接可以省去下次请求时需要建立连接的时间,提升资源加载速度。比如,一个Web页面中内嵌的图片就都来自同一个Web站点,如果初始化了一个持久连接,你就可以复用该连接,以请求其他资源,而不需要重新再建立新的TCP连接。

3. 重定向

到这里似乎请求流程快结束了,不过还有一种情况你需要了解下,比如当你在浏览器中打开geekbang.org后,你会发现最终打开的页面地址是 https://www.geekbang.org。
这两个URL之所以不一样,是因为涉及到了一个重定向操作。跟前面一样,你依然可以使用curl来查看下请求geekbang.org 会返回什么内容?
在控制台输入如下命令:
curl -I geekbang.org
注意这里输入的参数是-I,和-i不一样,-I表示只需要获取响应头和响应行数据,而不需要获取响应体的数据,最终返回的数据如下图所示:
Screen Shot 2021-05-29 at 12.59.03 AM.png
从图中你可以看到,响应行返回的状态码是301,状态301就是告诉浏览器,我需要重定向到另外一个网址,而需要重定向的网址正是包含在响应头的Location字段中,接下来,浏览器获取Location字段中的地址,并使用该地址重新导航,这就是一个完整重定向的执行流程。这也就解释了为什么输入的是 geekbang.org,最终打开的却是 https://www.geekbang.org 了。
不过也不要认为这种跳转是必然的。如果你打开 https://12306.cn,你会发现这个站点是打不开的。这是因为12306的服务器并没有处理跳转,所以必须要手动输入完整的 https://www.12306.com 才能打开页面。


问题解答


说了这么多,相信你现在已经了解了HTTP的请求流程,那现在我们再回过头来看看文章开头提出的问题。

1. 为什么很多站点第二次打开速度会很快?

如果第二次页面打开很快,主要原因是第一次加载页面过程中,缓存了一些耗时的数据。
那么,哪些数据会被缓存呢?从上面介绍的核心请求路径可以发现,DNS缓存和页面资源缓存这两块数据是会被浏览器缓存的。其中,DNS缓存比较简单,它主要就是在浏览器本地把对应的IP和域名关联起来,这里就不做过多分析了。
我们重点看下浏览器资源缓存,下面是缓存处理的过程:
Screen Shot 2021-05-29 at 1.05.33 AM.png
首先,我们看下服务器是通过什么方式让浏览器缓存数据的?
从上图的第一次请求可以看出,当服务器返回HTTP响应头给浏览器时,浏览器是通过响应头中的Cache-Control字段来设置是否缓存该资源。通常,我们还需要为这个资源设置一个缓存过期时长,而这个时长是通过Cache-Control中的Max-age参数来设置的,比如上图设置的缓存过期时间是2000秒。
Cache-Control:Max-age=2000
这也就意味着,在该缓存资源还未过期的情况下, 如果再次请求该资源,会直接返回缓存中的资源给浏览器。
但如果缓存过期了,浏览器则会继续发起网络请求,并且在HTTP请求头中带上:
If-None-Match:”4f80f-13c-3a1xb12a”
服务器收到请求头后,会根据If-None-Match的值来判断请求的资源是否有更新。

  • 如果没有更新,就返回304状态码,相当于服务器告诉浏览器:“这个缓存可以继续使用,这次就不重复发送数据给你了。”
  • 如果资源有更新,服务器就直接返回最新资源给浏览器。

简要来说,很多网站第二次访问能够秒开,是因为这些网站把很多资源都缓存在了本地,浏览器缓存直接使用本地副本来回应请求,而不会产生真实的网络请求,从而节省了时间。同时,DNS数据也被浏览器缓存了,这又省去了DNS查询环节

2. 登录状态是如何保持的?

  • 通过上面的介绍,你已经了解了缓存是如何工作的。下面我们再一起看下登录状态是如何保持的。
  • 用户打开登录页面,在登录框里填入用户名和密码,点击确定按钮。点击按钮会触发页面脚本生成用户登录信息,然后调用POST方法提交用户登录信息给服务器。
  • 服务器接收到浏览器提交的信息之后,查询后台,验证用户登录信息是否正确,如果正确的话,会生成一段表示用户身份的字符串,并把该字符串写到响应头的Set-Cookie字段里,如下所示,然后把响应头发送给浏览器

Set-Cookie: UID=3431uad;

  • 浏览器在接收到服务器的响应头后,开始解析响应头,如果遇到响应头里含有Set-Cookie字段的情况,浏览器就会把这个字段信息保存到本地。比如把UID=3431uad保持到本地。
  • 当用户再次访问时,浏览器会发起HTTP请求,但在发起请求之前,浏览器会读取之前保存的Cookie数据,并把数据写进请求头里的Cookie字段里(如下所示),然后浏览器再将请求头发送给服务器。

Cookie: UID=3431uad;

  • 服务器在收到HTTP请求头数据之后,就会查找请求头里面的“Cookie”字段信息,当查找到包含UID=3431uad的信息时,服务器查询后台,并判断该用户是已登录状态,然后生成含有该用户信息的页面数据,并把生成的数据发送给浏览器。
  • 浏览器在接收到该含有当前用户的页面数据后,就可以正确展示用户登录的状态信息了

好了,通过这个流程你可以知道浏览器页面状态是通过使用Cookie来实现的。Cookie流程可以参考下图:

Screen Shot 2021-05-29 at 1.12.17 AM.png

简单地说,如果服务器端发送的响应头内有 Set-Cookie 的字段,那么浏览器就会将该字段的内容保持到本地。当下次客户端再往该服务器发送请求时,客户端会自动在请求头中加入 Cookie 值后再发送出去。服务器端发现客户端发送过来的Cookie后,会去检查究竟是从哪一个客户端发来的连接请求,然后对比服务器上的记录,最后得到该用户的状态信息。

总结

本篇文章的内容比较多、比较碎,但是非常重要,所以我先来总结下今天的主要内容。
为了便于你理解,我画了下面这张详细的“HTTP请求示意图”,用来展现浏览器中的HTTP请求所经历的各个阶段。
image.png
从图中可以看到,浏览器中的HTTP请求从发起到结束一共经历了如下八个阶段:构建请求、查找缓存、准备IP和端口、等待TCP队列、建立TCP连接、发起HTTP请求、服务器处理请求、服务器返回请求和断开连接。
然后我还通过HTTP请求路径解答了两个经常会碰到的问题,一个涉及到了Cache流程,另外一个涉及到如何使用Cookie来进行状态管理。
通过今天系统的讲解,想必你已经了解了一个HTTP完整的工作流程,相信这些知识点之于你以后的学习或工作会很有帮助。

导航流程:从输入URL到页面展示这中间发生了什么

那么今天我们就一起来探索下这个流程,下图是我梳理出的“从输入URL到页面展示完整流程示意图”:
image.png

从图中可以看出,整个过程需要各个进程之间的配合,所以在开始正式流程之前,我们还是先来快速回顾下浏览器进程、渲染进程和网络进程的主要职责。

  • 浏览器进程主要负责用户交互、子进程管理和文件储存等功能。
  • 网络进程是面向渲染进程和浏览器进程等提供网络下载功能。
  • 渲染进程的主要职责是把从网络下载的HTML、JavaScript、CSS、图片等资源解析为可以显示和交互的页面。因为渲染进程所有的内容都是通过网络获取的,会存在一些恶意代码利用浏览器漏洞对系统进行攻击,所以运行在渲染进程里面的代码是不被信任的。这也是为什么Chrome会让渲染进程运行在安全沙箱里,就是为了保证系统的安全

回顾了浏览器的进程架构后,我们再结合上图来看下这个完整的流程,可以看出,整个流程包含了许多步骤,我把其中几个核心的节点用蓝色背景标记出来了。这个过程可以大致描述为如下:

  • 首先,用户从浏览器进程里输入请求信息;
  • 然后,网络进程发起URL请求;
  • 服务器响应URL请求之后,浏览器进程就又要开始准备渲染进程了;
  • 渲染进程准备好之后,需要先向渲染进程提交页面数据,我们称之为提交文档阶段;
  • 渲染进程接收完文档信息之后,便开始解析页面和加载子资源,完成页面的渲染。

这其中,用户发出URL请求到页面开始解析的这个过程,就叫做导航。下面我们来详细分析下这些步骤,同时也就解答了开头所说的那道经典的面试题。

从输入URL到页面展示

1. 用户输入

当用户在地址栏中输入一个查询关键字时,地址栏会判断输入的关键字是搜索内容,还是请求的URL。

  • 如果是搜索内容,地址栏会使用浏览器默认的搜索引擎,来合成新的带搜索关键字的URL。
  • 如果判断输入内容符合URL规则,比如输入的是 time.geekbang.org,那么地址栏会根据规则,把这段内容加上协议,合成为完整的URL,如 https://time.geekbang.org。

当用户输入关键字并键入回车之后,浏览器便进入下图的状态:
image.png
从图中可以看出,当浏览器刚开始加载一个地址之后,标签页上的图标便进入了加载状态。但此时图中页面显示的依然是之前打开的页面内容,并没立即替换为极客时间的页面。因为需要等待提交文档阶段,页面内容才会被替换。

2. URL请求过程

接下来,便进入了页面资源请求过程。这时,浏览器进程会通过进程间通信(IPC)把URL请求发送至网络进程,网络进程接收到URL请求后,会在这里发起真正的URL请求流程。那具体流程是怎样的呢?
首先,网络进程会查找本地缓存是否缓存了该资源。如果有缓存资源,那么直接返回资源给浏览器进程;如果在缓存中没有查找到资源,那么直接进入网络请求流程。这请求前的第一步是要进行DNS解析,以获取请求域名的服务器IP地址。如果请求协议是HTTPS,那么还需要建立TLS连接。
接下来就是利用IP地址和服务器建立TCP连接。连接建立之后,浏览器端会构建请求行、请求头等信息,并把和该域名相关的Cookie等数据附加到请求头中,然后向服务器发送构建的请求信息。
服务器接收到请求信息后,会根据请求信息生成响应数据(包括响应行、响应头和响应体等信息),并发给网络进程。等网络进程接收了响应行和响应头之后,就开始解析响应头的内容了。(为了方便讲述,下面我将服务器返回的响应头和响应行统称为响应头。)
(1)重定向
在接收到服务器返回的响应头后,网络进程开始解析响应头,如果发现返回的状态码是301或者302,那么说明服务器需要浏览器重定向到其他URL。这时网络进程会从响应头的Location字段里面读取重定向的地址,然后再发起新的HTTP或者HTTPS请求,一切又重头开始了。
比如,我们在终端里输入以下命令:
curl -I http://time.geekbang.org/
curl -I + URL的命令是接收服务器返回的响应头的信息。执行命令后,我们看到服务器返回的响应头信息如下:
image.png
从图中可以看出,极客时间服务器会通过重定向的方式把所有HTTP请求转换为HTTPS请求。也就是说你使用HTTP向极客时间服务器请求时,服务器会返回一个包含有301或者302状态码响应头,并把响应头的Location字段中填上HTTPS的地址,这就是告诉了浏览器要重新导航到新的地址上。
下面我们再使用HTTPS协议对极客时间发起请求,看看服务器的响应头信息是什么样子的。
curl -I https://time.geekbang.org/
我们看到服务器返回如下信息:
image.png
从图中可以看出,服务器返回的响应头的状态码是200,这是告诉浏览器一切正常,可以继续往下处理该请求了。
好了,以上是重定向内容的介绍。现在你应该理解了,在导航过程中,如果服务器响应行的状态码包含了301、302一类的跳转信息,浏览器会跳转到新的地址继续导航;如果响应行是200,那么表示浏览器可以继续处理该请求。
(2)响应数据类型处理
在处理了跳转信息之后,我们继续导航流程的分析。URL请求的数据类型,有时候是一个下载类型,有时候是正常的HTML页面,那么浏览器是如何区分它们呢?
答案是Content-Type。Content-Type是HTTP头中一个非常重要的字段, 它告诉浏览器服务器返回的响应体数据是什么类型,然后浏览器会根据Content-Type的值来决定如何显示响应体的内容。
这里我们还是以极客时间为例,看看极客时间官网返回的Content-Type值是什么。在终端输入以下命令:
curl -I https://time.geekbang.org/
返回信息如下图:
image.png
从图中可以看到,响应头中的Content-type字段的值是text/html,这就是告诉浏览器,服务器返回的数据是HTML格式。
接下来我们再来利用curl来请求极客时间安装包的地址,如下所示:
curl -I https://res001.geekbang.org/apps/geektime/android/2.3.1/official/geektime_2.3.1_20190527-2136_offical.apk
请求后返回的响应头信息如下:
image.png
从返回的响应头信息来看,其Content-Type的值是application/octet-stream,显示数据是字节流类型的,通常情况下,浏览器会按照下载类型来处理该请求。
需要注意的是,如果服务器配置Content-Type不正确,比如将text/html类型配置成application/octet-stream类型,那么浏览器可能会曲解文件内容,比如会将一个本来是用来展示的页面,变成了一个下载文件。
所以,不同Content-Type的后续处理流程也截然不同。如果Content-Type字段的值被浏览器判断为下载类型,那么该请求会被提交给浏览器的下载管理器,同时该URL请求的导航流程就此结束。但如果是HTML,那么浏览器则会继续进行导航流程。由于Chrome的页面渲染是运行在渲染进程中的,所以接下来就需要准备渲染进程了。

3. 准备渲染进程

默认情况下,Chrome会为每个页面分配一个渲染进程,也就是说,每打开一个新页面就会配套创建一个新的渲染进程。但是,也有一些例外,在某些情况下,浏览器会让多个页面直接运行在同一个渲染进程中。
比如我从极客时间的首页里面打开了另外一个页面——算法训练营,我们看下图的Chrome的任务管理器截图:
image.png
从图中可以看出,打开的这三个页面都是运行在同一个渲染进程中,进程ID是23601。
那什么情况下多个页面会同时运行在一个渲染进程中呢?
要解决这个问题,我们就需要先了解下什么是同一站点(same-site)。具体地讲,我们将“同一站点”定义为根域名(例如,geekbang.org)加上协议(例如,https:// 或者http://),还包含了该根域名下的所有子域名和不同的端口,比如下面这三个:
https://time.geekbang.org https://www.geekbang.org https://www.geekbang.org:8080
它们都是属于同一站点,因为它们的协议都是HTTPS,而且根域名也都是geekbang.org。
Chrome的默认策略是,每个标签对应一个渲染进程。但如果从一个页面打开了另一个新页面,而新页面和当前页面属于同一站点的话,那么新页面会复用父页面的渲染进程。官方把这个默认策略叫process-per-site-instance。
那若新页面和当前页面不属于同一站点,情况又会发生什么样的变化呢?比如我通过极客邦页面里的链接打开InfoQ的官网(https://www.infoq.cn/ ), 因为infoq.cn和geekbang.org不属于同一站点,所以infoq.cn会使用一个新的渲染进程,你可以参考下图
image.png
从图中任务管理器可以看出:由于极客邦和极客时间的标签页拥有相同的协议和根域名,所以它们属于同一站点,并运行在同一个渲染进程中;而infoq.cn的根域名不同于geekbang.org,也就是说InfoQ和极客邦不属于同一站点,因此它们会运行在两个不同的渲染进程之中。
总结来说,打开一个新页面采用的渲染进程策略就是:

  • 通常情况下,打开新的页面都会使用单独的渲染进程;
  • 如果从A页面打开B页面,且A和B都属于同一站点的话,那么B页面复用A页面的渲染进程;如果是其他情况,浏览器进程则会为B创建一个新的渲染进程。

渲染进程准备好之后,还不能立即进入文档解析状态,因为此时的文档数据还在网络进程中,并没有提交给渲染进程,所以下一步就进入了提交文档阶段。

4. 提交文档

首先要明确一点,这里的“文档”是指URL请求的响应体数据。

  • “提交文档”的消息是由浏览器进程发出的,渲染进程接收到“提交文档”的消息后,会和网络进程建立传输数据的“管道”。
  • 等文档数据传输完成之后,渲染进程会返回“确认提交”的消息给浏览器进程。
  • 浏览器进程在收到“确认提交”的消息后,会更新浏览器界面状态,包括了安全状态、地址栏的URL、前进后退的历史状态,并更新Web页面。

更新内容如下图所示:
image.png
这也就解释了为什么在浏览器的地址栏里面输入了一个地址后,之前的页面没有立马消失,而是要加载一会儿才会更新页面。
到这里,一个完整的导航流程就“走”完了,这之后就要进入渲染阶段了。

5. 渲染阶段

一旦文档被提交,渲染进程便开始页面解析和子资源加载了,关于这个阶段的完整过程,我会在下一篇文章中来专门介绍。这里你只需要先了解一旦页面生成完成,渲染进程会发送一个消息给浏览器进程,浏览器接收到消息后,会停止标签图标上的加载动画。如下所示:
image.png
至此,一个完整的页面就生成了。那文章开头的“从输入URL到页面展示,这中间发生了什么?”这个过程极其“串联”的问题也就解决了。

总结:从输入URL到页面展示,这中间发生了什么

  • 用户输入url并回车
  • 浏览器进程检查url,组装协议,构成完整的url
  • 浏览器进程通过进程间通信(IPC)把url请求发送给网络进程
  • 网络进程接收到url请求后检查本地缓存是否缓存了该请求资源,如果有则将该资源返回给浏览器进程
  • 如果没有,网络进程向web服务器发起http请求(网络请求),请求流程如下
    • 进行DNS解析,获取服务器ip地址,端口(端口是通过dns解析获取的吗?这里有个疑问)
    • 利用ip地址和服务器建立tcp连接
    • 构建请求头信息
    • 发送请求头信息
    • 服务器响应后,网络进程接收响应头和响应信息,并解析响应内容
  • 网络进程解析响应流程
    • 检查状态码,如果是301/302,则需要重定向,从Location自动中读取地址,重新进行第4步 (301/302跳转也会读取本地缓存吗?这里有个疑问),如果是200,则继续处理请求。
    • 200响应处理:检查响应类型Content-Type,如果是字节流类型,则将该请求提交给下载管理器,该导航流程结束,不再进行后续的渲染,如果是html则通知浏览器进程准备渲染进程准备进行渲染。
  • 准备渲染进程
    • 浏览器进程检查当前url是否和之前打开的渲染进程根域名是否相同,如果相同,则复用原来的进程,如果不同,则开启新的渲染进程
  • 传输数据、更新状态

    • 渲染进程准备好后,浏览器向渲染进程发起“提交文档”的消息,渲染进程接收到消息和网络进程建立传输数据的“管道”
    • 渲染进程接收完数据后,向浏览器发送“确认提交”
    • 浏览器进程接收到确认消息后更新浏览器界面状态:安全、地址栏url、前进后退的历史状态、更新web页面

      总结

      好了,今天就到这里,下面我来简单总结下这篇文章的要点:
  • 服务器可以根据响应头来控制浏览器的行为,如跳转、网络数据类型判断。

  • Chrome默认采用每个标签对应一个渲染进程,但是如果两个页面属于同一站点,那这两个标签会使用同一个渲染进程。
  • 浏览器的导航过程涵盖了从用户发起请求到提交文档给渲染进程的中间所有阶段。
  • 导航流程很重要,它是网络加载流程和渲染流程之间的一座桥梁,如果你理解了导航流程,那么你就能完整串起来整个页面显示流程,这对于你理解浏览器的工作原理起到了点睛的作用

渲染流程(上):HTML、CSS和JavaScript是如何变成页面的

在上一篇文章中我们介绍了导航相关的流程,那导航被提交后又会怎么样呢?就进入了渲染阶段。这个阶段很重要,了解其相关流程能让你“看透”页面是如何工作的,有了这些知识,你可以解决一系列相关的问题,比如能熟练使用开发者工具,因为能够理解开发者工具里面大部分项目的含义,能优化页面卡顿问题,使用JavaScript优化动画流程,通过优化样式表来防止强制同步布局,等等。
既然它的功能这么强大,那么今天,我们就来好好聊聊渲染流程。
通常,我们编写好HTML、CSS、JavaScript等文件,经过浏览器就会显示出漂亮的页面(如下图所示),但是你知道它们是如何转化成页面的吗?这背后的原理,估计很多人都答不上来。
image.png
从图中可以看出,左边输入的是HTML、CSS、JavaScript数据,这些数据经过中间渲染模块的处理,最终输出为屏幕上的像素。
这中间的渲染模块就是我们今天要讨论的主题。为了能更好地理解下文,你可以先结合下图快速抓住HTML、CSS和JavaScript的含义:
image.png
从上图可以看出,HTML的内容是由标记和文本组成。标记也称为标签,每个标签都有它自己的语意,浏览器会根据标签的语意来正确展示HTML内容。比如上面的

标签是告诉浏览器在这里的内容需要创建一个新段落,中间的文本就是段落中需要显示的内容
如果需要改变HTML的字体颜色、大小等信息,就需要用到CSS。CSS又称为层叠样式表,是由选择器和属性组成,比如图中的p选择器,它会把HTML里面

标签的内容选择出来,然后再把选择器的属性值应用到

标签内容上。选择器里面有个color属性,它的值是red,这是告诉渲染引擎把

标签的内容显示为红色
至于JavaScript(简称为JS),使用它可以使网页的内容“动”起来,比如上图中,可以通过JavaScript来修改CSS样式值,从而达到修改文本颜色的目的。
搞清楚HTML、CSS和JavaScript的含义后,那么接下来我们就正式开始分析渲染模块了。
由于渲染机制过于复杂,所以渲染模块在执行过程中会被划分为很多子阶段,输入的HTML经过这些子阶段,最后输出像素。我们把这样的一个处理流程叫做渲染流水线,其大致流程如下图所示:
image.png
按照渲染的时间顺序,流水线可分为如下几个子阶段:构建DOM树、样式计算、布局阶段、分层、绘制、分块、光栅化和合成。内容比较多,我会用两篇文章来为你详细讲解这各个子阶段。接下来,在介绍每个阶段的过程中,你应该重点关注以下三点内容

  • 开始每个子阶段都有其输入的内容;
  • 然后每个子阶段有其处理过程;
  • 最终每个子阶段会生成输出内容。

理解了这三部分内容,能让你更加清晰地理解每个子阶段。

构建DOM树

为什么要构建DOM树呢?这是因为浏览器无法直接理解和使用HTML,所以需要将HTML转换为浏览器能够理解的结构——DOM树。
这里我们还需要简单介绍下什么是树结构,为了更直观地理解,你可以参考下面我画的几个树结构:
image.png
从图中可以看出,树这种结构非常像我们现实生活中的“树”,其中每个点我们称为节点,相连的节点称为父子节点。树结构在浏览器中的应用还是比较多的,比如下面我们要介绍的渲染流程,就在频繁地使用树结构。
接下来咱们还是言归正传,来看看DOM树的构建过程,你可以参考下图
image.png
从图中可以看出,构建DOM树的输入内容是一个非常简单的HTML文件,然后经由HTML解析器解析,最终输出树状结构的DOM。
为了更加直观地理解DOM树,你可以打开Chrome的“开发者工具”,选择“Console”标签来打开控制台,然后在控制台里面输入“document”后回车,这样你就能看到一个完整的DOM树结构,如下图所示:
image.png
图中的document就是DOM结构,你可以看到,DOM和HTML内容几乎是一样的,但是和HTML不同的是,DOM是保存在内存中树状结构,可以通过JavaScript来查询或修改其内容。
那下面就来看看如何通过JavaScript来修改DOM的内容,在控制台中输入:
document.getElementsByTagName(“p”)[0].innerText = “black”
这行代码的作用是把第一个

标签的内容修改为black,具体执行结果你可以参考下图:
image.png
从图中可以看出,在执行了一段修改第一个

标签的JavaScript代码后,DOM的第一个p节点的内容成功被修改,同时页面中的内容也被修改了
好了,现在我们已经生成DOM树了,但是DOM节点的样式我们依然不知道,要让DOM节点拥有正确的样式,这就需要样式计算了

样式计算

样式计算的目的是为了计算出DOM节点中每个元素的具体样式,这个阶段大体可分为三步来完成

1. 把CSS转换为浏览器能够理解的结构

那CSS样式的来源主要有哪些呢?你可以先参考下图:
image.png
从图中可以看出,CSS样式来源主要有三种:

  • 通过link引用的外部CSS文件