Browser - 浏览器工作原理
Browser - 浏览器工作原理
浏览器(也称为网络浏览器或互联网浏览器)是安装在我们设备上的软件应用程序,使我们能够访问万维网。
有许多浏览器正在被使用,截至2022年,使用最多的是:谷歌浏览器、苹果的Safari、微软的Edge和火狐。
但是,它们实际上是如何工作的,从我们在地址栏中键入网络地址开始,到我们试图访问的页面显示在屏幕上,会发生什么?
关于这个问题的答案,一个极其简化的版本是:
当我们从一个特定的网站请求一个网页时,浏览器从网络服务器检索必要的内容,然后在我们的设备上显示该网页。
在这个看似超级简单的过程中还涉及更多的内容。在这个系列中,我们将讨论导航
、获取数据
、解析
和渲染
等步骤,并希望能使你对这些概念更清晰。
导航
导航是加载网页的第一步。它指的是当用户通过点击一个链接、在浏览器地址栏中写下一个网址、提交一个表格等方式请求一个网页时发生的过程。
DNS 查询(解决网址问题)
导航到一个网页的第一步是找到该网页的静态资源位置(HTML、CSS、Javascript和其他类型的文件)。如果我们导航到 example.com ,HTML 页面位于 IP 地址为 93.184.216.34 的服务器上(对我们来说,网站是域名,但对计算机来说,它们是 IP 地址)。如果我们以前从未访问过这个网站,就必须进行域名系统(DNS)查询。微信搜索公众号:架构师指南,回复:架构师 领取资料 。
DNS 服务器是包含公共 IP 地址及其相关主机名数据库的计算机服务器(这通常被比作电话簿,因为人们的名字与一个特定的电话号码相关联)。在大多数情况下,这些服务器按照要求将这些名字解析或翻译成 IP 地址(现在有 600 多个不同的 DNS 根服务器分布在世界各地)。
因此,当我们请求进行 DNS 查询时,我们实际做的是与这些服务器中的一个进行对话,要求找出与 example.com 名称相对应的IP地址。如果找到了一个对应的 IP,就会返回。如果发生了一些情况,查找不成功,我们会在浏览器中看到一些错误信息。
在这个最初的查询之后,IP 地址可能会被缓存一段时间,所以下次访问同一个网站会更快,因为不需要进行 DNS 查询(记住,DNS 查询只发生在我们第一次访问一个网站时)。
TCP (Transmission Control Protocol) 握手
一旦浏览器知道了网站的 IP 地址,它将尝试通过 TCP 三次握手(也称为 SYN-SYN-ACK,或者更准确的说是 SYN、SYN-ACK、ACK,因为 TCP 有三个消息传输,用于协商和启动两台计算机之间的TCP 会话),与持有资源的服务器建立连接。
TCP 是传输控制协议的缩写,是一种通信标准,使应用程序和计算设备能够在网络上交换信息。它被设计用来在互联网上发送数据包,并确保数据和信息在网络上成功传递。
TCP 握手是一种机制,旨在让两个想要相互传递信息的实体(在我们的例子中是浏览器和服务器)在传输数据之前协商好连接的参数。
因此,如果浏览器和服务器是两个人,他们之间的对话会是这样的:
浏览器向服务器发送一个 SYNC 消息,要求进行同步(同步意味着连接)
然后,服务器将回复一个 SYNC-ACK 消息( SYNChronization 和 ACKnowledgement)
在最后一步,浏览器将回复一个 ACK 信息
现在,TCP连接(双向连接)已经通过3次握手建立,TLS协商可以开始。
TLS协商
对于通过 HTTPS 建立的安全连接,需要进行另一次握手。这种握手(TLS协商)决定了哪个密码将被用于加密通信,验证服务器,并在开始实际的数据传输之前建立一个安全的连接。
传输层安全(TLS)是现已废弃的安全套接字层(SSL)的继任者,是一种加密协议,旨在通过计算机网络提供通信安全。该协议被广泛用于电子邮件和即时通讯等应用,但它在确保 HTTPS安全方面的应用仍然是最公开的。由于应用程序可以使用或不使用 TLS(或SSL)进行通信,因此客户(浏览器)有必要要求服务器建立 TLS 连接。
在这一步骤中,浏览器和服务器之间还交换了一些信息
- 客户端 hello。浏览器向服务器发送一条信息,其中包括它所支持的TLS版本和密码套件,以及一串随机字节,称为 客户端随机数。
- 服务器 hello 和证书。服务器发回一条信息,其中包括服务器的SSL证书、服务器选择的密码套件和服务器随机数,这是服务器生成的另一个随机字节串。
- 认证。浏览器会向颁发证书的机构核实服务器的 SSL 证书。这样,浏览器就可以确定服务器就是它所说的那个人。
- 预主密码。浏览器会再发送一串随机的字节,称为主密钥,用浏览器从服务器的 SSL 证书上获取的公钥进行加密。主密码只能由服务器用私钥解密。
- 使用私钥。服务器解密预主密码。
- 创建会话密钥。浏览器和服务器从客户端随机数、服务器随机数和预主密码中生成会话密钥。
- 客户端完成。浏览器向服务器发送一个消息,说它已经完成。
- 服务器完成。服务器向浏览器发送一个消息,表示它也完成了。
- 安全对称加密实现。握手完成,通信可以继续使用会话密钥。
现在可以开始从服务器请求和接收数据了
获取数据
在上一节中,我们谈到了导航,这是浏览器显示网站的第一步。现在,我们将进入下一个步骤,看看如何获取资源。
HTTP 请求
在我们与服务器建立安全连接后,浏览器将发送一个初始的 HTTP GET 请求。首先,浏览器将请求页面的 HTML 文件。它将使用 HTTP 协议来做这件事。
HTTP(超文本传输协议)是一个获取资源的协议,如HTML文件。它是网络上任何数据交换的基础,它是一个客户-服务器协议,这意味着请求是由接收者发起的,通常是网络浏览器。
请求方法 - POST, GET, PUT, PATCH, DELETE 等
URI - 是统一资源识别符的缩写。URIs 用于识别互联网上的抽象或物理资源,如网站或电子邮件地址等资源。一个 URI 最多可以有 5 个部分
scheme:用于说明使用的是什么协议
authority:用于识别域名
path:用于显示资源的确切路径
query:用于表示一个请求动作
fragment:用来指代资源的一部分
// URI parts
scheme :// authority path ? query # fragment
//URI example
<https://example.com/users/user?name=Alice#address>
https: // scheme name
example.com // authority
users/user // path
name=Alice // query
address // fragment
HTTP 头字段 - 是浏览器和服务器在每个 HTTP 请求和响应中发送和接收的字符串列表(它们通常对终端用户是不可见的)。在请求的情况下,它们包含关于要获取的资源或请求资源的浏览器的更多信息。
如果你想看看这些请求头字段是什么样子的,请进入 Chrome 浏览器并打开开发者工具(F12)。进入 Network 标签,选择 FETCH/XHR。在下面的屏幕截图中,我刚刚在搜索引擎上搜索了Palm Springs,这就是请求头的样子。
HTTP 响应
一旦服务器收到请求,它将对其进行处理并回复一个 HTTP 响应。在响应的正文中,我们可以找到所有相关的响应头和我们请求的HTML文档的内容
状态代码 - 例如:200、400、401、504网关超时等(我们的目标是 200 状态代码,因为它告诉我们一切正常,请求是成功的)
响应头字段 - 保存关于响应的额外信息,如它的位置或提供它的服务器。
一个 HTML 文档的例子可以是这样的
<!doctype HTML>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>我的页面</title>
<link rel="stylesheet" src="styles.css"/>
<script src="mainScripts.js"></script>
</head>
<body>
<h1 class="heading">这个是我的页面</h1>
<p>一个段落和一个 <a href="<https://example.com/about>">链接</a></p>
<div>
<img src="myImage.jpg" alt="image description"/>
</div>
<script src="sideEffectsScripts.js"></script>
</body>
</html>
对于我前面提到的同一个搜索,响应头是这样的
如果我们看一下HTML文档,我们会发现它引用了不同的 CSS 和 Javascript 文件。这些文件不会被请求。在这个时候,只有 HTML 被请求并从服务器接收。
这个初始请求的响应包含收到的第一个字节的数据。第一个字节的时间(TTFB)是指从用户提出请求(在地址栏中输入网站名称)到收到第一个 HTML 数据包(通常为14kb)的时间。
TCP 慢启动和拥塞算法
TCP 慢启动 是一种平衡网络连接速度的算法。第一个数据包将是 14kb(或更小),其工作方式是逐渐增加传输的数据量,直到达到预定的阈值。从服务器接收到每个数据包后,客户端以 ACK 消息响应。由于连接容量有限,如果服务器发送太多数据包太快,它们将被丢弃。客户端不会发送任何 ACK 消息,因此服务器会将此解释为拥塞。这就是拥塞算法发挥作用的地方。他们监控发送的数据包和 ACK 消息的流,以确定流量的最佳速率并创建稳定的流量流。
HTML 解析
到目前为止,我们讨论了导航和数据获取。今天我们将讨论解析,特别是 HTML 解析。
我们看到在向服务器发出初始请求后,浏览器如何收到包含我们尝试访问的网页的 HTML 资源(第一块数据)的响应。现在浏览器的工作就是开始解析数据。
解析是指将程序分析并转换为运行时环境实际可以运行的内部格式
换句话说,解析意味着将我们编写的代码作为文本(HTML、CSS)并将其转换为浏览器可以使用的内容。解析将由浏览器引擎完成(不要与浏览器的 Javascript 引擎混淆)。
浏览器引擎是每个主要浏览器的核心组件,它的主要作用是结合结构 (HTML) 和样式 (CSS),以便它可以在我们的屏幕上绘制网页。它还负责找出哪些代码片段是交互式的。我们不应将其视为一个单独的软件,而应将其视为更大软件(在我们的例子中为浏览器)的一部分。
有许多浏览器引擎,但大多数浏览器使用这三个活跃且完整引擎之一:
Gecko 它是由 Mozilla 为 Firefox 开发的。过去,它曾为其他几种浏览器提供支持,但目前,除了 Firefox,Tor 和 Waterfox 是唯一仍在使用 Gecko 的浏览器。它是用 C++ 和 JavaScript 编写的,自 2016 年起,还用 Rust 编写。
WebKit 它主要由 Apple 为 Safari 开发。它还为 GNOME Web (Epiphany) 和 Otter 提供支持。(令人惊讶的是,在 iOS 上,包括 Firefox 和 Chrome 在内的所有浏览器也由 WebKit 提供支持)。它是用 C++ 编写的。
Blink,Chromium 的一部分 它最初是 WebKit 的一个分支,主要由 Google 为 Chrome 开发。它还为 Edge、Brave、Silk、Vivaldi、Opera 和大多数其他浏览器项目(一些通过 QtWebEngine)提供支持。它是用 C++ 编写的。
现在我们了解了谁将进行解析,让我们看看在从服务器接收到第一个 HTML 文档后到底发生了什么。让我们假设文档如下所示:
<!doctype HTML>
<html>
<head>
<title>This is my page</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<h1>This is my page</h1>
<h3>This is a H3 header.</h3>
<p>This is a paragraph.</p>
<p>This is another paragraph,</p>
</body>
</html>
即使请求页面的 HTML 大于初始的 14KB 数据包,浏览器也会开始解析并尝试根据其拥有的数据呈现体验。HTML 解析涉及两个步骤:词法分析 和 树构造(构建称为 DOM 树的东西)。
词法分析
它将一些输入转换为标签(源代码的基本组件)。想象一下,我们将一段英文文本分解成单词,其中单词就是标签。
词法分析过程结束时的结果是一系列 0 个或多个以下标签:DOCTYPE、开始标签 (
构建 DOM
创建第一个 token 后,树构建开始。这实质上是基于先前解析的标签创建树状结构(称为文档对象模型)。
DOM 树描述了 HTML 文档的内容。 元素是文档树的第一个标签和根节点。树反映了不同标签之间的关系和层次结构。我们有父节点,嵌套在其他标签中的标签是子节点。节点数越多,构建 DOM 树所需的时间就越长。下面是我们从服务器获得的 HTML 文档示例的 DOM 树:
实际上,DOM 比我们在该模式中看到的更复杂,但我保持简单以便更好地理解。
此构建阶段是可重入的,这意味着在处理一个 token 时,分词器可能会恢复,导致在第一个 token 处理完成之前触发并处理更多 token。从字节到创建 DOM,整个过程如下所示:
解析器从上到下逐行工作。当解析器遇到非阻塞资源(例如图像)时,浏览器会向服务器请求这些图像并继续解析。另一方面,如果它遇到阻塞资源(CSS 样式表、在 HTML 的 部分添加的 Javascrpt 文件或从 CDN 添加的字体),解析器将停止执行,直到所有这些阻塞资源都被下载。这就是为什么,如果你正在使用 Javascript,建议在 HTML 文件的末尾添加
预加载器 & 使页面更快
Internet Explorer、WebKit 和 Mozilla 都在 2008 年实现了预加载器,作为处理阻塞资源的一种方式,尤其是脚本(我们之前说过,当遇到脚本标签时,HTML 解析将停止,直到脚本被下载并执行)。
使用预加载器,当浏览器卡在脚本上时,第二个较轻的解析器会扫描 HTML 以查找需要检索的资源(样式表、脚本等)。然后预加载器开始在后台检索这些资源,目的是在主 HTML 解析器到达它们时它们可能已经被下载(如果这些资源已经被缓存,则跳过此步骤)。
解析 CSS
解析完 HTML 之后,就该解析 CSS(在外部 CSS 文件和样式元素中找到)并构建 CSSOM 树(CSS 对象模型)。
当浏览器遇到 CSS 样式表时,无论是外部样式表还是嵌入式样式表,它都需要将文本解析为可用于设置布局样式的内容。浏览器将 CSS 变成的数据结构称为 CSSOM。DOM 和 CSSOM 遵循相似的概念,因为它们都是树,但它们是不同的数据结构。就像从我们的 HTML 构建 DOM 一样,从 CSS 构建 CSSOM 被认为是一个「渲染阻塞 」过程。
词法分析和构建 CSSOM
与 HTML 解析类似,CSS 解析从词法分析开始。CSS 解析器获取字节并将它们转换为字符,然后是标签,然后是节点,最后它们被链接到 CSSOM 中。浏览器会执行一些称为选择器匹配的操作,这意味着每组样式都将与页面上的所有节点(元素)匹配。
浏览器从适用于节点的最通用规则开始(例如:如果节点是 body 元素的子节点,则所有 body 样式都由该节点继承),然后通过应用更具体的规则递归地优化计算出的样式。这就是为什么我们说样式规则是级联的。
假设我们有下面的 HTML 和 CSS:
body {
font-size: 16px;
color: white;
}
h1 {
font-size: 32px;
}
section {
color: tomato;
}
section .mainTitle {
margin-left: 5px
}
div {
font-size: 20px;
}
div p {
font-size: 8px;
color: yellow;
}
这段代码的 CSSOM 看起来像这样:
请注意,在上面的模式中,嵌套元素既有继承的样式(来自父级 - 例如:h1 从 body 继承其颜色,section 从 body 继承其字体大小)和它们自己的样式(可以覆盖继承的规则 是否来自父节点 - 例如:p 覆盖了从 div 继承的颜色和字体大小,而 mainTitle 没有从父节点获得其左边距)。
由于我们的 CSS 可以有多个来源,并且它们可以包含适用于同一节点的规则,因此浏览器必须决定最终应用哪个规则。这就是优先级发挥作用的时候,如果您想了解更多相关信息,可以访问https://developer.mozilla.org/zh-CN/docs/Web/CSS/Specificity。
想象一下,您在机场寻找您的朋友 John。如果你想通过喊他的名字找到他,你可以喊 “ John ”。可能不止一个 John 会同时出现在机场,所以他们可能都会做出回应。更好的方法是用他的全名打电话给你的朋友,这样当你喊“John Doe”时,你就有更好的机会找到他,因为“ John Doe ”比“ John ”更具体。
同样,假设我们有这个元素:
<p>
<a href="<https://dev.to/>">This is just a link!</a>
</p>
以及这些 CSS 样式:
a {
color: red;
}
p a {
color: blue;
}
您认为浏览器会应用哪条规则?答案是第二条规则,因为 p 标签中的所有 a 标签选择器比所有a 标签选择器都具有更高的优先级。如果你想玩玩优先级,你可以使用这个 优先级计算器 ( https://specificity.keegan.st/ )。
重点
CSS 规则是从右到左阅读的,这意味着如果我们有这样的代码:section p { color: blue; }, 浏览器将首先查找页面上的所有 p 标签,然后它会查看这些 p 标签中是否有一个 section 标签作为父标签。如果查找能够命中,它将应用这个 CSS 规则
执行 Javascript
在解析 CSS 并创建 CSSOM 的同时,还会下载其他资产,包括 JavaScript 文件。这要归功于我们在之前文章中提到的预加载器。
预加载器就像一个解析器,它在主解析器处理 HTML 代码时扫描 HTML 文件。它的作用是查找样式表、脚本或图片(也需要从服务器检索)等资源并请求它们。希望在解析 HTML 时,这些资源已经下载并准备好进行处理。
所以,当我们从服务器获取 Javascript 文件后,代码被解释、编译、解析和执行。计算机无法理解 Javascript 代码,只有浏览器可以。JS 代码需要被翻译成计算机可以使用的东西,这是 Javascript 浏览器引擎的工作(不要与浏览器引擎混淆)。根据浏览器的不同,JS 引擎可以有不同的名称和不同的工作方式。
Javascript 引擎
javascript 引擎(有时也称为 ECMAScript 引擎)是一种在浏览器中执行(运行)Javascript 代码的软件,而不仅仅是零部件(例如,V8 引擎是 Node.js 环境的核心组件)。
JavaScript 引擎通常由 Web 浏览器供应商开发,每个主要浏览器都有一个。我们说过,目前使用最多的浏览器是 Chrome、Safari、Edge 和 Firefox。每个都使用不同的 Javascript 引擎,它们是:
V8 V8 是 Google 的高性能 JavaScript 引擎。它是用 C++ 编写的,用于 Chrome 和 Node.js 等。它实现了 ECMAScript(一种 JavaScript 标准,旨在确保网页在不同 Web 浏览器之间的互操作性)和 WebAssembley。它实现了 ECMA-262。
JavaScriptCore JavaScriptCore 是 WebKit 的内置 JavaScript 引擎,它为 Safari 浏览器、邮件和 macOS 上使用的其他应用程序提供支持。它目前按照 ECMA-262 规范实现 ECMAScript。它也被称为 SquirrelFish 或 SquirrelFish Extreme。
Chakra Chakra 是微软为其 Microsoft Edge 网络浏览器和其他 Windows 应用程序开发的 Javascript 引擎。它实现了 ECMAScript 5.1,并且对 ECMAScript 6 有部分(不断增加的)支持。它是用 C++ 编写的。
SpiderMonkey SpiderMonkey 是 Mozilla 的 Javascript 和 WebAssembly 引擎。它是用 C++、Javascript 和 Rust 编写的,用于为 Firefox、Servo 和其他项目提供支持。
一开始,Javascript 引擎只是简单的解释器。我们今天使用的现代浏览器能够执行称为即时 (JIT) 编译的功能,这是编译和解释的混合体。
编译
在编译过程中,一个称为编译器的软件将用高级语言编写的代码一次性转换为机器代码。创建一个目标文件,该文件可以在任何机器上运行。采取这些步骤后,就可以执行代码了。
解释
在解释过程中,解释器逐行检查 Javascript 代码并立即执行。没有进行编译,因此没有创建目标代码(代码的输出由解释器本身使用其内部机制创建)。旧版本的 Javascript 使用这种类型的代码执行。