从输入 URL 到页面展示发生了什么?

6 篇文章 0 订阅
1 篇文章 0 订阅

从输入 URL 到页面展示发生了什么?

“在浏览器里,从输入 URL 到页面展示,这中间发生了什么? ”这是一道经典的面试题,能比较全面地考察应聘者知识的掌握程度,其中涉及到了网络、操作系统、Web 等一系列的知识。

今天就结合下图对这个过程进行分析。当然实际过程还远比这张图复杂得多。

图一

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

文章内容概况

一、用户输入

用户输入查询关键字后,地址栏会判断输入的关键字是搜索内容还是请求的 URL。

  • 如果是搜索内容,地址栏会使用浏览器默认的搜索引擎,来合成带关键字的 URL。
  • 如果输入内容符合 URL 规则,地址栏会根据规则,把内容加上协议,合成完整的 URL。

当用户输入关键字并键入回车之后,这意味着当前页面即将要被替换成新的页面,不过在这个流程继续之前,浏览器还给了当前页面一次执行 beforeunload 事件的机会,beforeunload 事件允许页面在退出之前执行一些数据清理操作,还可以询问用户是否要离开当前页面,比如当前页面可能有未提交完成的表单等情况,因此用户可以通过 beforeunload 事件来取消导航,让浏览器不再执行任何后续工作。 当前页面没有监听 beforeunload 事件或者同意了继续后续流程,那么浏览器就进入加载状态。

总结就是一句话,浏览器进程处理完URL后,浏览器进程会发出 URL 请求给网络进程。

二、网络请求

接下来,便进入了页面资源请求过程。这时,浏览器进程会通过进程间通信(IPC)把 URL 请求发送至网络进程,网络进程接收到 URL 请求后,会在这里发起真正的 URL 请求流程。

首先,网络进程会查找本地缓存是否缓存资源。如果存在缓存资源。

1. 浏览器缓存

首先先说一下两个概念,强缓存和协商缓存。

服务端缓存控制

强缓存与服务端的缓存控制息息相关,启用强缓存有以下几种情况。

  • 存在 Cache-Control 属性,设置 max-age 属性值并且不存在 no-cache 和 no-store 。
  • 不存在 Cache-Control 属性,存在 Expires 字段。

Cache-Control是http1.1规范的,同样表示缓存的过期时间。 其中的max-age是作为判断是否过期的主要判据,它是一个相对时间,单位为s。 如知乎上的某一张图片的response header中的字段:cache-control: public, max-age=31536000。 public代表了这张图片是可以被任何用户缓存的,包括代理服务器等; 而max-age是表示在31536000s(一年)内,如果再次请求就使用本地资源。

no-store:不允许缓存,用于某些变化非常频繁的数据,例如秒杀页面;

no-cache:可以缓存,但在使用之前必须要去服务器验证是否过期,是否存在新的版本(如果存在新版本,使用新版本,如果不存在新版本使用本地缓存);

must-revalidate:如果缓存不过期就可以继续使用,过期了如果还想用就去服务器验证;

还有两个和缓存代理相关的属性。 

private:表示缓存只能在客户端保存,是用户“私有”的,不能通过代理服务器缓存,与别人分享; public:缓存完全开放,代理服务器随便缓存,谁都可以存,谁都可以用;

客户端缓存控制

进入协商缓存之前还需要经过 DNS 解析、建立 TCP/IP 连接,如果是 https 协议还需要建立 TLS 连接。

协商缓存由客户端发起(条件请求),如果没有命中强缓存,就会进入协商缓存阶段。

If-Modified-Since/Last-Modify:浏览器第一次请求一个资源的时候,服务器返回的header中会加上Last-Modify,Last-modify是一个时间标识该资源的最后修改时间;当浏览器再次请求该资源时,request的请求头中会包含If-Modified-Since,该值为缓存之前返回的Last-Modify。服务器收到If-Modified-Since后,根据资源的最后修改时间判断是否命中缓存

if-none-match/etag

Etag:web服务器响应请求时,告诉浏览器当前资源在服务器的唯一标识(生成规则由服务器决定)。

If-None-Match:当资源过期时(使用Cache-Control标识的max-age),发现资源具有Etage声明,则再次向web服务器请求时带上头If-None-Match (Etag的值)。web服务器收到请求后发现有头If-None-Match 则与被请求资源的相应校验串进行比对,决定是否命中协商缓存;

ETag和Last-Modified的作用和用法,他们的区别:

  • 在精度上,Etag优于last-modified。 如果一个文件在1s内改变了很多次,通过etag是可以判断出来并返回最新的资源的,但是last-modifed的精度只能到s,是无法返回最新资源的,准确地说,UNIX记录只能精确到s。 
  • 在准确率上,Etag优于last-modified。有些文件可能整体copy等,只是在时间上发生了变化,而内容上并没有发生变化(etag变化,last-modified不变),如果使用last-modified,那么就会返回最新的资源,实际上这是不需要的。
  • 在性能上,last-modified优于Etag。因为last-modified只需要记录时间,而etag需要重新由服务器生成一个hash值,所以在性能上etag略差。
  • 在优先级上,Etag优于last-modified。  也就是说,etag和last-modified是可以同时使用的,但是到服务器端,会优先判断etag,如果相同,直接返回304;如果不同,就继续比较last-modified,然后再决定是否返回新的资源。

浏览器缓存过程

  1. 浏览器第一次加载资源,服务器返回200,浏览器将资源文件从服务器上请求下载下来,并把response header及该请求的返回时间一并缓存;
  2. 下一次加载资源时,先比较当前时间和上一次返回200时的时间差,如果没有超过cache-control设置的max-age,则没有过期,命中强缓存,不发请求直接从本地缓存读取该文件(如果浏览器不支持HTTP1.1,则用expires判断是否过期);如果时间过期,则向服务器发送header带有If-None-Match和If-Modified-Since的请求
  3. 服务器收到请求后,优先根据Etag的值判断被请求的文件有没有做修改,Etag值一致则没有修改,命中协商缓存,返回304;如果不一致则有改动,直接返回新的资源文件带上新的Etag值并返回200;;
  4. 如果服务器收到的请求没有Etag值,则将If-Modified-Since和被请求文件的最后修改时间做比对,一致则命中协商缓存,返回304;不一致则返回新的last-modified和文件并返回200;

如果资源没有变化,服务器就会回应 “304 Not Modified”,表示缓存依然有效。

请求过程中还可能发生另外一种情况,那就是服务器返回状态码 301、302。

2. DNS 解析

上面说到浏览器缓存,首先,网络进程会查找本地缓存是否缓存资源。 如果存在缓存资源,就直接返回资源给浏览器进程。如果不存在缓存,这时就要判断是否需要 DNS 解析。

首先判断 url 是否是一个域名,如果是一个域名,判断是否存在DNS缓存(主要是把本地IP和域名管理起来),如果没有缓存走 DNS 解析流程,获取服务器的 IP 地址。

DNS 系统(字典树算法

DNS 的核心系统是一个三层的树状、分布式服务,基本对应域名的结构。

  • 根域名服务器(Root DNS Server):管理顶级域名服务器,返回“com”“net”“cn”等顶级域名服务器的 IP 地址;
  • 顶级域名服务器(Top-level DNS Server):管理各自域名下的权威域名服务器,比如 com 顶级域名服务器可以返回 apple.com 域名服务器的 IP 地址;
  • 权威域名服务器(Authoritative DNS Server):管理自己域名下主机的 IP 地址,比如 apple.com 权威域名服务器可以返回 www.apple.com 的 IP 地址;

dns.png

有了这个系统以后,任何一个域名都可以在这个树形结构里从顶至下进行查询,就好像是把域名从右到左顺序走了一遍,最终就获得了域名对应的 IP 地址。

在核心系统之外,还有两种缓存手段来减轻域名解析的压力,可以更快地获取结果。

  • 许多大公司、网络运营商都会建立自己的 DNS 服务器,代替用户访问核心 DNS 系统。这些“野生服务器”都被称为“非权威域名服务器”。
  • 操作系统也会对 DNS 解析结果缓存。操作系统里还有一个特殊的“主机映射文件”,即 Linux 中的“/etc/hosts”,Windows 中的“C:\WINDOWS\system32\drivers\etc\hosts”,如果操作系统在缓存里找不到 DNS 记录,就会找这个文件。

另外浏览器也会对 DNS 缓存的结果进行缓存,缓存策略和浏览器相关。

DNS 解析流程

有了上述理论知识,就可以很轻松掌握 DNS 的解析流程。

浏览器缓存 -> 操作系统缓存 -> 本地 hosts 文件 -> 非权威域名服务器 -> 根域名服务器 -> 顶级域名服务器 -> 权威域名服务器。

win10 启动加载时,会把 hosts 中的条目缓存在操作系统中。 win10 还会监听 hosts 文件的变化,并动态更新操作系统缓存。

3. 建立 TCP/IP 连接

获取到服务器的 IP 地址后,就可以建立 TCP/IP 连接了。

网络分层模型

讲述建立 TCP/IP 连接之前,我们先来了解一下 TCP/IP 网络分层模型。tcp_ip.png

TCP/IP 协议总共有四层,它的层次顺序是“从下往上”数的,第一层是最下面的一层。

第一层叫“链接层”(link layer),负责在以太网、WiFi 这样的的底层数据上发送原始数据包,工作在网卡这个层次,使用 MAC 地址来标记网络上的设备,所以有时候也叫 MAC 层。

第二层叫“网际层”或者“网络互连层”(internet layer),IP 协议就处在这一层。

第三层叫“传输层”(transport layer),这个层次协议的职责是保证数据在 IP 地址标记的两点之间“可靠”地传输。TCP 协议就位于这一层,另外还有 UDP 也位于这一层。

第四层叫“应用层”(application layer)。这一层有各种面向具体应用的协议。例如 Telnet、SSH、FTP、SMTP 等。当然还有最常见的 HTTP 协议。

三次握手

三次握手(Three-way Handshake)其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备
正是由于HTTP的三次握手的开销比较大所以才会有不断的将TCP连接可以进行1次到6次到多次HTTP请求

实现:

  1. 第一次:客户端发送一条给服务端带有标志字段的创建请求的数据包 (客户端的发送能力、服务端的接收能力是正常的。
  2. 第二次:服务器接送到数据后就会开启一个TCP的端口根据客服端的标志字段返回一条(也带标志位)数据包给客户端(服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常
  3. 第三次:客户端收到服务器的返回后在重新发送一条数据给服务器(确认客户端的接收能力是否正常

个人理解:

  • A给B说:'我爱你';B收到给A说:'我也喜欢你';A看到信息后在对B说:'那我们在一起吧',所以确定了关系

作用:

防止服务器开启无用的TCP连接,我们可以先通过第一次和第二次先测试一下是不是可以跑的通,那么服务端这边就会开启TCP连接

  • 比如客户端有一个返回时间的限制,那么在第二次握手的时候我们就可以测出来,是不可以的,就可以减少TCP资源的浪费

在 HTTP 协议里,建立 TCP/IP 连接后,浏览器会立即发送请求报文。

4. 建立 TLS 连接

建立 TCP/IP 连接后,如果发现请求协议是 HTTPS,还需要建立 TLS 连接。 这个“握手过程”与 TCP 类似,是 HTTPS 和 TLS 协议里最重要、最核心的部分。

HTTPS

众所周知,HTTP 是不安全的。HTTPS 在其基础上增加了 机密性、完整性、身份认证和不可否认四大安全特性。 它把 HTTP 下层的传输协议由 TCP/IP 换成 SSL/TLS,由 “HTTP over TCP/IP”变成 “HTTP over SSL/TLS”。

通过学习我认为我们要搞懂HTTPS,其实就是把HTTP的一些不好的点进行优化

  1. 使用明文(非加密的)数据传输 ----> 加密
  2. 不验证通行的身份 -------------------> 根据token(某一个标记)来确定身份
  3. 无法验证报文的完整性 --------------> 根据全部数据解析算法会生成一个哈希表(散列图)来判断是否相同

https = http + 加密 + 认证 + 完整性验证

那么 HTTPS 是如何做到这些的?

对称加密、非对称加密

“对称加密”很好理解,就是指加密和解密使用的密钥都是同一个,是“对称”的。

对称加密看上去实现了机密性,但是无法保证将密钥安全地传递给对方,即无法安全地实现“密钥交换”。 因为对称加密算法中只有持有密钥就可以解密。如果你和网站约定的密钥被黑客窃取,那通信就没有机密性可言了。

所以,后来就出现了非对称加密算法(也叫公钥加密算法)。 它有两个密钥,一个叫“公钥”,一个叫“私钥”。公钥可以公开给任何人使用,私钥必须严格保密。 两个密钥是不同的,公钥加密只能使用私钥解密,反过来,私钥加密也只能使用公钥解密。

非对称加密可以解决“密钥交换的问题”。网站保管私钥,在网上任意分发公钥,想要登录网站只要用公钥加密就行,密文只能有私钥持有者才能解密。黑客无法得到私钥,所以也就无法破解密文。

混合加密

TLS 目前使用的是混合加密。

因为非对称加密虽然没有“密钥交换”的问题,但都是基于复杂的数学难题,运算速度很慢。 如果仅用非对称加密,虽然可以保证安全,但通信速度得不到保证,实用价值为零。 混合加密把对称加密和非对称加密结合起来,两者互相取长补短,既能高效解密,又能安全的进行“密钥交换”。

即在通信刚开始的使用非对称算法,比如 RSA、DCDHE,首先解决密钥交换的问题。 然后使用随机数产生对称算法使用的“会话密钥”(session key),再用公钥加密。 因为会话密钥很短,通常只有 16 字节或 32 字节,所以慢一点也无所谓。 对方拿到密文后用子要解密,取出会话密钥。 这样,双方就实现了对称密钥的安全交换,后续也就不再使用非对称加密,全部使用对称加密进行通信。

混合加密解决的是数据通信中的机密性,但是无法解决完整性、身份认证和不可否认等特性。

数字证书和 CA

到现在,综合使用对称加密、非对称加密和摘要算法实现了安全的四大特性,是不是已经很完美了? 其实还存在一个问题,那就是“公钥信任”问题。如果谁都可以发布公钥,还是无法判断这个公钥就是你自己的。

这里就要说到第三方 CA(Certificate Authority,证书认证机构)。 CA 对公钥的签名是有格式的,包含序列号、用途、颁发者、有效时间等,把这些打成一个包在签名,完整地证明公钥关联的各种信息,形成“数字证书”(Certificate)。

证书分 DV、OV、EV 三种,区别在于可信程度。 DV 是最低的,只是域名级别的可信,背后是谁不知道。EV 是最高的,经过了法律和审计的严格核查,可以证明网站拥有者的身份(在浏览器地址栏会显示出公司的名字,例如 Apple、GitHub 的网站)。

不过你可能要问 CA 如何证明自己?

这还是信任链的问题。小一点的 CA 可以让大 CA 签名认证,但链条的最后,也就是 Root CA,就只能自己证明自己了,这个就叫“自签名证书”(Self-Signed Certificate)或者“根证书”(Root Certificate)。你必须相信,否则整个证书信任链就走不下去了。

有了这个证书体系,操作系统和浏览器都内置了各大 CA 的根证书,上网的时候只要服务器发过来它的证书,就可以验证证书里的签名,顺着证书链(Certificate Chain)一层层地验证,直到找到根证书,就能够确定证书是可信的,从而里面的公钥也是可信的。

证书信任链的验证过程如下:

服务器返回的是证书链(不包括根证书,根证书预置在浏览器中),浏览器会根据证书链包含的签发者信息,逐层向上查找知道找到根证书。然后浏览器就可以使用信任的根证书(根公钥)解析证书链的根证书得到一级证书的公钥 + 摘要验签,然后拿一级证书的公钥解密一级证书拿到二级证书的公钥和摘要验签,再然后拿二级证书的公钥解密二级证书得到服务器的公钥和摘要验签,验证过程就结束了。

SSL协议过程

相对于TCP或HTTP协议,SSL协议要复杂很多。由于它也是建立在TCP协议之上的,所以在使用SSL传输数据之前需要先进行三次握手和服务器建立连接,具体的流程如图所示:

实现 一共9步具体图片和解析在上面学习网站中,我会写出自己的理解

  1. Client: 产生随机数,和把支持的加密,压缩,SSL版本号和'hello'发给Server
  2. Server: 也产生随机数,把确定的加密,压缩,SSL版本号和'hello'发给client
  3. Server:发公钥给client
  4. server: 发送结束初次协议结束
  5. client:此报文中包含通信加密过程中使用的一种被称为Pre-master secret的随机密码串,并使用第三步接收到的公钥证书进行了加密(服务器可以通过加密的数据,通过私钥去推出:master secret)
  6. client:该报文告知服务端,此步骤之后的所有数据将使用第五步中生成的master secret进行加密
  7. client:整体全部数据的验证值(哈希表),用来验证项目的完整性
  8. server:回应收到5,6,7步的数据
  9. server: 解析第7步骤的数据验证值的哈希表 到这里SSL握手的过程结束,随后使用HTTP传输带有master secret传输数据

Https真的安全么

使用中间人攻击用截取数据的方法在加上自己注册的公钥证书,可以推出Pre-master secret,随后抓包工具使用从服务端接收到的公钥证书中的公钥对Pre-master secret进行加密,然后发送给服务端。随后的通信过程虽然进行了加密,但抓包工具已经生成了密钥(master secret),所以可以查看Https的通信内容

image

四次挥手

  1. cilent发出并停止再发送数据,主动关闭TCP连接,等待服务端的确定
  2. server:即服务端收到连接释放报文段后即发出确认报文段;client接到第2步的数据后会进入等待阶段,等待服务器的最终释放(由于cilent虽然结束了,但是服务器这边可能还是由一些数据没传输完全)
  3. 当server确定可以断开会发送一个报文,指定一个序列号,服务端进入LAST_ACK(最后确认)状态,等待客户端的确认。
  4. 客户端收到服务端的连接释放报文段后,对此发出确认报文段,客户端进入状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,客户端才进入CLOSED状态。
    • 1MSL = 最长报文段寿命,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。
    • 为什么是2MSL,由于如果第四步客户端收到服务端的连接释放报文段后,服务端没收到,服务端会重新执行第3步那么客户端会重新发送第四步同时等待时间也会重新为2MSL,主要可以确保关闭

作用:

确保可以可以在cilent需要关闭的时候server可以在运行完后在关闭(当然为了确保真的关闭TCP连接,会再次向客户端确认)

5. HTTP 代理

建立 TCP/IP 连接或 TLS 连接后,就可以发起 HTTP 数据请求了。 不过数据请求的过程中还可能存在一个或者多个中间人,有可能请求并不会直接到达源服务器。

代理服务器处理 HTTP 通信过程中的中间位置,对上屏蔽了真实客户端,对下屏蔽了真实服务器。 在这个中间层可以做很多的事情,为 HTTP 协议增加了更多的灵活性。

代理的一个常见的功能就是负载均衡,可以掌握请求分发的“大权”,决定由后面的哪台服务器响应请求。

代理中存在一些常用的负载均衡算法,比如轮询、一致性哈希等,这些算法的目的就是尽量把外部流量合理地分散到多台源服务器,提高系统的整体资源利用率和性能。

负载均衡算法

随机:简单、是否均匀看随机情况。 轮询(一般轮询、加权轮询):相对简单,会考虑机器资源和性能的均衡性。 哈希(一般哈希、一致性哈希、带虚拟节点的一致性哈希):相对复杂,越公平就会越复杂,适当考虑了请求。

我们使用的 CDN 就是一种代理,它可以代替源站服务器响应客户端的请求,通常扮演透明代理和反向代理的角色。(防止主要的服务器崩溃)

一般用来存放常用静态资源(有利于长时间的缓存)如果 CDN 的调度算法还优秀,还可以找到距离用户最近的节点,大幅度缩短响应时间。

CDN 也是现在互联网中的一项重要基础设施,除了基本的网络加速外,还提供负载均衡、安全防护、边缘计算、跨运营商网络等功能,能够成倍地“放大”源站服务器的服务能力,很多云服务商都把 CDN 作为产品的一部分。

代理相关的知识还有很多,比如缓存代理、负载均衡等,这里就不展开阐述了。

三、处理响应数据

重定向

重定向是由服务器发起的,浏览器使用者无法控制,浏览器收到 301、302 这两个状态码就会跳转到新的 URI。

其实除了状态码之外,要想实现重定向还需要 “Location”字段的配合。

Connection: keep-alive
Content-Length: 151
Content-Type: text/html
Date: Sun, 21 Feb 2021 00:48:52 GMT
Location: /index.html
Referer: /18-1
Server: openresty/1.19.3.1
复制代码

“Location”字段属于响应字段,必须出现在响应报文里。但只有配合 301/302 状态码才有意义,它标记了服务器要求重定向的 URI,这里就是要求浏览器跳转到“index.html”。

浏览器收到 301/302 报文,会检查响应头里有没有“Location”。如果有,就从字段值里提取出 URI,然后再发起新的 HTTP 或者 HTTPS 请求,一切又重头开始了。

“Location”里的 URI 既可以使用绝对 URI,也可以使用相对 URI。 如果重定向只在站内跳转,可以使用相对 URI。如果跳转到站外,必须使用绝对 URI。 重定向报文里还可以用 Refresh 字段,实现延时重定向,例如“Refresh:5; url=xxx” 告诉浏览器 5 秒后再跳转。

响应数据类型处理

HTTP 请求的数据类型,可能是一个下载类型,也可能是 HTML 页面,浏览器是如何区分它们的?

答案是 Content-Type。Content-Type 是 HTTP 头中一个非常重要的字段, 它告诉浏览器服务器返回的响应体数据是什么类型,然后浏览器会根据 Content-Type 的值来决定如何显示响应体的内容。

需要注意的是,如果服务器配置 Content-Type 不正确,比如将 text/html 类型配置成 application/octet-stream 类型,那么浏览器可能会曲解文件内容,比如会将一个本来是用来展示的页面,变成了一个下载文件。

所以,不同 Content-Type 的后续处理流程也截然不同。

如果 Content-Type 字段的值被浏览器判断为下载类型,那么该请求会被提交给浏览器的下载管理器,同时该 URL 请求的导航流程就此结束。 但如果是 HTML,那么浏览器则会继续进行导航流程。由于 Chrome 的页面渲染是运行在渲染进程中的,所以接下来就需要准备渲染进程了。

四、准备渲染进程

默认情况下,Chrome 会为每个页面分配一个渲染进程,也就是说,每打开一个新页面就会配套创建一个新的渲染进程。但是,也有一些例外,在某些情况下,浏览器会让多个页面直接运行在同一个渲染进程中。

那什么情况下多个页面会同时运行在一个渲染进程中呢?

要解决这个问题,我们就需要先了解下什么是同一站点(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。

总结来说,打开一个新页面采用的渲染进程策略就是:

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

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

五、提交文档

所谓提交文档,就是指浏览器进程将网络进程接收到的 HTML 数据提交给渲染进程,具体流程是这样的:

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

到这里,一个完整的导航流程就“走”完了,这之后就要进入渲染阶段了。

六、渲染阶段

当渲染进程接收 HTML 文件字节流时,会先开启一个预解析线程,如果遇到 JavaScript 文件或者 CSS 文件,那么预解析线程会提前下载这些数据

那么为什么CSS一般放开头,js放末尾?

1.CSS文件下载下来的时间和HTML编译解析是多线程,可以缓解一些生成CSSDM的时间,让CSS放前面(这里的CSS指:链接的文件)

2.就是因为可以更快的让页面先出来效果,而不必先出现一些交互功能,让JS放到代码后面

由于渲染机制过于复杂,所以渲染模块在执行过程中会被划分为很多子阶段,输入的 HTML 经过这些子阶段,最后输出像素。我们把这样的一个处理流程叫做渲染流水线,其大致流程如下图所示:

render_line.png

按照渲染的时间顺序,流水线可分为如下几个子阶段: 构建 DOM 树、样式计算、布局阶段、分层、绘制、分块、光栅化和合成。

1. 构建 DOM 树

浏览器无法直接理解和使用 HTML,所以需要将 HTML 转换为浏览器能够理解的结构——DOM 树。

dom_tree.png

2. 样式计算

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

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

CSS 样式来源主要有三种:

  • 通过 link 引用的外部 CSS 文件
  • 通过 link 引用的外部 CSS 文件
  • 元素的 style 属性内嵌的 CSS

和 HTML 文件一样,浏览器也是无法直接理解这些纯文本的 CSS 样式,所以当渲染引擎接收到 CSS 文本时,会执行一个转换操作,将 CSS 文本转换为浏览器可以理解的结构——styleSheets。

你可以在 Chrome 控制台中输入 document.styleSheets 查看其结构。

转换样式表中的属性值,使其标准化

现在我们已经把现有的 CSS 文本转化为浏览器可以理解的结构了,那么接下来就要对其进行属性值的标准化操作。

css02.png

可以看到上图左侧的 CSS 文本中有很多属性值,如 2em、blue、bold,这些类型数值不容易被渲染引擎理解,所以需要将所有值转换为渲染引擎容易理解的、标准化的计算值,这个过程就是属性值标准化。

计算出 DOM 树中每个节点的具体样式

现在样式的属性已被标准化了,接下来就需要计算 DOM 树中每个节点的样式属性了,如何计算呢?

这就涉及到 CSS 的继承规则和层叠规则了。

首先是 CSS 继承。CSS 继承就是每个 DOM 节点都包含有父节点的样式。这么说可能有点抽象,我们可以结合具体例子,看下面这样一张样式表是如何应用到 DOM 节点上的。

body { font-size: 20px }
p {color:blue;}
span  {display: none}
div {font-weight: bold;color:red}
div  p {color:green;}
复制代码

这张样式表最终应用到 DOM 节点的效果如下图所示:

css03.png

从图中可以看出,所有子节点都继承了父节点样式。比如 body 节点的 font-size 属性是 20,那 body 节点下面的所有节点的 font-size 都等于 20。

3. 布局阶段

现在,我们有 DOM 树和 DOM 树中元素的样式,但这还不足以显示页面,因为我们还不知道 DOM 元素的几何位置信息。那么接下来就需要计算出 DOM 树中可见元素的几何位置,我们把这个计算过程叫做布局。

Chrome 在布局阶段需要完成两个任务:创建布局树和布局计算。

创建布局树

你可能注意到了 DOM 树还含有很多不可见的元素,比如 head 标签,还有使用了 display:none 属性的元素。所以在显示之前,我们还要额外地构建一棵只包含可见元素布局树。

我们结合下图来看看布局树的构造过程:

layout.png

从上图可以看出,DOM 树中所有不可见的节点都没有包含到布局树中。

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

  • 遍历 DOM 树中的所有可见节点,并把这些节点加到布局树中;
  • 而不可见的节点会被布局树忽略掉,如 head 标签下面的全部内容,再比如 body.p.span 这个元素,因为它的属性包含 dispaly:none,所以这个元素也没有被包进布局树;

布局计算

现在我们有了一棵完整的布局树。那么接下来,就要计算布局树节点的坐标位置了。 布局的计算过程非常复杂,我们这里先跳过不讲,等到后面再做详细的介绍。

在执行布局操作的时候,会把布局运算的结果重新写回布局树中,所以布局树既是输入内容也是输出内容,这是布局阶段一个不合理的地方,因为在布局阶段并没有清晰地将输入内容和输出内容区分开来。针对这个问题,Chrome 团队正在重构布局代码,下一代布局系统叫 LayoutNG,试图更清晰地分离输入和输出,从而让新设计的布局算法更加简单。

4. 分层

现在我们有了布局树,而且每个元素的具体位置信息都计算出来了,那么接下来是不是就要开始着手绘制页面了?

答案依然是否定的。

因为页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-indexing 做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层(LayerTree)。 如果你熟悉 PS,相信你会很容易理解图层的概念,正是这些图层叠加在一起构成了最终的页面图像。

要想直观地理解什么是图层,你可以打开 Chrome 的“开发者工具”,选择“Layers”标签,就可以可视化页面的分层情况,如下图所示:

layer.png

从上图可以看出,渲染引擎给页面分了很多图层,这些图层按照一定顺序叠加在一起,就形成了最终的页面,你可以参考下图:layer02.png

 现在你知道了浏览器的页面实际上被分成了很多图层,这些图层叠加后合成了最终的页面。下面我们再来看看这些图层和布局树节点之间的关系,如文中图所示:tree.png

通常情况下,并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。如上图中的 span 标签没有专属图层,那么它们就从属于它们的父节点图层。但不管怎样,最终每一个节点都会直接或者间接地从属于一个层。

那么需要满足什么条件,渲染引擎才会为特定的节点创建新的图层呢?通常满足下面两点中任意一点的元素就可以被提升为单独的一个图层。

第一点,拥有层叠上下文属性的元素会被提升为单独的一层。

页面是个二维平面,但是层叠上下文能够让 HTML 元素具有三维概念,这些 HTML 元素按照自身属性的优先级分布在垂直于这个二维平面的 z 轴上。你可以结合下图来直观感受下:

context.png

从图中可以看出,明确定位属性的元素、定义透明属性的元素、使用 CSS 滤镜的元素等,都拥有层叠上下文属性。

若你想要了解更多层叠上下文的知识,你可以参考 MDN 这篇文章

第二点,需要剪裁(clip)的地方也会被创建为图层。

不过首先你需要了解什么是剪裁,结合下面的 HTML 代码:

<style>
      div {
            width: 200;
            height: 200;
            overflow:auto;
            background: gray;
        } 
</style>
<body>
    <div >
        <p>所以元素有了层叠上下文的属性或者需要被剪裁,那么就会被提升成为单独一层,你可以参看下图:</p>
        <p>从上图我们可以看到,document层上有A和B层,而B层之上又有两个图层。这些图层组织在一起也是一颗树状结构。</p>
        <p>图层树是基于布局树来创建的,为了找出哪些元素需要在哪些层中,渲染引擎会遍历布局树来创建层树(Update LayerTree)。</p> 
    </div>
</body>
复制代码

在这里我们把 div 的大小限定为 200 * 200 像素,而 div 里面的文字内容比较多,文字所显示的区域肯定会超出 200 * 200 的面积,这时候就产生了剪裁,渲染引擎会把裁剪文字内容的一部分用于显示在 div 区域,下图是运行时的执行结果:

clip.png

出现这种裁剪情况的时候,渲染引擎会为文字部分单独创建一个层,如果出现滚动条,滚动条也会被提升为单独的层。你可以参考下图:

clip02.png

所以说,元素有了层叠上下文的属性或者需要被剪裁,满足其中任意一点,就会被提升成为单独一层。

5. 图层绘制

在完成图层树的构建之后,渲染引擎会对图层树中的每个图层进行绘制,那么接下来我们看看渲染引擎是怎么实现图层绘制的?

试想一下,如果给你一张纸,让你先把纸的背景涂成蓝色,然后在中间位置画一个红色的圆,最后再在圆上画个绿色三角形。你会怎么操作呢?

通常,你会把你的绘制操作分解为三步:

  1. 绘制蓝色背景;
  2. 在中间绘制一个红色的圆;
  3. 再在圆上绘制绿色三角形;

渲染引擎实现图层的绘制与之类似,会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表,如下图所示:

render_list.png

从图中可以看出,绘制列表中的指令其实非常简单,就是让其执行一个简单的绘制操作,比如绘制粉色矩形或者黑色的线等。而绘制一个元素通常需要好几条绘制指令,因为每个元素的背景、前景、边框都需要单独的指令去绘制。所以在图层绘制阶段,输出的内容就是这些待绘制列表。

你也可以打开“开发者工具”的“Layers”标签,选择“document”层,来实际体验下绘制列表,如下图所示:paint.gifpaint.gif

在该图中,区域 1 就是 document 的绘制列表,拖动区域 2 中的进度条可以重现列表的绘制过程。

6. 栅格化(raster)操作

绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。你可以结合下图来看下渲染主线程和合成线程之间的关系:

paint.png

如上图所示,当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程,那么接下来合成线程是怎么工作的呢?

通常一个页面可能很大,但是用户只能看到其中的一部分,我们把用户可以看到的这个部分叫做视口(viewport)。

在有些情况下,有的图层可以很大,比如有的页面你使用滚动条要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。

基于这个原因,合成线程会将图层划分为图块(tile),这些图块的大小通常是 256x256 或者 512x512。

然后合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。而图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的,运行方式

如下图所示:

paint02.png

通常,栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。

相信你还记得,GPU 操作是运行在 GPU 进程中,如果栅格化操作使用了 GPU,那么最终生成位图的操作是在 GPU 中完成的,这就涉及到了跨进程操作。具体形式你可以参考下图:

paint03.png

从图中可以看出,渲染进程把生成图块的指令发送给 GPU,然后在 GPU 中执行生成图块的位图,并保存在 GPU 的内存中。

7. 合成与显示

一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。

到这里,经过这一系列的阶段,编写好的 HTML、CSS、JavaScript 等文件,经过浏览器就会显示出漂亮的页面了。

那文章开头的“从输入 URL 到页面展示,这中间发生了什么?”这个过程及其“串联”的问题也就解决了。

8. 总结

一个完整的渲染流程大致可总结为如下。

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

总结

看完这篇文章,如果再有面试官问你“从输入 URL 到页面展示,这中间发生了什么?”这个问题,你知道怎么回答了吗?你也可以用自己的语言组织下,为你自己的面试做准备。

以上的内容其实还是不够完整。比如面试官还可能会问起跨域、同源策略、HTTP 各版本差异、响应状态码、大文件传输、Cookie、http 优化、OSI 网络分层模型等问题,但是无外乎也是这个流程的一部分,所以也不必过于恐慌,遇到再完善补充就是了。

参考资料

  • 《浏览器工作原理与实践》- 李兵。
  • 《透视 HTTP 协议》- 罗剑峰。
  • 《深入理解 TCP 协议:从原理到实战》- 挖坑的张师傅。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值