“在浏览器里,从输入 URL 到页面展示,这中间发生了什么? ”这是一道经典的面试题,能比较全面地考察应聘者知识的掌握程度,其中涉及到了网络、操作系统、Web 等一系列的知识。所以我在面试应聘者时也必问这道题,但遗憾的是大多数人只能回答其中部分零散的知识点,并不能将这些知识点串联成线,无法系统而又全面地回答这个问题。
那么今天我们就一起来探索下这个流程,下图是我梳理出的“从输入 URL 到页面展示完整流程示意图”:
从图中可以看出,整个过程需要各个进程之间的配合,所以在开始正式流程之前,我们还是先来快速回顾下浏览器进程、渲染进程和网络进程的主要职责。
- 浏览器进程主要负责用户交互、子进程管理和文件储存等功能。
- 网络进程是面向渲染进程和浏览器进程等提供网络下载功能。
- 渲染进程的主要职责是把从网络下载的 HTML、JavaScript、CSS、图片等资源解析为可以显示和交互的页面。因为渲染进程所有的内容都是通过网络获取的,会存在一些恶意代码利用浏览器漏洞对系统进行攻击,所以运行在渲染进程里面的代码是不被信任的。这也是为什么 Chrome 会让渲染进程运行在安全沙箱里,就是为了保证系统的安全。
回顾了浏览器的进程架构后,我们再结合上图来看下这个完整的流程,可以看出,整个流程包含了许多步骤,我把其中几个核心的节点用蓝色背景标记出来了,接下来就一一讲解。
用户输入
当用户在地址栏中输入一个查询关键字时,地址栏会判断输入的关键字是搜索内容,还是请求的 URL。
- 如果是搜索内容,地址栏会使用浏览器默认的搜索引擎,来合成新的带搜索关键字的 URL。
- 如果判断输入内容符合 URL 规则,比如输入的是 www.kaola.com,那么地址栏会根据规则,把这段内容加上协议,合成为完整的 URL,如 https://www.kaola.com。
开始导航之前
当用户输入关键字并键入回车之后,这意味着当前页面即将要被替换成新的页面,不过在这个流程继续之前,浏览器还给了当前页面一次执行 beforeunload 事件的机会,beforeunload 事件允许页面在退出之前执行一些数据清理操作,还可以询问用户是否要离开当前页面,比如当前页面可能有未提交完成的表单等情况,因此用户可以通过 beforeunload 事件来取消导航,让浏览器不再执行任何后续工作。
当前页面没有监听 beforeunload 事件或者同意了继续后续流程,那么浏览器便进入下图的状态:
开始加载 URL 浏览器状态
从图中可以看出,当浏览器刚开始加载一个地址之后,标签页上的图标便进入了加载状态。但此时图中页面显示的依然是之前打开的页面内容,并没立即替换为极客时间的页面。因为需要等待提交文档阶段,页面内容才会被替换。
URL 请求过程
接下来,便进入了页面资源请求过程。这时,浏览器进程会通过进程间通信(IPC)把 URL 请求发送至网络进程。
关于进程通讯 进程之间的内容相互隔离。进程隔离是为保护操作系统中进程互不干扰的技术,每一个进程只能访问自己占有的数据,也就避免出现进程 A 写入数据到进程 B 的情况。正是因为进程之间的数据是严格隔离的,所以一个进程如果崩溃了,或者挂起了,是不会影响到其他进程的。如果进程之间需要进行数据的通信,这时候,就需要使用用于进程间通信(IPC)的机制了。
浏览器进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程A把数据从用户空间拷到内核缓冲区,进程B再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC)
浏览器进程会通过进程间通信(IPC)把 URL 请求发送至网络进程。网络进程接收到 URL 请求后,会在这里发起真正的 URL 请求流程。那具体流程是怎样的呢?
首先,网络进程会查找本地dns缓存是否缓存了该资源。如果有缓存资源,那么直接返回资源给浏览器进程;如果在缓存中没有查找到资源,那么直接进入网络请求流程。这请求前的第一步是要进行 DNS 解析。
DNS服务器
在网络世界,你肯定记得住网站的名称,但是很难记住网站的IP地址,因此就需要有一个地址簿来记录这些网站的IP,这就是DNS服务器。但是每个人都上网,都需要访问它,这对它来讲也是一个非常大的挑战,一旦它出了故障整个互联网都将瘫痪。 另外上网的人分布在全世界各地,如果大家都去访问某一台服务器,延时将会非常大。 因此DNS服务器一定要设计成高可用,高并发,分布式。
于是就有了这样树状层次结构。
- 根DNS服务器: 返回顶级域DNS服务器的IP地址
- 顶级域DNS服务器: 返回权威DNS服务器的IP地址
- 权威DNS服务器: 返回相应主机的IP地址
DNS 解析流程
当电脑客户端会发出一个DNS请求,问www163.com的IP是啥啊,并发给本地域名服务器(本地DNS)。那本地域名服务器(本地DNS)是什么呢?如果是通过DHCP配置,本地 DNS由你的网络服务商(ISP),如电信、移动等自动分配,它通常就在你网络服务商的某个机房。
2.本地DNS收到来自客户端的请求。你可以想象这台服务器上缓存了一张域名与之对应IP地址的大表格。如果能找到www163com,它直接就返回IP地址。如果没有,本地DNS会去问它的根域名服务器:“老大,能告诉我www.163.com的IP 地址吗?”根域名服务器是最高层次的,全球共有13套。它不直接用于域名解析,但能指明一条道路。
3.根DNS收到来自本地DNS的请求,发现后缀是.com,说:“哦,www.163.com啊,这个域名是由com区域管理,我给你它的顶级域名服务器的地址,你去问问它吧。4.本地DNS转向问顶级域名服务器:“老二,你能告诉我www163.com的IP地址吗?”顶级域名服务器就是大名鼎鼎的比如.com、.net 、 .org这些一级域名,它负责管理二级域名,比如163.com,所以它能提供一条更清晰的方向。
5.顶级域名服务器说:“我给你负责www.163.com区域的权威DNS服务器的地址,你去问它应该能问到。
6.本地DNS 转向问权威 DNS 服务器: “您好,www163.com对应的IP是啥呀?”163.com的权威DNS服务器,它是域名解析结果的原出处。为啥叫权威呢?就是我的域名我做主。
7.权限DNS服务器查询后将对应的IP 地址XX.X.X告诉本地DNS。
8.本地DNS再将IP地址返回客户端,客户端和目标建立连接。
至此我们完成了DNS的解析过程。现在总结下,整个过程我画了一张图。
接下来就是利用 IP 地址和服务器建立 TCP 连接。
客户端与服务器之间数据的发送和返回的都是需要靠 TCP connection (连接) TCP更像一个管道 把请求和响应的数据包在客户端和服务端来回传递,这个连接可以一直保持,HTTP请求是在这个连接的基础上发送的;
连接建立之后,浏览器端会构建请求行、请求头等信息,并把和该域名相关的 Cookie 等数据附加到请求头中,然后向服务器发送构建的请求信息。
服务器接收到请求信息后,会根据请求信息生成响应数据(包括响应行、响应头和响应体等信息),并发给网络进程。等网络进程接收了响应行和响应头之后,就开始解析响应头的内容了。
响应数据类型处理
URL 请求的数据类型,有时候是一个下载类型,有时候是正常的 HTML 页面,那么如何区分它们呢?
答案是 Content-Type。Content-Type 是 HTTP 头中一个非常重要的字段, 它告诉客户端服务器返回的响应体数据是什么类型,然后客户端会根据 Content-Type 的值来决定如何显示响应体的内容。
响应头中的 Content-type 字段的值是 text/html,这就是告诉浏览器,服务器返回的数据是 HTML 格式。
如果响应头中的 Content-Type 的值是 application/octet-stream,显示数据是字节流类型的,浏览器会按照下载类型来处理该请求。
当浏览器判断为下载类型,那么该请求会被提交给浏览器的下载管理器,同时该 URL 请求的导航流程就此结束。但如果是 HTML,那么浏览器则会继续进行导航流程。由于 Chrome 的页面渲染是运行在渲染进程中的,所以接下来就需要准备渲染进程了。
用户发出 URL 请求到页面开始解析的这个过程,就叫做导航。
准备渲染进程
默认情况下,Chrome 会为每个页面分配一个渲染进程,也就是说,每打开一个新页面就会配套创建一个新的渲染进程。渲染进程准备好之后,还不能立即进入文档解析状态,因为此时的文档数据还在网络进程中,并没有提交给渲染进程,所以下一步就进入了提交文档阶段。
提交文档
所谓提交文档,就是指浏览器进程将网络进程接收到的 HTML 数据提交给渲染进程,具体流程是这样的:
- 首先当浏览器进程接收到网络进程的响应头数据之后,便向渲染进程发起“提交文档”的消息;
- 渲染进程接收到“提交文档”的消息后,会和网络进程建立传输数据的“管道”;
- 等文档数据传输完成之后,渲染进程会返回“确认提交”的消息给浏览器进程;
- 浏览器进程在收到“确认提交”的消息后,会更新浏览器界面状态,包括了安全状态、地址栏的 URL、前进后退的历史状态,并更新 Web 页面。
这也就解释了为什么在浏览器的地址栏里面输入了一个地址后,之前的页面没有立马消失,而是要加载一会儿才会更新页面。到这里,一个完整的导航流程就“走”完了,这之后就要进入渲染阶段了。
渲染阶段一旦文档被提交,渲染进程便开始页面解析和子资源加载了,关于这个阶段的完整过程,我会在下节课介绍。
重定向
在接收到服务器返回的响应头后,网络进程开始解析响应头,如果发现返回的状态码是 301 或者 302,那么说明服务器需要浏览器重定向到其他 URL。这时网络进程会从响应头的 Location 字段里面读取重定向的地址,然后再发起新的 HTTP 或者 HTTPS 请求,一切又重头开始了。