从输入URL到页面渲染出来的过程(详细)

思维导图获取

一、 URL 解析

1. 输入内容合成地址

用户输入URL,浏览器会根据用户输入的信息判断是搜索还是网址。
如果是搜索内容,就将搜索内容(会对内容进行字符编码等操作)+ 默认搜索引擎合成新的URL;
如果用户输入的内容符合URL规则,浏览器就会根据URL协议,在这段内容上加上协议合成合法的

2. HSTS(HTTP Strict Transport Security)

因为 http 存在安全隐患(比如:明文传输,http 劫持),所以建议通过 HSTS 强制客户端使用 HTTPS。
关于HSTS和http劫持可以看下这两篇文章:
你所不知道的 HSTS
什么是HTTP劫持?

3. 其他操作

  • 安全检查
    比如访问某网站出现安全警告
  • 访问限制
    之前国产浏览器限制 996.icu
  • 黑客篡改网址
    经过黑客封装的浏览器,可能会对链接做些修改,比如淘宝客的推广码换成自己的

4. 检查缓存

  • 强制缓存
    根据Cache-Control、Expires(优先使用Cache-Control)判断资源是否过期。
    过期,则走协商缓存流程;
    未过期,则从缓存中获取,不走网络请求,状态码200,Size显示disk cache或memory cache。
  • 协商缓存
    需要发起 http 请求,流程为:DNS查询 => TCP连接 => 处理请求 => 接受响应。
    服务器根据 ETag/If-None-Match、Last-Modified 和 If-Modified-Since(优先使用 ETag) 判断。
    过期:返回最新资源,状态码200
    未过期:返回缓存中的资源,状态码304。

二、DNS(Domain Name System) 查询

1. 浏览器 DNS 缓存查询

浏览器会按照一定的频率缓存 DNS 记录,所以会先检查是否在缓存中,没有则调用系统库函数进行查询。

2. 操作系统(OS)级 DNS 缓存查询

会先检查自己本地的hosts文件是否有这个网址映射关系,
如果有,就先调用这个IP地址映射,完成域名解析。
如果没有,浏览器会发出一个 DNS请求到本地DNS服务器

3. 本地 DNS 服务器查询

本地 DNS 服务器(TCP/ip 参数中设置的首选DNS服务器)会首先查询它的缓存记录,如果缓存中有此条记录,就可以直接返回结果,此过程是递归的方式进行查询。如果没有,本地DNS服务器还要向根 DNS 服务器进行查询。

本地区域名服务器通常性能都会很好,它们一般都会缓存域名解析结果,当然缓存时间是受域名的失效时间控制的,一般缓存空间不是影响域名失效的主要因素。大约90%的域名解析都到这里就已经完成了,所以LDNS主要承担了域名的解析工作。

4. 根 DNS 服务器查询

根 DNS 服务器如果没有记录具体的域名和IP地址的对应关系,会通知本地 DNS 服务器到域服务器上去继续查询,并给出域服务器的地址。这种过程是迭代的过程。

5.顶级域名(TLD)服务器查询

本地DNS服务器继续向域服务器发出请求,域服务器收到请求之后,告诉本地 DNS 服务器域名解析服务器的地址。

在前面所有步骤没有缓存的情况下,下面这个图很好的诠释了整个流程:

关于 DNS 劫持

DNS劫持方法

  • 本机DNS劫持
    攻击者通过某些手段使用户的计算机感染上木马病毒,或者恶意软件之后,恶意修改本地DNS配置,比如修改本地hosts文件,缓存等
  • 路由DNS劫持
    很多用户默认路由器的默认密码,攻击者可以侵入到路由管理员账号中,修改路由器的默认配置
  • 攻击DNS服务器
    直接攻击DNS服务器,例如对DNS服务器进行DDOS攻击,可以是DNS服务器宕机,出现异常请求,还可以利用某些手段感染dns服务器的缓存,使给用户返回来的是恶意的ip地址

DNS的防范

  • 加强本地计算机病毒检查,开启防火墙等,防止恶意软件,木马病毒感染计算机
  • 改变路由器默认密码,防止攻击者修改路由器的DNS配置指向恶意的DNS服务器
  • 企业的话可以准备两个以上的域名,一旦一个域名挂掉,还可以使用另一个
  • 用HTTP DNS 代替 Local DNS

三、HTTP请求

1.建立TCP连接

  1. 客户端发送的TCP报文中标志位SYN置1,初始序号seq=x(随机选择)。Client进入SYN_SENT状态,等待Server确认。
  2. 服务器收到数据包后,根据标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=x+1,随机产生一个初始序号seq=y,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。
  3. Client收到确认后,检查ack是否为x+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=y+1,并将该数据包发送给Server。Server检查ack是否为y+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。

2. 发起 HTTP 请求

建立起安全的加密信道后,浏览器开始发送 HTTP/HTTPS 请求,请求格式:

  • 请求报头(Request Header):请求方法、请求头、目标地址、遵循的协议等等
  • 请求主体(其他参数,Get 请求没有)

3. 返回 HTTP 响应

服务器检查客户端的报文头中是否有缓存信息If-None-Match、If-Modified-Since、ETag等,验证缓存是够有效,有效返回304和缓存中的资源,无效返回200和资源

4. 维持连接

完成一次 HTTP 请求后,服务器并不是马上断开与客户端的连接。
在 HTTP/1.1 中,Connection: keep-alive 是默认启用的,表示持久连接,以便处理不久后到来的新请求,无需重新建立连接而增加慢启动开销,提高网络的吞吐能力。
在反向代理软件 Nginx 中,持久连接超时时间默认值为 75 秒,如果 75 秒内没有新到达的请求,则断开与客户端的连接。
同时,浏览器每隔 45 秒会向服务器发送 TCP keep-alive 探测包,来判断 TCP 连接状况,如果没有收到 ACK 应答,则主动断开与服务器的连接。
注意,HTTP keep-alive 和 TCP keep-alive 虽然都是一种保活机制,但是它们完全不相同,一个作用于应用层,一个作用于传输层。

5. TCP 断开连接

  1. Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。
  2. Server收到FIN后,发送一个ACK给Client,确认序号为u + 1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。
  3. Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。
  4. Client收到FIN后,Client进入TIME_WAIT状态(主动关闭方才会进入该状态),接着发送一个ACK给Server,确认序号为w + 1,Server进入CLOSED状态,完成四次挥手。
关于 TIME_WAIT 过渡到 CLOSED 状态说明: 从 TIME_WAIT 进入 CLOSED 需要经过 2MSL,其中 MSL 就叫做 最长报文段寿命(Maxinum Segment Lifetime),根据 RFC 793 建议该值这是为 2 分钟,也就是说需要经过 4 分钟,才进入 CLOSED 状态。

为什么要等待呢?

为了这种情况: B向A发送 FIN = 1 的释放连接请求,但这个报文丢失了, A没有接到不会发送确认信息, B 超时会重传,这时A在 WAIT_TIME 还能够接收到这个请求,这时再回复一个确认就行了。(A收到 FIN = 1 的请求后 WAIT_TIME会重新记时)

另外服务器B存在一个保活状态,即如果A突然故障死机了,那B那边的连接资源什么时候能释放呢? 就是保活时间到了后,B会发送探测信息, 以决定是否释放连接

为什么连接的时候是三次握手,关闭的时候却是四次握手?

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

四、浏览器解析

Renderer 进程负责页面的解析工作,Renderer 进程通过在主线程中持有的 Blink 实例边接收边解析 HTML 内容,每次从网络缓冲区中读取 8KB 以内的数据,浏览器自上而下逐行解析 HTML 内容。

1. 解析 HTML 成 DOM 树

解码

传输回来的其实都是一些二进制字节数据,浏览器需要根据文件指定编码(例如UTF-8)转换成字符串,也就是HTML 代码。

预解析

预解析做的事情是提前加载资源,减少处理时间,它会识别一些会请求资源的属性,比如img标签的src属性,并将这个请求加到请求队列中。

符号化

符号化是词法分析的过程,将输入解析成符号,HTML 符号包括,开始标签、结束标签、属性名和属性值。

它通过一个状态机去识别符号的状态,比如遇到<,>状态都会产生变化。

构建树

注意:符号化和构建树是并行操作的,也就是说只要解析到一个开始标签,就会创建一个 DOM 节点。

在上一步符号化中,解析器获得这些标记,然后以合适的方法创建DOM对象并把这些符号插入到DOM对象中。

浏览器容错机制

你从来没有在浏览器看过类似"语法无效"的错误,这是因为浏览器去纠正错误的语法,然后继续工作。

事件

当整个解析的过程完成以后,浏览器会通过DOMContentLoaded事件来通知DOM解析完成。

2. 解析CSS 成 CSS 规则树

一旦浏览器下载了 CSS,CSS 解析器就会处理它遇到的任何 CSS,根据语法规范解析出所有的 CSS 并进行标记化,然后我们得到一个规则表。

CSS 匹配规则

在匹配一个节点对应的 CSS 规则时,是按照从右到左的顺序的,例如:div p { font-size :14px } 会先寻找所有的p标签然后判断它的父元素是否为div

所以我们写 CSS 时,尽量用 id 和 class,千万不要过度层叠。

3. 合成渲染树(render tree)

其实这就是一个 DOM 树和 CSS 规则树合并的过程。

注意:渲染树会忽略那些不需要渲染的节点,比如设置了display:none的节点。

计算

通过计算让任何尺寸值都减少到三个可能之一:auto、百分比、px,比如把rem转化为px。

级联

浏览器需要一种方法来确定哪些样式才真正需要应用到对应元素,所以它使用一个叫做 specificity 的公式,这个公式会通过:

  1. 标签名、class、id
  2. 是否内联样式
  3. !important
    然后得出一个权重值,取最高的那个。

渲染阻塞

当遇到一个script标签时,DOM 构建会被暂停,直至脚本完成执行,然后继续构建 DOM 树。

但如果 JS 依赖 CSS 样式,而它还没有被下载和构建时,浏览器就会延迟脚本执行,直至 CSS Rules 被构建。

所有我们知道:

  • CSS 会阻塞 JS 执行
  • JS 会阻塞后面的 DOM 解析

为了避免这种情况,应该以下原则:

  • CSS 资源排在 JavaScript 资源前面
  • JS 放在 HTML 最底部,也就是 前

另外,如果要改变阻塞模式,可以使用 defer 与 async,详见:为什么css要放头部,js放尾部以及async、defer该如何处理?

4. 布局与绘制(paint)

确定渲染树所有节点的几何属性,比如:位置、大小等等,最后输入一个盒子模型,它能精准地捕获到每个元素在屏幕内的准确位置与大小。

然后遍历渲染树,调用渲染器的 paint() 方法,绘制页面像素信息(本质上是一个像素填充的过程)。这个过程也出现于回流或一些不影响布局的 CSS 修改引起的屏幕局部重画,这时候它被称为重绘(Repaint)。实际上,绘制过程是在多个层上完成的,这些层我们称为渲染层(RenderLayer)。

5. 显示

1.浏览器会将各层的信息发送给GPU
2.GPU会将多个绘制后的渲染层按照恰当的重叠顺序进行合并,而后生成位图(composite),显示在屏幕上

参考:
https://www.jianshu.com/p/225cbd5ed927
https://zhuanlan.zhihu.com/p/133906695
https://zhuanlan.zhihu.com/p/80551769
https://segmentfault.com/a/1190000017184701
https://bubuzou.com/2020/12/16/browser-url/
http://www.dailichun.com/2018/03/12/whenyouenteraurl.html
https://www.jianshu.com/p/225cbd5ed927
https://www.cnblogs.com/ityouknow/p/6380603.html
https://juejin.cn/post/6844903863623876622
https://juejin.cn/post/6844903923694698504
https://juejin.cn/post/6844903966573068301
https://zhuanlan.zhihu.com/p/103162095