从输入url到页面展现发生了什么
大致是如下步骤:
根据地址栏输入的地址向 DNS(Domain Name System)查询 IP
通过 IP 向服务器发起 TCP 连接
向服务器发起请求
服务器返回请求内容
浏览器开始解析渲染页面并显示
关闭连接
一.DNS
域名解析的过程是逐级查询的
浏览器缓存: 首先会向浏览器的缓存中读取上一次访问的记录,在 chrome 可以通过地址栏中输入 chrome://net-internals/#dns 查看缓存的当前状态
操作系统缓存:查找存储在系统运行内存中的缓存。在 mac 中可以通过下面的命令清除系统中的 DNS 缓存。
dscacheutil -flushcache
- 在 host 文件中查找:如果在缓存中都查找不到的情况下,就会读取系统中预设的 host 文件中的设置。
- 路由器缓存:有些路由器也有 DNS 缓存的功能,访问过的域名会存在路由器上。
- ISP DNS 缓存:互联网服务提供商(如中国电信)也会提供 DNS 服务,比如比较著名的 114.114.114.114,在本地查找不到的情况下,就会向 ISP 进行查询,ISP 会在当前服务器的缓存内查找是否有记录,如果有,则返回这个 IP,若没有,则会开始向根域名服务器请求查询。
- 顶级 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)
详细版
- 在浏览器地址栏输入 URL
- 浏览器查看缓存,如果请求资源在缓存中并且新鲜,跳转到转码步骤
(1)如果资源未缓存,发起新请求
(2)如果已缓存,检验是否足够新鲜,足够新鲜直接提供给客户端,否则与服务器进行验证。
(3)检验新鲜通常有两个 HTTP 头进行控制 Expires 和 Cache-Control:
HTTP1.0 提供 Expires,值为一个绝对时间表示缓存新鲜日期
HTTP1.1 增加了 Cache-Control: max-age=,值为以秒为单位的最大新鲜时间 - 浏览器解析 URL 获取协议,主机,端口,path
- 浏览器组装一个 HTTP(GET)请求报文
- 浏览器获取主机 ip 地址,过程如下:
(1)浏览器缓存
(2)本机缓存
(3)hosts 文件
(4)路由器缓存
(5)ISP DNS 缓存
(6)DNS 递归查询(可能存在负载均衡导致每次 IP 不一样) - 打开一个 socket 与目标 IP 地址,端口建立 TCP 链接,三次握手如下:
(1)客户端发送一个 TCP 的 SYN=1,Seq=X 的包到服务器端口
(2)服务器发回 SYN=1, ACK=X+1, Seq=Y 的响应包
(3)客户端发送 ACK=Y+1, Seq=Z - TCP 链接建立后发送 HTTP 请求
- 服务器接受请求并解析,将请求转发到服务程序,如虚拟主机使用 HTTP Host 头部判断请求的服务程序
- 服务器检查 HTTP 请求头是否包含缓存验证信息如果验证缓存新鲜,返回 304 等对应状态码
- 处理程序读取完整请求并准备 HTTP 响应,可能需要查询数据库等操作
- 服务器将响应报文通过 TCP 连接发送回浏览器
- 浏览器接收 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 报文 - 浏览器检查响应状态吗:是否为 1XX,3XX, 4XX, 5XX,这些情况处理与 2XX 不同
- 如果资源可缓存,进行缓存
- 对响应进行解码(例如 gzip 压缩)
- 根据资源类型决定如何处理(假设资源为 HTML 文档)
- 解析 HTML 文档,构件 DOM 树,下载资源,构造 CSSOM 树,执行 js 脚本,这些操作没有严格的先后顺序,以下分别解释
- 构建 DOM 树:
(1)Tokenizing:根据 HTML 规范将字符流解析为标记
(2)Lexing:词法分析将标记转换为对象并定义属性和规则
(3)DOM construction:根据 HTML 标记关系将对象组成 DOM 树 - 解析过程中遇到图片、样式表、js 文件,启动下载
- 构建 CSSOM 树:
(1)Tokenizing:字符流转换为标记流
(2)Node:根据标记创建节点
(3)CSSOM:节点创建 CSSOM 树 - 根据 DOM 树和 CSSOM 树构建渲染树 (opens new window):
(1)从 DOM 树的根节点遍历所有可见节点,不可见节点包括:1)script,meta 这样本身不可见的标签。2)css 隐藏的节点,如 display: none
(2)对每一个可见节点,找到恰当的 CSSOM 规则并应用
(3)发布可视节点的内容和计算样式 - 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 事件 - 显示页面(HTML 解析过程中会逐步显示页面)
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性