2
头图

解析地址栏中的信息

浏览器监听用户输入的信息并尝试匹配你想要访问的网址或关键词。以掘金为例,在浏览器地址栏中输入信息,然后回车,浏览器会进行以下判断:

  • 判断是否是合法的 URL 链接;
  • 是。继续判断 URL 是否完整,如果不完整,浏览器可能会对域进行猜测,对输入的内容添加前缀、后缀、或者前后缀来补全 URL,常见的 URL 通产包括:

    • 协议:如 http https websocket
    • 域名(主机名):可能是IP地址,也可能是域名。域名可能由根域名、顶级域名、二级域名等组成,域名的叫法是根据域名从右向左以 . 分隔进行划分,比如:juejin.cn.. 代表根域名,.cn 代表顶级域名,juejin.cn 代表二级域名(也就是主机名)
    • 端口号:http 协议默认端口号为 80,https 协议默认端口号为 443。浏览器会自动隐藏默认端口号。
    • 路径:以 / 划分每一层目录,比如:/web/user
    • 查询:以 ? 开始,以 & 分隔键值对,如:?username="张三"&age=16
    • 哈希:以 # 开始,利用它可实现定位到当前页面的具体位置
  • 否。浏览器将输入的内容作为搜索条件,使用用户设置的默认搜索引擎进行查询并返回结果

查找强缓存

浏览器进程通过进程间通信(IPC)将 URL 请求发送给网络进程,网络进程接收到URL请求后,会发起真正的请求。但在请求之前,网络进程会查找本地是否缓存了该资源。如果有缓存资源,那么直接返回资源给浏览器进程。首选,查找强缓存资源,如果有则检查强缓存资源是否过期,没过期直接使用该资源,过期则重新向服务器请求资源。强缓存涉及到两个字段:

  • Expires。即过期时间(Expires=Wed, 21 Oct 2015 07:28:00 GMT),HTTP/1.0 采用此字段,它存在于服务器返回的响应头中,告知浏览器在过期时间范围内直接使用缓存资源。但它有个很大的缺点,当服务器和客户端的时间不一致时,那么服务器返回的时间是不准确的,因此,HTTP/1.1 抛弃了这个字段而采用了 Cache-Control字段
  • Cache-Control。即过期时长(Cache-Control:max-age=3600),HTTP/1.1 采用此字段,它也存在于服务器返回的响应头中,告知浏览器在过期时长范围内直接使用缓存资源。它还可以设置其他指令,下面列举一些关键指令:

    • public。浏览器和代理服务器都可以缓存资源
    • private。只能浏览器缓存资源,代理服务器不能缓存资源
    • no-cache。跳过强缓存阶段。向服务器发送请求,进入协商缓存阶段
    • no-store。不缓存
    • s-maxage。代理服务器的缓存时间
    • must-revalidate。一旦缓存过期,就必须回到源服务器验证

扩展:

  • 怎么设置强缓存?可以在服务端代码中设置 Cache-Control 字段以及他对应的值;
  • 强缓存资源缓存在哪儿? memory cachedisk cache ,也就是内存或硬盘中,一般会将图片、脚本文件、字体文件缓存在 memory cache 中;将样式文件缓存在 disk cache 中。
    image.png
    image.png
  • 访问缓存的优先级?遵循三级缓存原理:先在 memory cache 中找,有则直接使用;没有再去 disk cache 中找,有则指直接使用;没有就进行网络请求,将请求返回的资源根据响应头字段信息进行缓存。

DNS域名解析

如果在强缓存中没有找到所需资源,那么直接进入网络请求流程。通常情况下,我们在浏览器的地址栏中输入的都是域名,而在网络通信中是以 IP 地址确定目的主机的,所以还得通过域名找到对应的 IP 地址。
DNS 又是什么?DNS全名是 domain name system(域名系统),它将域名和 IP 地址映射关系保存在一个分布式数据库中,所以我们可以通过 DNS 找到对应的 IP,而这个查找的过程就是 DNS 域名解析。下面以 juejin.cn.来分析域名的解析过程:

  • 浏览器 DNS 缓存。浏览器从 URL 中提取出主机名,从浏览器 DNS 中查找是否有缓存记录,有则直接使用缓存IP,完成解析;
  • hosts 文件。从本机的 hosts 文件中查找是否有缓存记录,有则返回对应 IP,完成解析;
  • 本地 DNS 服务器。向本地 DNS 服务器发送查询请求,有则本地 DNS 服务器将记录作为响应返回给主机,完成解析;
  • ISP(互联网服务提供商)DNS缓存。本地 DNS 服务器将查询请求转发给 ISP 提供的 DNS 服务器,有则将记录作为响应返回给本地 DNS 服务器,本地 DNS 服务器返回给主机,完成解析;
  • 根域名服务器。根据本地 DNS 服务器的设置(是否设置转发器)进行查询,若是未用转发模式,本地 DNS 就把请求发至 13 台根域名服务器 ,根域名服务器收到请求后会判断这个域名(.cn)是谁来授权管理,并会返回一个负责该顶级域名服务器的一个IP。若是转发模式,该 DNS 服务器就会把请求转发至上一级 DNS 服务器,由上一级服务器进行解析,上一级服务器如果不能解析,或找根域名服务器或将请求转至上上级,以此循环;
  • 顶级域名服务器。从根域名服务器得到顶级域名服务器地址后,本地 DNS 服务器向顶级域名服务器发送查询请求,收到本地域名服务器请求后会查看区域文件记录,有则将记录作为响应返回给本地 DNS 服务器,由本地 DNS 服务器返回给主机,完成解析。如果自己无法解析,它就会找一个管理(.cn)域的二级域名服务器 IP 返回给本地DNS服务器;
  • 二级域名服务器。从顶级域名服务器得到二级域名服务器地址后,本地 DNS 服务器向二级域名服务器发送查询请求,二级域名服务器收到本地域名服务器请求后会查看区域文件记录,若有则将记录返回给本地 DNS 服务器,完成解析。到这一步还是没能完成解析,那域名可能存在错误而产生异常。
    递归查询:
    客户端向本地 DNS 服务器发起查询请求,等待本地域名服务器返回结果。本地 DNS 服务器若无法解析,自己会以DNS客户机的身份向其他域名服务器发起查询请求,直到将查询结果返回给客户端为止。

迭代查询:本地 DNS 服务器首先向根域名服务器发起查询请求,若根域名没有找到对应记录就将下一个目标域名服务器的 IP 返回给本地 DNS 服务器(也称为根提示),直到将查询结果返回给客户端为止。

建立TCP连接

建立连接遵循三次握手原则。三次握手的主要目的就是确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备。实质上就是连接服务器指定端口,建立 TCP 连接,并同步连接双方的序列号和确认号,交换 TCP 窗口大小信息。现在,客户端处于 Closed 状态,服务端处于 Listen 状态

  • 客户端给服务端发出一个连接SYN报文,并指明同步位SYN=1,初始序号seq=x,然后客户端处于 SYN_SEND 状态;
  • 服务器收到客户端的SYN报文后,会以自己的 SYN 报文 (SYN=1,ACK=1) 作为应答,并且指定自己的初始化序列号 seq=y ,同时会把客户端的 seq + 1 = x + 1 作为 ack 的值 (ack=x+1) ,表示自己已经收到客户端的 SYN报文,此时服务器处于 SYN_REVD 状态;
  • 客户端收到 SYN报文 后,会发送一个 ACK 报文 (ACK=1) 作为应答,并且之地当自己的序列号 seq=x+1 ,同时会把服务器的 seq + 1 = y + 1 作为 ack 的值 (ack=y+1) ,表示自己已经收到服务器的 SYN报文 ,此时客户端处于 ESTABLISHED 状态,服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。

客户端发起请求,服务器处理请求

客服端会将请求行、请求头,请求体的相应信息发送给服务器。
image.png
服务器收到请求后进行逻辑处理,并根据处理结果返回响应数据(响应行、响应头、响应体等)。
image.png
这里提一下 HTTP 数据传输过程中的优化策略。在 HTTP 数据传输的过程中会将报文进行拆包(将报文拆分成小的数据包),依次传输给接收方,接收方每次接收到数据包后必须向发送方确认,如果发送方没有收到这个确认的消息,就判定为数据包丢失,并重新发送该数据包,接收方需要完成组包(将每次接收的数据包按顺序组装为完整的数据包)从而获得完整的数据包。

关闭TCP连接

数据传输完毕后还要根据 Connection 字段判断是否需要断开连接,若请求头或响应头中包含 Connection: Keep-Alive 表示持久连接,之后请求同一站点的资源会复用此连接;不满足上述情况就需要断开连接,断开连接遵循四次挥手原则。断开连接这个动作可以由客户端或者服务器任一方发起,此时,客户端和服务器都处于 ESTABLISHED 状态,假设由客户端发起关闭请求,则流程如下:

  • 客户端发起一个连接释放FIN报文段 (FIN=1) ,报文中指定一个序列号 (seq=u) ,并停止发送数据,关闭 TCP 连接。客户端进入 FIN_WAIT1(终止等待1) 状态,等待服务器的确认;
  • 服务器收到连接释放的FIN报文段后,会发出 ACK 报文 (ACK=1) ,并指定自己的序列号 (seq=v) ,同时会把客户端的 seq + 1 = u + 1 作为 ack 的值 (ack=u+1),表明已经收到客户端的报文了。服务端进入 CLOSE_WAIT(关闭等待) 状态,此时的TCP处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入 FIN_WAIT2(终止等待2) 状态,等待服务端发出的连接释放报文段;
  • 若此时服务器已经没有数据需要发送给客户端了,服务器就会发出连接释放报文段 (FIN=1, ACK=1, 序列号seq=w, 确认号ack=u+1) ,服务端进入 LAST_ACK(最后确认) 状态,等待客户端的确认;
  • 客户端收到服务端的连接释放报文后,对此发出确认报文段 (ACK=1, seq=u+1, ack=w+1) ,客户端进入 TIME_WAIT(时间等待) 状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,客户端才进入 CLOSED 状态。

处理响应信息

网络进程接收到响应数据后开始解析响应头,解析到响应状态码会根据不同的码值进行处理,下面列举一下常用的状态码:

  • 200 OK。请求处理成功,响应数据放在响应体中。
  • 301 Moved Permanently 。永久重定向,如果以前的域名地址不再使用,需要更换新的域名地址来访问资源,可以将响应状态码置为301,浏览器默认会做缓存优化,再次访问原地址时会自动访问重定向的那个地址
  • 302 Found。临时重定向,若只是暂时不使用原地址可以返回 302 状态码。如:网站正在维护,那么可以在当前域给出一个解释页面来通知访问者
  • 304 Not Modified。协商缓存。浏览器首次请求资源时,服务器会将 Last-ModifiedETag 两个字段放在响应头中返回给浏览器。

    • Last-Modified。即资源的最后修改时间,当浏览器第二次向服务器发起请求时,会在请求头中携带 If-Modified-Since 字段,服务器拿到请求头中的 If-Modified-Since 字段后会将字段值与当前服务器中该资源的Last-Modified 字段值比较。若 If-Modified-Since 的值小于服务器中 Last-Modified 的值 ,表明服务器上的资源更新过,服务器会更新 Last-Modified 的值并将新的资源返回给浏览器,响应状态码为 200 ;否则返回 304 状态码,告诉浏览器直接使用缓存资源;
    • Etag。即资源最后修改的内容,根据文件内容生成 hash 值。当浏览器第二次向服务器发起请求时,会在请求头中携带 If-None-Match 字段,服务器拿到请求头中的 If-None-Match 字段后会将字段值与当前服务器中该资源的 Etag 相比较,若两值不相等,服务器将更新 Etag 的值并返回新的资源给浏览器,响应状态码为 200 ;否则返回 304 状态码,告诉浏览器直接使用缓存资源。当 EtagLast-Modified 同时存在时,先根据 Etag 判断,再根据 Last-Modified 判断返回什么状态码。
  • 400 Bad Request。请求参数有误
  • 401。身份认证
  • 403 Forbidden。服务器禁止访问
  • 404 Not Found。服务器未找到相应资源
  • 500 Internal Server Error。服务器出错了
  • 503 Service Unavailable。服务器繁忙,暂时无法处理响应服务
    解析到响应数据类型会判断 Content-Type 字段,它会告诉浏览器服务器返回的响应体数据是什么类型,然后浏览器会根据它的值来决定如何显示响应体的内容。如果值是 application/octet-stream 字节流类型,通常会按下载类型来处理;如果值是 text/html 类型则准备渲染进程。

准备渲染进程

通常情况下打开一个 Tab 页就要启动一个渲染进程,但这里有个同一站点(same-site)的特例,属于同一站点的Tab页使用同一个渲染进程。同一站点的特性包括:

  • 根域名加上协议相同
  • 属于同一个根域名下的所有子域名加上不同的端口号

    // 同一站点
    https://time.geekbang.org
    https://www.geekbang.org
    https://www.geekbang.org:8080

提交文档阶段

渲染进程准备好后进入提交文档阶段。流程如下:

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

image.png

构建DOM树

浏览器无法直接理解和使用 HTML ,所以需要将 HTML 转换为浏览器能够理解的 DOM 树结构。可在浏览器的控制台输入 document 进行查看。具体转换过程如下:

  • 转换(字节->字符):浏览器从磁盘或网络读取 HTML 的原始字节,并根据文件的指定编码(如:UTF-8)把它们转换成各个字符
  • 令牌化(字符->令牌):浏览器将字符转换为符合 W3C 标准的令牌(如:<html> <body>),以及其他尖括号内的字符串。每个令牌都具有特殊含义和一组规则
  • 词法分析(令牌->节点):发出的令牌转换成定义其属性和规则的“对象”
  • DOM构建(节点->DOM):由于 html 标记定义不同标记之间的关系,创建的对象链接在一个树数据结构内,此结构也会捕获原始标记中定义的父项-子项关系:HTML 对象是 body 对象的父项,bodyparagraph 对象的父项,依此类推。

在解析 HTML 文件的过程中,可能中途需要网络进程去下载脚本文件以及样式文件,那么就有一个阻塞 DOM 解析以及渲染的问题,具体可参考 原来 CSS 与 JS 是这样阻塞 DOM 解析和渲染的 这篇文章,把结论贴一下:

  • CSS 不会阻塞 DOM 的解析,但会阻塞 DOM 渲染。
  • JS 阻塞 DOM 解析,但浏览器会"偷看"DOM,预先下载相关资源。
  • 浏览器遇到 <script> 且没有 deferasync 属性的标签时,会触发页面渲染,如果前面 CSS 资源尚未加载完毕,浏览器就会等待它加载完毕再执行脚本。

样式计算

把 CSS 转换为浏览器能够理解的结构:浏览器无法直接理解 CSS 样式,所以当渲染引擎接收到 CSS 文本时,会执行转换操作,将 CSS 文本转换为 styleSheets 。可在浏览器的控制台输入 document.styleSheets 进行查看。具体转换过程如下:

  • 转换样式表中的属性值,使其标准化:比如,在编写代码时使用的是十六进制的颜色,需要转换成 rgb 的格式,李兵老师的课程有说把 em 单位转为 px 单位,bold 转为700,我打开 Chrome 的开发者工具看并没有转,这里跟老师的描述有点出入。
  • 计算出 DOM 树中节点的样式:主要通过继承规则和层叠规则来进行计算。计算完成之后输出每个 DOM 节点的样式,并保存到 ComputedStyle 的结构内。
  • 继承规则:子节点如果没有设置 font-sizecolorfont-family 几个属性的样式,那么可以继承父节点的样式,如果父节点也没有设置其样式,那么默认使用 UserAgent 样式。注意:只有可继承属性才能继承。
  • 层叠规则:它是一个定义了如何合并来自多个源的属性值的算法。

布局阶段

计算出 DOM 树中可见元素的几何位置。具体过程如下:

  • 创建布局树:遍历 DOM 树中所有可见节点,生成一棵只包含可见节点的布局树。不可见的节点会被忽略,不会出现在布局树上,不可见节点包括:1. 不会渲染输出的节点(script/link/meta/head),2. 通过 CSS 隐藏的节点(display: none)会被忽略掉;
  • 布局计算:计算布局树节点的几何位置,并将计算出的信息保存到布局树中。

分层

由于页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-index 做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)。通常情况下,并不是布局树中每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。这些图层经过合成变为最终的页面。渲染引擎为特定节点创建单独的图层的条件包括:

  • 拥有层叠上下文属性的元素会被提升为单独的一层。以下是层叠上下文属性示意图:
    image.png
  • 需要剪裁(clip)的地方也会被创建为图层。当一个元素设置了固定宽高而里面的内容超出了这个元素,那么超出的部分会被剪裁,渲染引擎会创建一个单独的图层来存放被剪裁的内容。

图层绘制

构建完图层树之后,渲染引擎会对图层树中的每个图层进行绘制。渲染引擎会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表。

栅格化(raster)

绘制列表准备好之后,主线程会把绘制列表提交给合成线程,由合成线程来完成具体的绘制操作。先了解一下什么是视口(viewport)?视口就是屏幕上页面的可见区域。随着业务的复杂性,某些情况下图层可能会很长,而用户通过视口只能看见页面的一小部分,如果一次性绘制图层会产生很大的开销,所以合成线程会将图层划分为图块(tile),图块大小通常是 256x256 或者 512x512

image.png
合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,生成的位图被保存在 GPU 内存中。

合成与显示

所有的图层进行光栅化之后,合成线程就会生成一个绘制图块的 DrawQuad 命令,合成线程将这个命令发给浏览器进程,由浏览器进程中的 viz 组件接收 DrawQuad 命令,它会根据命令将页面内容绘制到内存中,最后将内存中的内容显示到屏幕上。

参考文章


废废
187 声望3 粉丝