URL输入到页面渲染

Posted by Youzi on March 8, 2021

在浏览器中从 URL 输入到页面展现到底发生什么?

记录该问题的详细解答;

浏览器进程

现代浏览器是多进程的,其子进程分类如下:

  • 主进程:一个,控制页面创建、销毁、网络资源管理、下载等;
  • 插件进程:每种类型的插件对应一个进程,仅当使用时才创建;
  • GPU 进程:最多一个,用于 3D 绘制等;
  • 浏览器渲染进程(浏览器内核):每个 Tab 对应一个进程,互不影响;

1. 输入网址并解析

仅考虑输入是 URL 结构的字符串,如果非 URL 结构的,会用浏览器默认的搜索引擎搜索该字符串;

URL 组成

URL 由协议,主机,端口,路径,查询参数,锚点 6 个部分组成;

例:

1
2
3
4
5
6
http://(协议)
localhost(主机)
:8080(端口)
/path/index.html(路径)
?q=1&p=2(查询参数)
#hash(锚点)

解析 URL

浏览器会解析 URL 的协议,主机,端口,路径等信息,然后构造一个 HTTP 请求,这里会涉及到强缓存以及协商缓存;

HSTS

详见:你所不知道的 HSTS;简单来说是如果加入了 HSTS 列表,用户不需要输入协议HTTP / HTTPS都会定向到HTTPS进行访问;

缓存

缓存内容详见:前端缓存

  1. 浏览器发送请求前,会根据 HTTP 请求头部的字段expires | cache-control判断是否命中强缓存,如果命中,则直接从缓存获取所需资源,不会发送请求到服务器端,此时资源请求响应的状态码是200(from cache),否则进入下一步;
  2. 没有命中强缓存,浏览器会根据请求头的last-modified | etag来判断是否命中协商缓存,如果命中,会从缓存中获取资源,此时资源请求响应的状态码是304(not modified),否则进入下一步;需要注意的是,这一步浏览器会发送请求到服务器端;
  3. 如果前两步都么有命中,则会从服务器端获取资源。

DNS 域名解析

在发起 HTTP 请求之前,浏览器首先要获取域名对应的 ip 地址,浏览器会发送一个 UDP 的包给 DNS 域名解析服务器;

DNS解析过程

可以看到其实 DNS 也是由缓存的,这个查询过程是递归查询,依次是:浏览器缓存,系统缓存,路由器缓存,ISP 缓存;如果缓存都未命中,会发起迭代查询,向域名解析服务器轮询;依次是:根域名服务器,com 级域名服务器,公共的域名服务器;迭代查询指的是每次到域名服务器查询时,如果查不到相关记录,会把再上一级的域名服务器 ip 返回给查询者,让查询自己再去查上一级;

TCP/IP 连接:三次握手

TCP/IP 协议及分层

OSI 七层模型 TCP/IP 概念层模型 功能 TCP/IP 协议簇
应用层 应用层 文件传输,电子邮件,文件服务 HTTP, FTP, SMTP…
表示层 应用层 数据格式化,代码转换,数据加密 没有协议
会话层 应用层 解除或建立与别的节点的联系 没有协议
传输层 传输层 提供端对端的接口 TCP,UDP
网络层 网络层 为数据包选择路由 IP,ICMP,RIP…
数据链路层 链路层 传输有地址的帧及错误检测 SLIP,CSLIP,ARP…
物理层 链路层 以二进制数据形式在物理媒体上传输数据 IEEE802,ISO2110…

三次握手

客户端和服务端在进行 TCP 连接时,会进行三次握手;

三次握手

TCP 的标志位:

  • SYN(synchronous 建立联机)
  • ACK(acknowledgement 确认)
  • PSH(push 传送)
  • FIN(finish 结束)
  • RST(reset 重置)
  • URG(urgent 紧急)

第一次握手:client 发送同步信号SYN=1期望建立连接,并随机生成序列号seq=999的数据包到服务器,server 由SYN=1知道 client 要求建立连接;(第一次握手由浏览器发起,告诉服务器我要发送请求了);

第二次握手:server 收到请求后要确认连接信息,向 client 发送确认信号ack=client的seq+1,同步信号SYN=1,随机生成的序列号seq=666;(第二次握手,由服务器发起,告诉浏览器我准备接受数据包了);

第三次握手:client 收到包后,检查ack是否正确(ack 的自增规则是确定的),以及同步信号SYN=1是否正确,如果正确,client 会再次发送ack=server的seq+1,随机生成的序列号seq=333,发送后 server 收到后确认seqack则建立连接成功;(第三次握手由浏览器发送,告诉服务器,我马上就要发送数据包了)

为什么需要三次握手

由于 TCP 是一种“可靠”(相对于 UDP)的传输协议,client 和 server 都需要互相确认对方的接收发送能力;第一次握手可以确认 client 的发送能力,第二次握手,server 发送SYN, seq给 client,这样 client 确认了服务器的发送能力,另外 server 还发送了ACK=客户端的seq+1,这就让 client 确认了 server 的接收能力;第三次握手,client 发送了ACK=服务器的seq+1,让 server 确认了 client 的接收能力,双方互相确认后,才不会出现丢包等等的现象;

第三次握手的必要性

试想如果是用两次握手,则会出现下面这种情况:如客户端发出连接请求,但因连接请求报文丢失而未收到确认,于是客户端再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接,客户端共发出了两个连接请求报文段,其中第一个丢失,第二个到达了服务端,但是第一个丢失的报文段只是在某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达服务端,此时服务端误认为客户端又发出一次新的连接请求,于是就向客户端发出确认报文段,同意建立连接,不采用三次握手,只要服务端发出确认,就建立新的连接了,此时客户端忽略服务端发来的确认,也不发送数据,则服务端一致等待客户端发送数据,浪费资源。

半连接队列

这个概念是针对 server 的,server 在第一次收到建立连接的请求时,会处于 SYN_RCVD 状态,此时连接还没有完全建立,所以 server 会把这种状态的请求连接放在队列里,这个队列就是半连接队列;

与之相对的是全连接队列,就是完成了三次握手的连接,如果队列满了就可能出现丢包;

在 server 等待第三次握手请求的时间里,如果等待时间超出了系统设定的 SYN-ACK 重传时间,则会进行重传,也就是把刚刚发给 client 的确认包再次发送,重传次数取决于系统规定的最大重传次数,如果超过了,就会把这个连接信息从半连接队列里删除;

重传等待时间一般是指数增长的,每次间隔为 1s,2s,4s,8s…

ISN(初始序列号)

当一端为建立连接而发送 SYN 时,它会为连接选择一个初始序号,ISN 随时间变化,因此每个连接都有不同的 ISN,是一个 32 位的计数器,每 4ms 自增 1,这样选择序列号在于防止网络中被延迟的分组在以后被传输,而导致某个连接的一方对其作出错误解释;

三次握手重要功能是 client 和 server 交换 ISN,以便让对方知道接下来接收数据的时候如何按照序列号组装数据,如果 ISN 是固定的,那攻击者很容易就猜出了后续的确认号。

HTTP 请求

HTTP 版本:

  • HTTP2

二进制分帧:相比于 HTTP1.X 的文本格式,2 采用了二进制格式进行数据传输,并将请求和响应数据分割为更小的帧来进行传输;

多路复用:代替原来的序列和阻塞机制,所有请求都是通过一个 TCP 连接并发完成的;在 1.X 中,想要并发多个请求,就得使用多个 TCP 连接,而且浏览器一般对并发请求数有限制(允许 6-8 个请求并发),后续的请求会挂起等待;在 2 中,同域名下所有通信都在单个连接上完成;

  • 同一个域名下的所有通信都在单个连接上完成,消除了多个 TCP 连接带来的延时和内存消耗;
  • 单个连接可以承载任意数量的双向数据流;
  • 数据流以消息形式发送,消息由多个帧构成,多个帧之间可以乱序发送,因为帧首部有流标识,可以重新组装;

服务器推送:服务器可以在发送页面 HTML 时主动推送其他资源,不用等到浏览器解析后发送请求才返回相应的资源;客户端也可以选择接收与否,缓存策略依旧生效;主动推送也遵守同源策略;

头部压缩:1.X 请求会携带大量的冗余头信息,占用了很多带宽,cookie,浏览器属性,描述此次通信的资源等;而在 2 中,为了减少头部资源的消耗,对头部采取了压缩策略;

  • 在客户端和服务器使用首部表来跟踪和存储之前发送的头部key-value对,对于相同的数据,不再通过每次请求和响应发送;
  • 首部表在 2 的链接存续期内始终存在,由客户端和服务器共同渐进更新;
  • 每个新的首部key-value对要么被追加到当前表的末尾,要么替换掉之前的值;

  • HTTP3(待补充)

HTTPS

在 HTTP 基础上加上 TLS(传输层安全协议)或者 SSL(安全套接层)就构成了 HTTPS 协议;它默认工作在 TCP 协议的 443 端口上,工作流程如下:

  1. TCP 三次握手;
  2. 客户端验证服务器的证书;
  3. DH 算法协商对称加密算法的密钥,hash 算法的密钥;
  4. SSL 安全加密隧道协商完成;
  5. 数据以对称加密的方式传输,用协商好的加密算法和密钥进行加密;用协商好的 hash 算法对数据进行完整性保护,保证不被篡改;

TLS握手过程

握手过程:

  1. 客户端向服务端发送 Client Hello 消息,其中携带客户端支持的协议版本、加密算法、压缩算法以及客户端生成的随机数X;服务端收到客户端支持的协议版本、加密算法等信息后;

  2. 向客户端发送 Server Hello 消息,并携带选择特定的协议版本、加密方法、会话 ID 以及服务端生成的随机数Y

    • 向客户端发送 Certificate 消息,即服务端的证书链,其中包含证书支持的域名、发行方和有效期等信息;
    • 向客户端发送 Server Key Exchange 消息,传递公钥以及签名等信息;
    • 向客户端发送可选的消息 Certificate Request,验证客户端的证书;
    • 向客户端发送 Server Hello Done 消息,通知服务端已经发送了全部的相关信息;
  3. 客户端收到服务端的协议版本、加密方法、会话 ID 以及证书等信息后,验证服务端的证书,随机数Z

    • 向服务端发送 Client Key Exchange 消息,包含使用服务端公钥加密Z后的随机字符串,即预主密钥(Pre Master Secret);
    • 向服务端发送 Change Cipher Spec 消息,通知服务端后面的数据段会加密传输;
    • 向服务端发送 Finished 消息,其中包含加密后的握手信息;
  4. 服务端收到 Change Cipher Spec 和 Finished 消息后,服务器接收到预主密钥后,用自己的私钥解密,得到随机数Z,此时客户端和服务器都有了三个随机数X, Y, Z,双方都用约定好的算法生成一个对称密钥,后续的消息都用该密钥来加解密;

    • 向客户端发送 Change Cipher Spec 消息,通知客户端后面的数据段会加密传输;
    • 向客户端发送 Finished 消息,验证客户端的 Finished 消息并完成 TLS 握手

握手过程的会话缓存:

为了加快建立握手的速度,减少 TLS 协议带来的性能和资源消耗,TLS 有两种会话缓存的机制:会话标识 sessionID 和会话记录 sessionTicket;

  • sessionID 由服务器支持,是协议中的标准字段,服务器会保存会话 ID 及协商的通信信息,nginx 中 1M 内存可以保存大概 4000 个会话 ID 相关的信息,比较占用服务器资源;
  • sessionTicket 需要双端都支持,属于扩展字段,支持范围不是很广,其特点是将协商好的通信信息加密后发给客户端保存,密钥只在服务器上保存,占用服务器资源很少;

服务器处理请求并返回 HTTP 报文

每台服务器上都会安装处理请求的应用——web server,常见的有apache, nginx...

HTTP 请求一般分为两类,静态资源和动态资源;

请求静态资源时,直接根据请求的 URL 在服务器里找到相应的资源返回就好了;

请求动态资源,需要 web server 把不同请求委托给服务器上处理相应请求的程序去处理,比如各类脚本;然后把处理结果返回给客户端;

服务器在处理请求时主要有三种方式:

  • 单线程处理所有请求,同时只能处理一个,效率很低;
  • 每个请求都分配线程,这样会导致服务器 CPU 使用率爆炸;
  • 采用复用 I/O 的方式来处理;比如见识所有连接,当连接状态发生改变时才分配空间去处理请求;

浏览器渲染

页面渲染流程:

页面渲染过程

DOM 树

DOM 的表示方法有多种,其中 DOM 树是比较普遍的一种实现方式;DOM 结构的基本组成部分是节点(node),节点可以是整个文档(称为文档节点 Document),可以是元素节点(element),属性节点,注释节点等;

以 HTML Document 节点为根节点,其余节点为其子节点,构成的一个树型的数据结构,就是 DOM 树;

HTML 解释器

HTML 解释器的工作就是将网络或者本地磁盘获取到的 HTML 网页和资源,从字节流解释为 DOM 数结构;

一般流程为:读取字节->字符->令牌->节点->DOM,详细可以看这篇从 Chrome 源码看浏览器如何构建 DOM 树 - 李银城的文章 - 知乎

DOM树生成过程

  • 字节转换成字符:由于读取 HTML 文件的原始字节要根据文件编码转换成可识别的字符,所以有第一步;
  • 将 HTML 字符串转换成令牌,每个令牌都有特殊含义和一组规则,为了更好的做词法分析;
  • DOM 树构建:我们知道 HTML 标记间存在父子关系,所以很适合建立起树形结构,有了令牌后,就可以根据令牌结构生成 DOM 树。

CSS 对象模型(CSSOM)

其构建过程与 DOM 树的过程类似;

布局树

  • DOM 树与 CSSOM 树合并后形成 render tree;
  • 渲染树只包含渲染页面需要的节点;
  • 布局计算每个对象的精确位置和大小;
  • 最后一步是绘制,把最终渲染树渲染到屏幕上。

渲染

  1. 获取 DOM 后分割成多个图层;
  2. 对每个图层的节点计算样式结果(Recalculate style–样式重计算);
  3. 为每个节点生成图形和位置(Layout–重排,回流);
  4. 将每个节点绘制填充到图层位图中 (Paint–重绘)
  5. 图层作为纹理上传至 GPU
  6. 组合多个图层到页面上生成最终屏幕图像 (Composite Layers–图层重组)

回流、重绘

  • 重绘(repaint)

页面样式改变,且不影响它在文档流中的位置时,不影响布局,比如(color,background-color,visibility 等),浏览器会将新的样式赋予给元素并重新绘制它,这就是重绘;

  • 回流(reflow)

当渲染树中部分或全部元素的尺寸,结构,或某些属性发生改变时,浏览器重新渲染部分或者全部文档的过程,称为回流;每个页面至少需要一次回流,因为要构建渲染树;在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程称为重绘。

回流必会引起重绘,而重绘不一定引起回流

引起回流的情况:

  1. 页面首次渲染;
  2. 浏览器窗口大小发生改变
  3. 元素尺寸或者位置发生改变
  4. 元素内容变化(文字数量或图片大小)
  5. 元素字体大小变化
  6. 添加或者删除可见的 DOM 元素
  7. 激活 CSS 伪类(::hover)
  8. 查询某些属性或者调用某些方法

引起回流的属性和相关方法:

  • clientWidth、clientHeight、clientTop、clientLeft
  • offsetWidth、offsetHeight、offsetTop、offsetLeft
  • scrollWidth、scrollHeight、scrollTop、scrollLeft
  • scrollIntoView()、scrollIntoViewIffNeeded()
  • getComputedStyle()
  • getBoundingClientRect()
  • scrollTo()

减少回流的方式:

  • CSS

    1. 避免使用 table 布局;
    2. 尽可能在 DOM 数的最末端改变 class
    3. 避免设置多层内联样式;
    4. 将动画效果应用到 position 属性为 absolute 或 fixed 元素上;
    5. 避免使用 CSS 表达式(calc())
  • JS

    1. 避免频繁操作样式,最好一次性重写 style,或者将样式列表定义为 class 并一次性更改 class;
    2. 避免频繁操作 DOM,创建一个 documentFragment,在它上面应用所有 DOM 操作,最后再添加到文档中;
    3. 也可以先为元素设置display: none,操作结束后再把它显示出来;设置这个属性后,在元素上进行的 DOM 操作不会引起回流和重绘;
    4. 避免频繁读取会引发回流、重绘的属性,如果确实需要多次使用,就要用一个变量缓存起来;
    5. 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素以及后续元素的频繁回流;

断开连接:TCP 四次挥手

四次挥手流程

  1. 刚开始双方都处于 established 状态,假如是客户端先发起关闭请求
  2. 第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1 状态
  3. 第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值+1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态
  4. 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发送 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态
  5. 需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。

挥手为什么需要四次?

因为当服务端收到客户端的 SYN 连接请求报文后,可以直接发送 SYN+ACK 报文。其中 ACK 报文是用来应答的,SYN 报文是用来同步的。但是关闭连接时,当服务端收到 FIN 报文时,很可能并不会立即关闭 SOCKET,所以只能先回复一个 ACK 报文,告诉客户端,“你发的 FIN 报文我收到了”。只有等到我服务端所有的报文都发送完了,我才能发送 FIN 报文,因此不能一起发送。故需要四次挥手。

四次挥手释放连接时,等待 2MSL 的意义?

MSL(最长报文周期)

  1. 保证客户端发送的最后一个 ACK 报文段能够到达服务端。

这个 ACK 报文段有可能丢失,使得处于 LAST-ACK 状态的 B 收不到对已发送的 FIN+ACK 报文段的确认,服务端超时重传 FIN+ACK 报文段,而客户端能在 2MSL 时间内收到这个重传的 FIN+ACK 报文段,接着客户端重传一次确认,重新启动 2MSL 计时器,最后客户端和服务端都进入到 CLOSED 状态,若客户端在 TIME-WAIT 状态不等待一段时间,而是发送完 ACK 报文段后立即释放连接,则无法收到服务端重传的 FIN+ACK 报文段,所以不会再发送一次确认报文段,则服务端无法正常进入到 CLOSED 状态。

  1. 防止“已失效的连接请求报文段”出现在本连接中。

客户端在发送完最后一个 ACK 报文段后,再经过 2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现这种旧的连接请求报文段。

参考

感谢:https://juejin.cn/post/6935232082482298911