从输入url到页面展现发生了什么

发布时间 2023-11-17 16:22:57作者: 柯基与佩奇

大致是如下步骤:
根据地址栏输入的地址向 DNS(Domain Name System)查询 IP
通过 IP 向服务器发起 TCP 连接
向服务器发起请求
服务器返回请求内容
浏览器开始解析渲染页面并显示
关闭连接

一.DNS

域名解析的过程是逐级查询的
浏览器缓存: 首先会向浏览器的缓存中读取上一次访问的记录,在 chrome 可以通过地址栏中输入 chrome://net-internals/#dns 查看缓存的当前状态
操作系统缓存:查找存储在系统运行内存中的缓存。在 mac 中可以通过下面的命令清除系统中的 DNS 缓存。
dscacheutil -flushcache

  1. 在 host 文件中查找:如果在缓存中都查找不到的情况下,就会读取系统中预设的 host 文件中的设置。
  2. 路由器缓存:有些路由器也有 DNS 缓存的功能,访问过的域名会存在路由器上。
  3. ISP DNS 缓存:互联网服务提供商(如中国电信)也会提供 DNS 服务,比如比较著名的 114.114.114.114,在本地查找不到的情况下,就会向 ISP 进行查询,ISP 会在当前服务器的缓存内查找是否有记录,如果有,则返回这个 IP,若没有,则会开始向根域名服务器请求查询。
  4. 顶级 DNS 服务器/根 DNS 服务器:根域名收到请求后,会判别这个域名(.com)是授权给哪台服务器管理,并返回这个顶级 DNS 服务器的 IP。请求者收到这台顶级 DNS 的服务器 IP 后,会向该服务器发起查询,如果该服务器无法解析,该服务器就会返回下一级的 DNS 服务器 IP(nicefilm.com),本机继续查找,直到服务器找到(www.nicefilm.com)的主机。

可以通过 dig 命令查看域名解析的记录
dig math.stackexchange.com

重点看返回的应答,会看到有四条记录,返回了该网址的四个 IP

;; ANSWER SECTION:
math.stackexchange.com.    31    IN    A    151.101.1.69
math.stackexchange.com.    31    IN    A    151.101.129.69
math.stackexchange.com.    31    IN    A    151.101.193.69
math.stackexchange.com.    31    IN    A    151.101.65.69

31 是 TTL 的值,表示该域名的缓存时间,即该时间内不用重新查询。A 是该 DNS 查询的记录类型,表示返回一个 IPv4 格式的地址。还有其他记录类型诸如 NS(返回查询的服务器地址)、AAAA(返回 IPV6 格式的地址)、CNAME(域名的别名)等。

二.TCP 连接

拿到了要请求的资源服务器 IP 后,浏览器通过操作 OS 的 socket 与服务器进行 TCP 连接(一般来说操作系统已经封装好了 TCP/IP 等协议,提供套接字给应用去使用,该部分涉及到标准网络模型的知识,另外再开篇拓展。)

这个连接就是所熟知的三次握手
本机主动打开连接
第一次,本机将标识位 SYN 置为 1, seq = x(Sequence number)发送给服务端。此时本机状态为 SYN-SENT
第二次,服务器收到包之后,将状态切换为 SYN-RECEIVED,并将标识位 SYN 和 ACK 都置为 1, seq = y, ack = x + 1, 并发送给客户端。
第三次,客户端收到包后,将状态切换为 ESTABLISHED,并将标识位 ACK 置为 1,seq = x + 1, ack = y + 1, 并发送给服务端。服务端收到包之后,也将状态切换为 ESTABLISHED。

需要注意的一点是,有一些文章对 ACK 标识位 和 ack(Acknowledgement Number)的解释比较模糊,有一些画图的时候干脆就写在一起了。虽然这两者有关联,但不是同一个东西,搞清楚这个误区可以更方便去理解。还有一些会把第二次握手描述成两个包(比如某百科...),实际上这也是不正确的

标识位 ACK 置为 1 表示已确认收到 seq 为 x 的包,并回复确认序号 ack = x + 1
而 SYN 表示这是第一次随机生成 seq 的序列 x,此后每次发送的包都会在上一次发送的基础上增加 y(有数据的时候,y 是数据的长度,没有的时候 y = 1)。所以,当 seq 已初始化完成之后,没必要再把 SYN 置为 1
理解了这两点,也就不难理解为什么三次握手分别是 SYN、ACK/SYN、ACK 了。

标识位(TCP FLAG)
TCP 的头部固定有 20 个字节,其中分配了 6bits 给 TCP FLAG,组合起来用来表示当前包的类型。分别是
URGACKPSHRSTSYNFIN(CWRECE 放在保留位,暂不考虑)

URG:紧急指针,用于将要发送的包标识为“紧急”,这意味着不必等待前段数据被响应处理完即可发送给接收端。
ACK:确认标识,用于表示对数据包的成功接收。
PSH:推送标识,表示这个数据包应该被立即发送,不需要等待额外的数据。
RST:reset 标识,用来异常关闭连接。
SYN:同步标识,表示 TCP 连接已初始化。
FIN:完成标识,用于拆除上一个 SYN 标识。一个完整的 TCP 连接过程一定会有 SYN 和 FIN 包。
至此了解了一个 TCP 连接的过程,通道通了,是时候利用这个通道送东西了。
从传输层再回到应用层。

三.HTTP 请求与响应

https://www.segmentfault.com 举例子。
在应用层,浏览器会分析这个 url,并设置好请求报文发出。请求报文中包括请求行、请求头、空行、请求主体。https 默认请求端口 443, http 默认 80。

请求行:请求行中包括请求的方法,路径和协议版本。
请求头:请求头中包含了请求的一些附加的信息,一般是以键值的形式成对存在,比如设置请求文件的类型 accept-type,以及服务器对缓存的设置。
空行:协议中规定请求头和请求主体间必须用一个空行隔开
请求主体:对于 post 请求,所需要的参数都不会放在 url 中,这时候就需要一个载体了,这个载体就是请求主题。
服务端收到请求之后,会根据 url 匹配到的路径做相应的处理,最后返回浏览器需要的页面资源。浏览器会收到一个响应报文,而所需要的资源就就在报文主体上。与请求报文相同,响应报文也有与之对应的起始行、首部、空行、报文主体,不同的地方在于包含的东西不一样。

响应行:响应报文的起始行同样包含了协议版本,与请求的起始行不同的是其包含的还有状态码和状态码的原因短语。
响应头:对应请求报文中的请求头,格式一致,但是各自有不同的首部。也有一起用的通用首部。
空行
报文主体:请求所需要的资源。
http 缓存
请求是浏览器的一个优化点,可以通过缓存来减少不必要的请求,进而加快页面的呈现。通过简单地设置 http 头部可以使用缓存的功能。一般来说有三种设置的方式

Last-Modify(响应头) + If-Modified-Since(请求头)
服务器在返回资源的时候设置 Last-Modify 当前资源最后一次修改的时间,浏览器会把这个时间保存下来,在下次请求的时候,请求头部 If-Modified-Since 会包含这个时间,服务端收到请求后,会比对资源最后更新的时间是否在 If-Modified-Since 设置的时间之后,如果不是,返回 304 状态码,浏览器将从缓存中获取资源。反之返回 200 和资源内容。

ETag(响应头) + If-None-Match(请求头)
根据资源标识符来确定文件是否存在修改,服务器每一次返回资源,都会在 Etag 中存放资源的标识符,浏览器收到这个标识符,在下一次请求的时候将标识符放在 If-None-Match 中,服务端将判断是否匹配,如果不匹配,返回 200 以及新的资源,反之返回 304,浏览器从缓存中获取资源

Cache-Control/Expires(响应头)
首先这不是一种方法,而是协议更替中的一种演化。
在 http 1.0 的时代,基于 Pragma 和 Expires 控制缓存的生命周期。可以通过设置 Pragma 为 no-cache 关闭缓存功能,同样也可以在 Expires 中设置一个缓存失效的时间。需要注意的是,这个失效的时间是相对于服务器的实践而言的,如果人为地改变了客户端的时间,是会导致缓存失效的。

所以,为了解决这个问题,HTTP1.1 的协议加入了 Cache-Control,通过设置 Cache-Control 的 max-age 可以控制缓存的周期。在这个周期内,资源是新鲜的,浏览器再一次需要使用资源的时候,就不会发出请求了。

四.页面呈现

至此浏览器已经拿到了一个 HTML 文档,并为了呈现文档而开始解析。呈现引擎开始工作,基本流程如下(以 webkit 为例)

通过 HTML 解析器解析 HTML 文档,构建一个 DOM Tree,同时通过 CSS 解析器解析 HTML 中存在的 CSS,构建 Style Rules,两者结合形成一个 Attachment。
通过 Attachment 构造出一个呈现树(Render Tree)
Render Tree 构建完毕,进入到布局阶段(layout/reflow),将会为每个阶段分配一个应出现在屏幕上的确切坐标。
最后将全部的节点遍历绘制出来后,一个页面就展现出来了。

从构建 DOM 树到呈现的过程如下

op=>operation: Parsing HTML to construct the DOM tree
op1=>operation: Render Tree construction
op2=>operation: Layout of the Render Tree
op3=>operation: Painting the Render Tree
op->op1->op2->op3

需要注意的是,这是一个渐进的过程,呈现引擎为了力求显示的及时,会在文档请求不完全的情况下就开始渲染页面,同时,如果在解析的过程中遇到 script 的时候,文档的解析将会停止下来,立即解析执行脚本,如果脚本是外部的,则会等待请求完成并解析执行。所以,为了不阻塞页面地呈现,一般会把 script 脚本放在文档的最后。

在最新的 HTML4 和 HTML5 规范中,也可以将脚本标注为 defer,这样就不会停止文档解析,而是等到解析结束后才执行。HTML5 增加了一个选项,可将脚本标记为 async,以便由其他线程解析和执行。

五. 连接关闭

现在的页面为了优化请求的耗时,默认都会开启持久连接(keep-alive),那么一个 TCP 连接确切关闭的时机,是这个 tab 标签页关闭的时候。这个关闭的过程就是著名的四次挥手。关闭是一个全双工的过程,发包的顺序的不一定的。一般来说是客户端主动发起的关闭,过程如下。

假如最后一次客户端发出的数据 seq = x, ack = y;

客户端发送一个 FIN 置为 1 的包,ack = y, seq = x + 1,此时客户端的状态为 FIN_WAIT_1
服务端收到包后,状态切换为 CLOSE_WAIT 发送一个 ACK 为 1 的包, ack = x + 2。客户端收到包之后状态切换为 FNI_WAIT_2
服务端处理完任务后,向客户端发送一个 FIN 包,seq = y; 同时将自己的状态置为 LAST_ACK
客户端收到包后状态切换为 TIME_WAIT,并向服务端发送 ACK 包,ack = y + 1,等待 2MSL 后关闭连接。

为什么客户端等待 2MSL?
MSL: 全程 Maximum Segment Lifetime,中文可以翻译为报文最大生存时间。
等待是为了保证连接的可靠性,确保服务端收到 ACK 包,如果服务端没有收到这个 ACK 包,将会重发 FIN 包给客户端,而这个时间刚好是服务端等待超时重发的时间 + FIN 的传输时间。

(https://juejin.cn/post/6844903832435032072)

详细版

  1. 在浏览器地址栏输入 URL
  2. 浏览器查看缓存,如果请求资源在缓存中并且新鲜,跳转到转码步骤
    (1)如果资源未缓存,发起新请求
    (2)如果已缓存,检验是否足够新鲜,足够新鲜直接提供给客户端,否则与服务器进行验证。
    (3)检验新鲜通常有两个 HTTP 头进行控制 Expires 和 Cache-Control:
    HTTP1.0 提供 Expires,值为一个绝对时间表示缓存新鲜日期
    HTTP1.1 增加了 Cache-Control: max-age=,值为以秒为单位的最大新鲜时间
  3. 浏览器解析 URL 获取协议,主机,端口,path
  4. 浏览器组装一个 HTTP(GET)请求报文
  5. 浏览器获取主机 ip 地址,过程如下:
    (1)浏览器缓存
    (2)本机缓存
    (3)hosts 文件
    (4)路由器缓存
    (5)ISP DNS 缓存
    (6)DNS 递归查询(可能存在负载均衡导致每次 IP 不一样)
  6. 打开一个 socket 与目标 IP 地址,端口建立 TCP 链接,三次握手如下:
    (1)客户端发送一个 TCP 的 SYN=1,Seq=X 的包到服务器端口
    (2)服务器发回 SYN=1, ACK=X+1, Seq=Y 的响应包
    (3)客户端发送 ACK=Y+1, Seq=Z
  7. TCP 链接建立后发送 HTTP 请求
  8. 服务器接受请求并解析,将请求转发到服务程序,如虚拟主机使用 HTTP Host 头部判断请求的服务程序
  9. 服务器检查 HTTP 请求头是否包含缓存验证信息如果验证缓存新鲜,返回 304 等对应状态码
  10. 处理程序读取完整请求并准备 HTTP 响应,可能需要查询数据库等操作
  11. 服务器将响应报文通过 TCP 连接发送回浏览器
  12. 浏览器接收 HTTP 响应,然后根据情况选择关闭 TCP 连接或者保留重用,关闭 TCP 连接的四次握手如下:
    (1)主动方发送 Fin=1, Ack=Z, Seq= X 报文
    (2)被动方发送 ACK=X+1, Seq=Z 报文
    (3)被动方发送 Fin=1, ACK=X, Seq=Y 报文
    (4)主动方发送 ACK=Y, Seq=X 报文
  13. 浏览器检查响应状态吗:是否为 1XX,3XX, 4XX, 5XX,这些情况处理与 2XX 不同
  14. 如果资源可缓存,进行缓存
  15. 对响应进行解码(例如 gzip 压缩)
  16. 根据资源类型决定如何处理(假设资源为 HTML 文档)
  17. 解析 HTML 文档,构件 DOM 树,下载资源,构造 CSSOM 树,执行 js 脚本,这些操作没有严格的先后顺序,以下分别解释
  18. 构建 DOM 树:
    (1)Tokenizing:根据 HTML 规范将字符流解析为标记
    (2)Lexing:词法分析将标记转换为对象并定义属性和规则
    (3)DOM construction:根据 HTML 标记关系将对象组成 DOM 树
  19. 解析过程中遇到图片、样式表、js 文件,启动下载
  20. 构建 CSSOM 树:
    (1)Tokenizing:字符流转换为标记流
    (2)Node:根据标记创建节点
    (3)CSSOM:节点创建 CSSOM 树
  21. 根据 DOM 树和 CSSOM 树构建渲染树 (opens new window):
    (1)从 DOM 树的根节点遍历所有可见节点,不可见节点包括:1)script,meta 这样本身不可见的标签。2)css 隐藏的节点,如 display: none
    (2)对每一个可见节点,找到恰当的 CSSOM 规则并应用
    (3)发布可视节点的内容和计算样式
  22. js 解析如下:
    (1)浏览器创建 Document 对象并解析 HTML,将解析到的元素和文本节点添加到文档中,此时 document.readystate 为 loading
    (2)HTML 解析器遇到没有 async 和 defer 的 script 时,将他们添加到文档中,然后执行行内或外部脚本。这些脚本会同步执行,并且在脚本下载和执行时解析器会暂停。这样就可以用 document.write()把文本插入到输入流中。同步脚本经常简单定义函数和注册事件处理程序,他们可以遍历和操作 script 和他们之前的文档内容
    (3)当解析器遇到设置了 async 属性的 script 时,开始下载脚本并继续解析文档。脚本会在它下载完成后尽快执行,但是解析器不会停下来等它下载。异步脚本禁止使用 document.write(),它们可以访问自己 script 和之前的文档元素
    (4)当文档完成解析,document.readState 变成 interactive
    (5)所有 defer 脚本会按照在文档出现的顺序执行,延迟脚本能访问完整文档树,禁止使用 document.write()
    (6)浏览器在 Document 对象上触发 DOMContentLoaded 事件
    (7)此时文档完全解析完成,浏览器可能还在等待如图片等内容加载,等这些内容完成载入并且所有异步脚本完成载入和执行,document.readState 变为 complete,window 触发 load 事件
  23. 显示页面(HTML 解析过程中会逐步显示页面)