在浏览器中从 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
进行访问;
缓存
缓存内容详见:前端缓存
- 浏览器发送请求前,会根据 HTTP 请求头部的字段
expires | cache-control
判断是否命中强缓存,如果命中,则直接从缓存获取所需资源,不会发送请求到服务器端,此时资源请求响应的状态码是200(from cache)
,否则进入下一步; - 没有命中强缓存,浏览器会根据请求头的
last-modified | etag
来判断是否命中协商缓存,如果命中,会从缓存中获取资源,此时资源请求响应的状态码是304(not modified)
,否则进入下一步;需要注意的是,这一步浏览器会发送请求到服务器端; - 如果前两步都么有命中,则会从服务器端获取资源。
DNS 域名解析
在发起 HTTP 请求之前,浏览器首先要获取域名对应的 ip 地址,浏览器会发送一个 UDP 的包给 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 收到后确认seq
和ack
则建立连接成功;(第三次握手由浏览器发送,告诉服务器,我马上就要发送数据包了)
为什么需要三次握手
由于 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 端口上,工作流程如下:
- TCP 三次握手;
- 客户端验证服务器的证书;
- DH 算法协商对称加密算法的密钥,hash 算法的密钥;
- SSL 安全加密隧道协商完成;
- 数据以对称加密的方式传输,用协商好的加密算法和密钥进行加密;用协商好的 hash 算法对数据进行完整性保护,保证不被篡改;
握手过程:
-
客户端向服务端发送 Client Hello 消息,其中携带客户端支持的协议版本、加密算法、压缩算法以及客户端生成的随机数
X
;服务端收到客户端支持的协议版本、加密算法等信息后; -
向客户端发送 Server Hello 消息,并携带选择特定的协议版本、加密方法、会话 ID 以及服务端生成的随机数
Y
;- 向客户端发送 Certificate 消息,即服务端的证书链,其中包含证书支持的域名、发行方和有效期等信息;
- 向客户端发送 Server Key Exchange 消息,传递公钥以及签名等信息;
- 向客户端发送可选的消息 Certificate Request,验证客户端的证书;
- 向客户端发送 Server Hello Done 消息,通知服务端已经发送了全部的相关信息;
-
客户端收到服务端的协议版本、加密方法、会话 ID 以及证书等信息后,验证服务端的证书,随机数
Z
;- 向服务端发送 Client Key Exchange 消息,包含使用服务端公钥加密
Z
后的随机字符串,即预主密钥(Pre Master Secret); - 向服务端发送 Change Cipher Spec 消息,通知服务端后面的数据段会加密传输;
- 向服务端发送 Finished 消息,其中包含加密后的握手信息;
- 向服务端发送 Client Key Exchange 消息,包含使用服务端公钥加密
-
服务端收到 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 树 - 李银城的文章 - 知乎
- 字节转换成字符:由于读取 HTML 文件的原始字节要根据文件编码转换成可识别的字符,所以有第一步;
- 将 HTML 字符串转换成令牌,每个令牌都有特殊含义和一组规则,为了更好的做词法分析;
- DOM 树构建:我们知道 HTML 标记间存在父子关系,所以很适合建立起树形结构,有了令牌后,就可以根据令牌结构生成 DOM 树。
CSS 对象模型(CSSOM)
其构建过程与 DOM 树的过程类似;
布局树
- DOM 树与 CSSOM 树合并后形成 render tree;
- 渲染树只包含渲染页面需要的节点;
- 布局计算每个对象的精确位置和大小;
- 最后一步是绘制,把最终渲染树渲染到屏幕上。
渲染
- 获取 DOM 后分割成多个图层;
- 对每个图层的节点计算样式结果(Recalculate style–样式重计算);
- 为每个节点生成图形和位置(Layout–重排,回流);
- 将每个节点绘制填充到图层位图中 (Paint–重绘)
- 图层作为纹理上传至 GPU
- 组合多个图层到页面上生成最终屏幕图像 (Composite Layers–图层重组)
回流、重绘
- 重绘(repaint)
页面样式改变,且不影响它在文档流中的位置时,不影响布局,比如(color,background-color,visibility 等),浏览器会将新的样式赋予给元素并重新绘制它,这就是重绘;
- 回流(reflow)
当渲染树中部分或全部元素的尺寸,结构,或某些属性发生改变时,浏览器重新渲染部分或者全部文档的过程,称为回流;每个页面至少需要一次回流,因为要构建渲染树;在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程称为重绘。
回流必会引起重绘,而重绘不一定引起回流
引起回流的情况:
- 页面首次渲染;
- 浏览器窗口大小发生改变
- 元素尺寸或者位置发生改变
- 元素内容变化(文字数量或图片大小)
- 元素字体大小变化
- 添加或者删除可见的 DOM 元素
- 激活 CSS 伪类(::hover)
- 查询某些属性或者调用某些方法
引起回流的属性和相关方法:
- clientWidth、clientHeight、clientTop、clientLeft
- offsetWidth、offsetHeight、offsetTop、offsetLeft
- scrollWidth、scrollHeight、scrollTop、scrollLeft
- scrollIntoView()、scrollIntoViewIffNeeded()
- getComputedStyle()
- getBoundingClientRect()
- scrollTo()
减少回流的方式:
-
CSS
- 避免使用 table 布局;
- 尽可能在 DOM 数的最末端改变 class
- 避免设置多层内联样式;
- 将动画效果应用到 position 属性为 absolute 或 fixed 元素上;
- 避免使用 CSS 表达式(calc())
-
JS
- 避免频繁操作样式,最好一次性重写 style,或者将样式列表定义为 class 并一次性更改 class;
- 避免频繁操作 DOM,创建一个 documentFragment,在它上面应用所有 DOM 操作,最后再添加到文档中;
- 也可以先为元素设置
display: none
,操作结束后再把它显示出来;设置这个属性后,在元素上进行的 DOM 操作不会引起回流和重绘; - 避免频繁读取会引发回流、重绘的属性,如果确实需要多次使用,就要用一个变量缓存起来;
- 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素以及后续元素的频繁回流;
断开连接:TCP 四次挥手
- 刚开始双方都处于 established 状态,假如是客户端先发起关闭请求
- 第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1 状态
- 第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值+1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态
- 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发送 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态
- 需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。
挥手为什么需要四次?
因为当服务端收到客户端的 SYN 连接请求报文后,可以直接发送 SYN+ACK 报文。其中 ACK 报文是用来应答的,SYN 报文是用来同步的。但是关闭连接时,当服务端收到 FIN 报文时,很可能并不会立即关闭 SOCKET,所以只能先回复一个 ACK 报文,告诉客户端,“你发的 FIN 报文我收到了”。只有等到我服务端所有的报文都发送完了,我才能发送 FIN 报文,因此不能一起发送。故需要四次挥手。
四次挥手释放连接时,等待 2MSL 的意义?
MSL(最长报文周期)
- 保证客户端发送的最后一个 ACK 报文段能够到达服务端。
这个 ACK 报文段有可能丢失,使得处于 LAST-ACK 状态的 B 收不到对已发送的 FIN+ACK 报文段的确认,服务端超时重传 FIN+ACK 报文段,而客户端能在 2MSL 时间内收到这个重传的 FIN+ACK 报文段,接着客户端重传一次确认,重新启动 2MSL 计时器,最后客户端和服务端都进入到 CLOSED 状态,若客户端在 TIME-WAIT 状态不等待一段时间,而是发送完 ACK 报文段后立即释放连接,则无法收到服务端重传的 FIN+ACK 报文段,所以不会再发送一次确认报文段,则服务端无法正常进入到 CLOSED 状态。
- 防止“已失效的连接请求报文段”出现在本连接中。
客户端在发送完最后一个 ACK 报文段后,再经过 2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现这种旧的连接请求报文段。
参考
感谢:https://juejin.cn/post/6935232082482298911