侧边栏壁纸
  • 累计撰写 218 篇文章
  • 累计创建 59 个标签
  • 累计收到 5 条评论

浏览器从输入到渲染发生了什么?

barwe
2022-03-27 / 0 评论 / 0 点赞 / 1,064 阅读 / 5,993 字
温馨提示:
本文最后更新于 2022-04-03,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

合成 URL

我在浏览器中输入了一个东西,有两种情况:

  1. 我想要用搜索引擎搜索些什么,我输入的是关键词
  2. 我知道我想要访问那个网站,我输入的是网站的具体网址

浏览器会判断我们的输入属于哪一种方式,最终合成一个可访问的完备的 URL 地址。

关键词

将浏览器的默认搜索引擎与输入的关键词拼接起来,并对其中非标准的字符进行 百分号编码

例如用百度搜索“快乐”得到的 URL 是:

https://www.baidu.com/baidu?tn=monline_4_dg&ie=utf-8&wd=%E5%BF%AB%E4%B9%90

这是一个完备的 URL 地址,有协议(https)、域名(www.baidu.com)以及查询参数。

查询参数中的非标准 URL 字符串已经被编码成 %XX 形式,X 代表一个十六进制数。

具体网址

如果我输入的内容符合 URL 的规则(看起来就像 URL,至少得有个域名),浏览器会自动补全剩余的部分并标准化 URL 字符串,例如加上协议头和非标准的 URL 字符,例如输入:

souyisou.com?q=快乐

最终合成的标准 URL 就是:

https://souyisou.com?q=%E5%BF%AB%E4%B9%90

百分号编码

URL 的标准字符集只是 ASCII 字符集的一个子集,其余字符都需要进行百分号编码。

这种转码一般发生在查询参数中,当查询参数的值中出现了像 ?=& 等在 URL 中有特殊含义的字符时,它们都需要被转码。

转码的规则来自 UTF-8 编码,由于每个汉字的 UTF-8 编码通常都是三字节,因此一个汉字转码后通常对应三个百分号编码。

JavaScript 提供了两个 API 做这个事情:

  • encodeURI 编码整个 URL
  • encodeURIComponent 编码参数部分,要求更加严格

检查缓存

浏览器合成一个有效的 URL 后是不是就开始向外发送请求了呢?并没有,在发送请求前还需要检查一下自己有没有缓存数据,如果一个万年不变的网站,每次访问都要重新请求那也太浪费流量和时间了。

浏览器会在发请求前检查缓存资源,首先看有没有,然后看过没过期。

在有效期内的缓存可以直接使用,这时浏览器就不会发请求了,而是直接从缓存中拿东西,这叫做 强缓存。不同的浏览器显示的内容可能不太一样,例如 Chrome 使用强缓存时状态码仍然是 200,但是 size 会变成 memory cache 或者 disk cache,分别表示从内存或者磁盘中拿出缓存。资源被缓存在内存还是磁盘中,是由浏览器自己分配的,比如内存不够了,就只能放到磁盘里了咯。数据可能在内存和磁盘里都有缓存,一般来说,不关闭标签页直接刷新,从内存中取;如果关闭了标签页,内存被释放掉了,再重新打开,就只能从磁盘里读了。

过期了的缓存,浏览器不能直接使用,需要先咨询一下服务器我们能不能使用过期了的缓存资源,结果无外乎两种:

  • 可以使用过期缓存,响应状态码为 304,服务器不会发送额外数据
  • 不能使用过期缓存,响应状态码为 200,服务器会附带新的数据,此时与正常请求-响应一致

DNS域名解析

如果本地没有缓存或者缓存过期了,就需要向服务器请求新的数据,或者询问服务器能否继续使用过期的缓存。在发起请求时,首先要做的就是定位服务器在互联网上的位置,即 DNS 解析。

人只能记住域名,记不住那么一长串的 IP 地址(除了 127.0.0.1),然而计算机只认识数字(还是二进制的),不认识 abcd。域名系统(Domain Name System, DNS)就负责将 域名 转化为 IP地址 供计算机使用。

域名查找大致可以分为两步,先查找本地缓存,找到了就 OK 了。如果没找到,就向外部 DNS 服务器发出查询请求。

本地缓存

本地缓存也分了很多层,按照离浏览器的距离可以分为:

  1. 浏览器的缓存:最优先查找
  2. 本地 hosts 文件
  3. 本地 DNS 解析器的缓存
  4. 本地 DNS 服务器

从上到下,由近到远依次查找,一旦找到,就返回结果,这是一个 递归 过程。

当然实际场景中的缓存层级可能更多,例如:

  • 路由器的缓存
  • 互联网服务提供商(Internet Service Provider, IPS)的缓存

迭代查询

如果以上缓存中均找到相关域名,那就只好走标准流程了,比如下面的 URL:

https://mail.163.com?user=barwe

首先本地 DNS 服务器请求 根域名服务器,根域名服务器看到 .com 结尾告诉我应该去找 .com 顶级域名服务器,然后本地 DNS 服务器又去请求 .com 顶级域名服务器,.com 顶级域名服务器看到 163 告诉我应该去找 163 权威域名服务器,然后我又去找 163 权威域名服务器,163 权威域名服务器接收到了我的请求,看到了 mail,于是它明白了应该给我提供邮件服务。

图片来源: https://juejin.cn/post/6871947938701475847

这个查询过程是一个 迭代 过程:本地 DNS 服务器依次请求根域名服务器、顶级域名服务器和权威域名服务器等,最终确定提供该服务的服务器位置。


建立 TCP 连接

当前架构下,WEB 应用的协议头只有两种:

  • http: 普通的协议,需要 TCP 三次握手
  • https: HTTP + SSL\TLS,需要 TCP 三次握手和 SSL 握手

HTTP 是明文传输,HTTPS 在 HTTP 的基础上进行了数据加密,传输的是加密后的数据。

传输层与应用层

TCP/IP 是 传输层协议/网际协议,HTTP 是 应用层协议

IP 就像连接每户人家的公路,它标识除了每户人家在地理上的位置。

TCP 和 UDP 就像跑在公路上的卡车,负责运输货物(数据),但是如果我们将东西一股脑的塞进卡车里,而不对货物进行标记,这些货物就是无意义的,因为我不知道该将什么东西送到谁手里。

所以 TCP/IP 解决的是数据怎么在网络中传输,但是不能识别数据内容。

HTTP 属于应用层协议,它解决的就是 TCP/IP 传输的数据的识别问题,比如 HTTP/HTTPS 负责将数据分发给 WEB 服务器,FTP 表示将数据分发给文件传输服务器,SMTP 表示将数据分发给邮件服务器。

如果说卡车运输的是快递,应用层的协议就相当于快递上的标签,标识了哪些快递送到哪里。

详细信息参考 OSI 分层模型。

TCP 三次握手

不管是 HTTP 还是 HTTPS 都需要先建立客户端和服务器之间连接,建立连接需要三次握手:

  1. 客户端告诉服务器,我想与你建立连接,自己进行连接请求已发送状态(SYN_SEND):SYN = 1, ACK = 0, seq = x
  2. 服务器收到客户端的连接请求,分配连接资源,并告诉客户端我已转备好与你建立连接,服务器进入已接受连接请求状态(SYN_RECV):SYN = 1, ACK = 1, ack = x + 1, seq = y
  3. 客户端收到服务器的返回消息,得知服务器已经准备好连接之后,自己进入连接状态(ESTABLISHED),并再次告诉服务器我已转备好连接了:SYN = 0, ACK = 1, ack = y + 1, seq = x + 1

客户端开始愉快的请求了~

上面涉及到的 TCP 数据报头的标志位主要有:

  • SYN:1 表示该数据包是一个 连接请求 或者 响应连接请求 的数据包,所以只在前两次握手中值为 1
  • ACK:1 表示需要确认数据包的序号(ack 确认序列),所以只在后两次握手中值为 1
  • seq & ack:第一次握手时客户端随机生成一个 seq 序号,它表示该数据包的起始编号,客户端将 seq = x 的连接请求数据报发送非服务器,服务器返回的响应数据报必须要设置确认序号 ack = x + 1 才能被客户端正确接受,同时服务器也会生成自己第一个数据包的序号 seq = y。客户端收到响应数据报后再次发送的数据包需要设置确认序号 ack = y + 1 才能被服务器正确接受,同时设置自己的数据包序号为 seq = x + 1

说的再简单点:

  1. 客户端发送 建立连接 的请求
  2. 服务器返回对建立连接请求的确认,并未该连接分配资源
  3. 客户端发送对服务器确认消息的确认,并未该连接分配资源

☺ 为什么需要三次握手?

如果客户端发送了一个失效的建立连接的请求,服务器收到请求后分配资源,然后将确认消息返回给客户端,如果此时客户端已经关闭了,服务器就会一直等待客户端的请求,所以需要客户端再次发送一个关于确认的确认消息,如果服务器收到了这个客户端的确认,那么连接就成功了,如果没有收到,就说明客户端已经放弃了连接,服务器就可以释放自己的资源。

SSL 握手

SSL/TLS 是一个安全通信框架,上面可以承载 HTTP 协议或者 SMPT/POP3 协议等。

TLS (Transport Layer Security) 是在 SSL 3.0 (Secure Socket Layer) 的基础上设计的改进版本。

TLS 握手设计到三个步骤:

  1. 非对称加密:身份认证和秘钥协商
  2. 对称加密:用协商的秘钥对数据进行加密
  3. 散列算法:验证信息的完整性

非对称加密的特点是秘钥成对出现,一般称为 公钥私钥,公钥加密的信息只能通过私钥解密,私钥加密的信息只能通过公钥解密,例如 RSA 算法。

TLS 握手的第一步,就是客户端和服务器通过非对称加密协商出一个 对称加密的秘钥。因为非对称加密成本高、耗时长,因此在正式的数据传输阶段都使用对称加密,这个秘钥只有参与协商的客户端和服务器知道。

握手过程分为五步,可参考阮一峰图解SSL/TLS协议

  1. 客户端发送一个自己生成的随机数(client random)给服务器
  2. 服务器收到 client random,将自己的 公钥(证书)和另一个随机数(server random)返回给客户端
  3. 客户端收到服务器的证书(公钥),然后又生成一个新的随机数(premaster secret),然后使用服务器发过来的公钥对这个随机数进行加密,然后将加密后的结果发送给服务器
  4. 服务器接收到客户端发来的经过自己公钥加密后的随机数,使用自己的私钥进行解密
  5. 客户端和服务器根据上面四步传输的三个随机数生成 对称秘钥(session key),用来加密接下来的整个对话过程

在上面的过程中,client random 和 server random 都是明文传输的,它们都可以被第三方接收到。

而 premaster secret 是通过服务器的公钥吉加密过的,第三方虽然可以截获服务器的公钥,但是却不知道这个加密结果对应的原文是什么(唯一获取的原文的方式可能就是暴力破解了)。因此这个 premaster secret 理论上来说只有参与协商的客户端和服务器知道,所以这第三个随机数能否被破译是整个 TLS 安全机制的关键点。

通过这种非对称加密的方式(服务器将公钥提供给客户端进行加密,然后自己对加密结果进行解密),协商的客户端和服务器能够获得只有它两知道的 对称秘钥,利用对称秘钥解码接下来的会话,极大节省使用非对称加密进行通话的开销。


发送请求 & 接收响应

在 TCP 连接(HTTPS 还需要进行 TSL 确认)完成后,客户端就能愉快地向服务器发起请求了。

客户端使用 HTTP 协议(80 端口)或者 HTTPS 协议(443 端口)发送请求数据报给服务器。

请求

一个 HTTP 请求报文的例子:

该报文大概由五部分构成:

  1. 请求方法:最最常用的就是 GET 和 POST 了
  2. 请求 URL:和请求头中的 Host 属性一起组成了完整的 URL
  3. HTTP 协议和版本
  4. 请求头:一些关于请求和客户端的额外信息
    • Accept: 告诉服务器我期望接收到什么类型的数据(MIME 类型)
    • Cookie
    • Referer: 请求是从哪个页面发起的,常用来设置防盗
    • Cache-Control: 告诉服务器你返回的数据我客户端会怎么去缓存
    • ……
  5. 请求体:请求的参数部分

客户端将一股脑儿信息塞到请求数据报里面,然后发送给服务器。

服务器收到请求,根据缓存客户端的缓存需求确定是否需要协商缓存。

协商缓存可参考:前端缓存之 http 缓存

响应

服务器可在响应头中设置缓存相关的参数。

服务器返回响应之后,一次浏览器和服务器之间友好的交流就结束啦。


关闭 TCP 连接

当浏览器与服务器会话结束后,需要关闭 TCP 连接。

在建立连接时,需要客户端主动发起建立请求;而断开连接时,客户端和服务器都可以主动断开。

以客户端主动断开连接为例,整个过程被叫做 四次挥手

https://barwe.cc/archives/2022-03-27-16-24-48


浏览器渲染

浏览器通过缓存或者请求拿到了需要的数据,然后就是将数据渲染到页面上。

构建 DOM 树

浏览器请求到的数据都是 字节流,需要先转化为 DOM 树,具体步骤是:

  1. 解码:通过指定的编码方式(例如 UTF-8)将请求到的字节流数据解码成 html 字符串
    1. 能解析出带 charset 属性的 meta 标签,边接收边解码
    2. 找不到 charsetmeta 标签,接收完了使用默认的编码方式解码
  2. 分词(token):根据一些特殊的字符例如</>= 等从 html 字符串中提取出标签和属性列表
  3. 创建 & 添加节点:在分词步骤中,每解析出一个 token 就新建一个节点,并将节点加入到 DOM 树中

经过解码、分词、创建和添加节点,最终得到了一颗 DOM 树,这棵树包含了各个节点间的父子兄弟关系。

样式计算

css 样式需要应用到节点上才有意义,一个节点的样式也会受到它的所有祖先节点样式的影响。

css 样式来源主要有三个:

  • 通过 .css 文件从外部引入的,常通过 class 属性与元素关联起来
  • 在头部的 style 标签中定义的样式
  • 在元素 style 属性中定义的样式

计算样式主要做这几个事情:

  1. 属性值的标准化:例如将 em, rem, % 等等数值转化为 px 标准值,颜色代码转化等等
  2. 处理样式的继承和层叠

样式计算最终产生样式表,可通过 document.styleSheets 查看。

页面布局

经过前面两步,我们计算出了所有节点的 DOM 树和 CSS 样式表。

然后就要根据标签类型或者样式信息对 DOM 的节点过滤,计算可见节点的位置和样式,最终生成一颗只包含可见元素的树。

剪枝:

  • 去掉 script, meta 等非视觉化的标签
  • 去掉 display: none 的标签
  • ……

这个过程就是将 DOM 树与样式表结合,构造布局树的过程。

页面更新时会发生 回流(reflow)和 重排(repaint)。

生成分层树

为了更方便的实现某些页面效果,例如滚动、z-index等,浏览器会为特殊的节点生成专用的图层。

渲染引擎会划分出多个图层,这些图层的叠加就构成了最终的视觉页面。

并不是布局树上的每一个节点都会生成一个图层,一个没有对应图层的节点属于它的父节点的图层。

渲染引擎在生成图层时有它自己的一套规则,这里略。

栅格化

现在分层树也建好了,渲染线程的任务就算完成了。

哪些图层、哪些节点需要展示给用户就需要 合成线程 进行调度。

栅格化负责将图层进行叠加组合,然后分块,并将视口区域附近的分块展示给用户。

如果页面很大,浏览器不需要一下绘制出所有页面,因为视口区域实际上非常有限,视口区域之外的内容绘制那么多也没啥实际作用。

显式

合成线程发送绘图指令给浏览器主进程,浏览器按照绘图指令绘图展示给用户。


参考

https://juejin.cn/post/6871947938701475847

0

评论区