阅读 1478

前端性能优化指南[8]--页面呈现过程之网络加载篇

本篇是此系列第 8 篇, 上一篇:Web 性能指标
下篇预告:页面呈现过程之渲染篇

关于页面如何呈现的文章网络上有很多很多了,大家可以去搜一搜, 这也是一道经典的面试题,例如:

还有很多很多没有列举出来,有讲的简单的,也有讲的很详细的。页面如何呈现这个过程真要细讲的话用几本书都讲不完,大家总结的各自有各自的侧重点和关注点,可以多看看互补一下,也比较建议每个人都写一篇属于自己的关于页面如何呈现的文章,可以帮助自己疏通知识脉络,查漏补缺。

在讨论 Web 页面的性能优化策略之前,很有必要把页面从导航开始到呈现到屏幕的过程梳理一下,了解在页面呈现过程中哪些阶段会出现哪些性能问题,然后针对这些问题,提出对应的优化措施。

网络知识是浩瀚的,我怀着忐忑的心情尝试梳理了一下这个过程,以串联页面加载过程为主题,尽量覆盖核心知识点和与性能有关的内容 😇。

另外,本篇文章参考了一些书籍,文中非本人原创图片已注明来源。更详细的内容,建议大家阅读这些书籍:

  • 《TCP/IP 详解 卷 1:协议》
  • 《网络是怎样连接的》
  • 《HTTP 权威指南》
  • 《图解 HTTP》

前置知识

了解页面的网络加载过程,我们就需要知道网络是如何通信的,包括通信对象和通信协议。

客户端 & 服务器

通信对象是指连接到互联网中的计算机,可以分为客户端和服务器。

  • 客户端:用户接入网络的设备以及设备中的应用程序,例如 PC 端 - 电脑 - 浏览器、移动端 - 手机 - App
  • 服务器:存储网页和应用程序,可分为代理服务器和源服务器(源服务器是相对于代理而言的)

客户端从服务器下载网页的过程可用下图表示。

image.png 客户端与服务器

网络协议 & 协议分层

客户端与服务器之间通过网络实现通信需要事先达成一种约定,以确定数据应该如何封装、定址、传输、路由以及在目的地如何接收,这种约定称为协议。

这些协议在不同厂商的设备以及不同的操作系统中被实现,只要这些设备支持这些协议并在与其他设备通信时遵循相同的协议就能够实现通信。

与 W3C 性能工作组、Web 性能标准和各大浏览器厂商的关系一样,我们需要一个制定通信协议规范的标准化组织,让不同厂商生产的计算机实现这些协议,让全世界的计算机都能互相通信。

OSI 七层参考模型

1984 年,国际标准化组织(International Organization for Standards,ISO) 制定了一个国际标准--开放式通信系统互联(Open Systems Interconnection,OSI ),作为通信协议设计指标的参考模型,通常称为 OSI 参考模型。 image.png OSI 七层通信模型

OSI 参考模型将通信协议分成了 7 层,并定义了各层的分工:

应用层

应用程序提供服务并规定应用程序中通信相关的细节。包括文件传输、电子邮件、远程登录(虚拟终端)等协议。

表示层

将应用处理的信息转换为适合网络传输的格式,或将来自下一层的数据转换为上层能够处理的格式。因此它主要负责数据格式的转换

具体来说,就是将设备固有的数据格式转换为网络标准传输格式。不同设备对同一比特流解释的结果可能会不同。因此,使它们保持一致是这一层的主要作用。

会话层 

负责建立和断开通信连接(数据流动的逻辑通路),以及数据的分割等数据传输相关的管理。

传输层 TCP

起着可靠传输的作用。只在通信双方节点上进行处理,而无需在路由器上处理。

网络层 IP

将数据传输到目标地址。目标地址可以是多个网络通过路由器连接而成的某一个地址。因此这一层主要负责寻址和路由选择。

数据链路层 

负责物理层面上互连的、节点之间的通信传输。例如与 1 个以太网相连的 2 个节点之间的通信。将 0、1 序列划分为具有意义的数据帧传送给对端(数据帧的生成与接收)。

物理层

负责 0、1 比特流(0、1序列)与电压的高低、光的闪灭之间的互换。

TCP/IP 四层模型

在同一时期,互联网工程任务组(Internet Engineering Task Force,IETF)制定了 TCP/IP 协议。和 Web 性能标准一样,TCP/IP 协议有一个标准化流程:

  • 草案阶段
  • 提议标准阶段
  • 草案标准阶段
  • 标准阶段

最终被标准化的协议,记入 RFC(Request For Comment)文档并在互联网上公布。RFC 不仅记录了协议规范内容,还包含了协议的实现和运用的相关信息以及实验方面的信息。

我们可以在 rfc-editor.org 查询所有的通信协议文件,网站中有一个名为 rfc-index.txt 的文件包含了所有 RFC 的概览, 记录了各协议对应的编号,我们可以根据编号去主页检索。这里我整理了我们常用的一些协议文档。

表:RFC 协议文档

协议RFC
HTTP 1.1RFC 2616 - HTTP/1.1
RFC 7230 - HTTP/1.1  Message Syntax and Routing
RFC 7231 - HTTP/1.1  Semantics and Content
RFC 7232 - HTTP/1.1  Conditional Requests
RFC 7233 - HTTP/1.1  Range Requests
RFC 7234 - HTTP/1.1  Caching
RFC 7235 - HTTP/1.1  Authentication
HTTP 2
RFC 7540 - HTTP/2
RFC 7541 -  HPACK: Header Compression for HTTP/2
DNS
RFC 6895 - Domain Name System (DNS)
TLSRFC 4346 - TLS
TCPRFC 793 - TCP
UDPRFC 768 - UDP
IPRFC 791 - IP
RFC 8200 - Internet Protocol, Version 6 (IPv6) Specification

如果对某个协议规范的内容扩展,会用一个全新编号的 RFC 文档记录,新的 RFC 文档会明确说明是扩展了哪个已有的 RFC。如果是修改已有某个协议规范内容,会重新发行一个新的 RFC 文档,同时老的那份 RFC 作废,新的 RFC 文档会明确说明要作废哪个已有 RFC。

协议的内容这里就不像 Web 性能标准那样一一写出了,大家可以点击进去浏览一下,只看目录就能知道这个协议定义了哪些规范。

看到 IETF 组织制定的协议标准的具体内容之后,我们也应该会发现,这些协议很注重实用性,根据协议定义的内容实现,是可以被实际应用的。

TCP/IP 协议簇因其可行性较强被广泛使用,而 OSI 参考模型只是对各层的作用做了一些粗略的界定,并没有对协议和接口进行详细的定义,因而只是一个参考模型,没有给出可落地的实现方案。

但是许多通信协议会以 OSI 参考模型作为指导去制定,并可以对应到 7 个分层中的某层,我们可以把 TCP/IP 协议簇中的各种协议对应到 OSI 参考模型当中,在学习通信协议时,可以通过协议所处的层次来了解该协议在整个通信功能中的位置和作用。

页面加载过程

基于 HTTP 协议加载页面的通信模型结构如下图所示,本篇文章对页面加载过程的讲述将通过对照 OSI 参考模型中通信功能的分类和 TCP/IP 的功能实现逐层展开。借助 OSI 参考模型和 TCP/IP 协议簇的功能定义加深对页面加载过程的理解。

OSI 模型的分层粒度很细,每一层都是独立的,但在实际实现过程中,并不能做到这么细粒度的独立分层。可以看到应用层、表示层和会话层都是由应用程序中实现的,你可以在 Chrome 的源码中看到这些实现代码,他们并不能完全独立的实现。

图中还有一个五层模型,因为一般在讲 TCP/IP 四层模型时,都会把网络接口层分为数据链路层和物理层来讲。

image.png** 通信模型与架构实现

触发请求的方式有很多,比如地址栏输入、HTML 中的 img、video 等标签、Script 脚本中发起(包括 beacon)等等,最终会有一个请求的 URL。

对于 HTML 文档,一般都是从地址栏输入开始。浏览器需要先解析 URL,判断使用哪一种应用层协议发起这个请求,如果是 HTTP 协议,会先查找本地缓存,再决定是否发起网络请求。

下面我们从 URL 解析开始页面加载的旅程。

解析 URL

浏览器的第一步工作就是对 URL 进行解析,解析完 URL,浏览器就能知道通过何种方式访问位于哪个位置的哪个资源了。 image.png URL 中的信息

首先需要知道的是 URL 的协议类型,这决定将要创建什么类型的请求,例如:

  • http://  使用 HTTP 协议访问 Web 服务器
  • file://   读取客户端本地文件
  • ftp://   使用 FTP 协议访问 FTP 服务器,用于上传和下载文件

协议类型不同,URL 的规范也不同,浏览器需要根据协议类型来解析 URL。 image.png 来自《网络是怎样连接的》 URL 的各种格式

根据这个规则,我们可以从 HTTP 请求的 URL 中获取这些信息:

  • 协议类型:HTTP 或 HTTPS,定义了  Web 客户端和 Web 服务器的通信规则
  • 服务器域名:通过域名解析们可以获得目标服务器的 IP 地址
  • 端口号:用来识别要连接的服务器程序的编号
  • 文件路径名:资源在服务器中的位置

其中,协议定义的通信规则应用于应用层准备 HTTP 报文和接收并解析 HTTP 报文时,通过服务器域名获得的 IP 地址和端口号的作用体现在 TCP/IP 层建立连接阶段,而文件路径则是在请求到达服务器之后查找资源所用。

内部重定向(HTTP > HTTPS)

HTTP Strict Transport Security 规范定义了一种安全机制,当访问 https:// 的地址时,服务端可以在响应头中添加 Strict-Transport-Security, 告诉浏览器记录下这些信息,当在后面使用 http:// 访问这个网站时自动把HTTP 替换为 HTTPS。

// max-age 的值是过期时间
strict-transport-security: max-age=86400
复制代码

所以,当我们访问 www.taobao.com/ 时,解析 URL 发现使用的是 HTTP 协议,浏览器会查询是否记录了在之前访问 www.taobao.com/ 时存储的 strict-transport-security 信息,如果存在且没有过期,就不会向服务器发起网络请求,而直接在浏览器内部重定向到 www.taobao.com/

更多细节在后面的《浏览器接收响应- HTTP 重定向》中介绍。

查找 HTTP 缓存

如果不需要内部重定向,则通过资源的 URL 查找客户端本地缓存。这里以手淘首页 main.m.taobao.com/?sprefer=sy… 为例。

首先查找本地缓存的资源,我们可以在地址栏输入 chrome://cache 查看浏览器缓存的资源,Chrome 浏览器记录了所有请求的响应,包括文档、静态资源、Ajax请求、重定向等。因为存储空间是有限的,这些记录会根据一定的规则被清除。

注意:chrome://cache 以及后面提到的 chrome://net-internals/#dns 在 Chrome 65 版本之后就没有这个入口了,如果想要通过 Chrome 浏览器查看,建议下载  Chrome 65 及之前的稳定版本,下载完之后需要将本地最新版本的 Chrome 删除才能看到入口。

我是在这里www.chromedownloads.net/)下载的。

image.png 查看 Chrome 中缓存的资源

浏览器中有缓存并不能代表就能使用,我们需要查看缓存资源的具体信息。例如 main.m.taobao.com/?sprefer=sy… 的缓存信息。

image.png 浏览器第一次访问资源缓存的信息

这是浏览器第一次访问时缓存的信息,返回状态码 200,还有两个与缓存相关的响应头:

cache-control: max-age=300, s-maxage=600
etag: W/"2c8d-17145f41a6e"
复制代码

cache-control 告诉浏览器,该资源在客户端的缓存时间为 300 秒(5 分钟),在代理缓存服务器上的缓存时间为 10 分钟。

当我们在 5 分钟内再次访问 main.m.taobao.com/?sprefer=sy… 时,浏览器直接从本地缓存读取资源而不需要发送网络请求。

image.png 从本地缓存获取资源

当 5 分钟之后访问此资源时,发现资源已过期,浏览器需要发起网络请求从服务器获取资源,并在请求头中添加 If-None-Match,其值为缓存副本中的 Etag 值,即浏览器会将之前服务器返回的标识此资源的 Etag 再发送给服务器,当服务端接收到请求后,与服务端的资源对比(Etag 资源在服务端的唯一标识,资源更改,Etag 会变化),判断资源是否改变。

当资源没有变化时,服务端返回 304,告知浏览器从本地直接取缓存副本。

image.png 浏览器与服务端协商缓存

到这里,很多人会发现在缓存有效期内访问 main.m.taobao.com/?sprefer=sy… 时,浏览器并没有直接从本地取缓存副本,而是发起了网络请求。

正常的缓存机制应该是像上面说的那样在缓存有效期内直接取本地缓存副本,缓存过期之后再发起网络请求的,但在这个例子中,Chrome 浏览器做了一些其他的事情。

你会发现,本地明明有缓存且在有效期内,但浏览器却忽略了 cache-control: max-age=300,自行在请求头中加上了字段 cache-control:max-age=0,表示浏览器不希望直接从本地获取,而应该向服务器发送请求,验证本地资源的有效性再决定是否使用本地缓存,即浏览器需要与服务器协商。

image.png

浏览器自行添加了请求头 cache-control:max-age=0 与服务器协商缓存

浏览器为什么要这么做呢?是所有的资源缓存都有这种策略吗?

从 Chrome 发布的这篇博客  Reload, reloaded: faster and leaner page reloads 了解到,他们考虑到用户经常会重新刷新页面,用户重新加载页面一般是因为两种原因:页面崩溃了或者希望刷新看到新的内容,在这之前为了解决页面崩溃时刷新页面的问题,所有资源都会从缓存获取,但是当用户希望刷新内容时就很难看到新的页面了,尤其是在移动设备上。

所以,后来 Chrome 改变了重新加载页面时的行为,只验证主资源是否更新,这样既保证了最大限度的重用缓存资源,也保证了页面的新鲜度。

这大概就是当访问  main.m.taobao.com/?sprefer=sy… 时浏览器自行添加 cache-control:max-age=0 发起网络请求与服务端协商缓存而不直接取本地缓存的原因吧。

另外,当我们重新打开一个 Tab 访问 main.m.taobao.com/?sprefer=sy… 时,会发现浏览器是直接从缓存读取的,而在多次访问后就算新打开一个 Tab 访问也发送了网络请求没有直接读取缓存。

正如 kiewicIs Chrome ignoring Cache-Control: max-age? 中所说,浏览器有一套算法猜测用户想做什么,当用户刷新页面或者多次打开同一个页面,浏览器猜测用户希望看到更新鲜的页面,所以对于主文档会发起网络请求并与服务器协商是否使用本地缓存,而对于其他静态资源以缓存副本中的 cache-control 为准,浏览器不会自行忽略并添加 max-age=0。

PS: 试了下 Firefox,也会自行添加 cache-control:max-age=0

HTTP 缓存机制以 Cache-Control、Etag、If-None-Match 这些协议头为准,但由于历史原因,还有一些与缓存相关的请求头和响应头,这里不具体说明,用一张表简单列举一下。

image.png HTTP 缓存相关协议头

查找缓存的过程可以看这张流程图:

image.png 获取缓存流程

发起网络请求

如果本地没有缓存或缓存资源不可用,就需要发起网络请求了。

应用层:DNS 域名解析

发起网络请求之前我们需要与目标服务器建立连接,在建立连接之前需要知道目标服务器的 IP 地址,这就需要进行域名解析。

域名解析服务(Domain Name System,DNS)是和 HTTP 协议一样位于应用层的协议。它提供域名到 IP 地址之间的解析服务。用户通常使用主机名或域名来访问对方的计算机,而不是直接通过 IP 地址访问。

DNS 协议提供通过域名查找 IP 地址,或逆向从 IP 地址反查域名的服务。关于 DNS 的解析原理建议查阅书籍《网络是怎样连接的》,DNS 解析过程如下图。

image.png  来自《网络是怎样连接的》DNS 服务器之间的查询操作 具体过程如下:

  • 0、从浏览器缓存查询 DNS 记录,如果没有则从操作系统缓存中查询 DNS 记录。
  • 1、向本地域名服务器发起请求,询问 www.lab.glasscom.com 的 IP 地址。如果本地 DNS 服务器有缓存且未过期则直接返回 IP。
  • 2、本地 DNS 服务器询问根域名服务器,根域名服务器返回顶级域名(com)的服务器地址。
  • 3、本地 DNS 服务器询问顶级域名(com)服务器 www.lab.glasscom.com 的 IP 地址,顶级域名(com)服务器返回二级域名(glasscom.com)服务器地址。
  • 4、本地 DNS 服务器询问二级域名(glasscom)服务器 www.lab.glasscom.com 的 IP 地址,二级域名(glasscom)服务器返回三级域名(lab.glasscom.com)服务器地址.
  • 5、本地 DNS 服务器询问三级域名(lab.glasscom.com)服务器 www.lab.glasscom.com 的 IP 地址,三级域名(lab.glasscom.com)服务器返回四级域名(www.lab.glasscom.com)服务器的 IP 地址,这就是 Web 服务器的 IP。
  • 6、本地 DNS 服务器将 www.lab.glasscom.com 的 IP 返回给客户端
  • 7、客户端根据这个 IP 向 Web 服务器发我请求。

这个查询过程基于两个前提:

  • 根域的 DNS 服务器信息保存在互联网中所有的 DNS 服务器。
  • 负责管理下级域的 DNS 服务器的 IP 地址注册在它们的上级 DNS 服务器中,上级 DNS 服务器的 IP 地址再注册到更上一级的 DNS 服务器。

应用层:准备 HTTP 请求消息

当需要发起网络请求时,浏览器需要准备 HTTP 消息。在 HTTP 1.1 中 请求报文是由请求方法、请求 URI、协议版本、可选的请求首部字段和内容实体构成的。

image.png 请求报文的构成

所以浏览器需要确定以下内容:

  • 请求方法
  • 请求 URI
  • 协议版本
  • 请求首部字段
  • 内容实体

每条内容都涉及到很多知识,这里简单介绍一下。

请求方法

请求方法表示请求访问服务器的类型。 HTTP/1.0 和 HTTP/1.1 支持的方法有以下这些,浏览器默认发起 GET 请求,如果是使用脚本发起的请求,以脚本中指定的请求方法为准。

image.png 来自《图解 HTTP》 HTTP/1.0 和 HTTP/1.1 支持的方法

协议版本

HTTP 的版本号,表示客户端使用的 HTTP 协议版本。目前为止,HTTP 的版本已经到了 HTTP/3 了。这里简要罗列下每个版本定义的功能。 HTTP 协议的发展.png HTTP 协议的发展

目前,多数主流浏览器已经在 2015 年底支持了 HTTP/2 协议,国内外一些排名靠前的站点基本都实现了 HTTP/2 的部署。

关于 HTTP/1、 HTTP/2、 HTTP/3 的介绍建议看下李兵老师的《浏览器工作原理与实践》 生动的介绍了 HTTP 协议的发展历程

请求首部字段

请求首部字段主要是给浏览器和服务器提供报文主体大小、所使用的语言、认证信息、以及一些协商内容。例如:

  • 访问来源:Host、Referer、User-Agent
  • 认证信息:Cookie、Authorization
  • 缓存协商信息:If-Modified-Since、If-None-Match
  • 内容编码、字符集和数据格式协商:Accept、Accept-Encoding、Accept-Language
  • 其他

浏览器(应用程序)不会亲自负责数据的传送,而是委托操作系统中的网络控制软件(协议栈)将消息发送给服务器。HTTP 请求消息在应用层(浏览器)中准备好之后,将传递给操作系统中的网络控制程序(TCP/IP 协议栈)。

表示层:HTTP/2 二进制分帧

如果使用的是 HTTP/2 协议,HTTP/2 协议在传输层之上,应用层之下定义了一个二进制分帧层,我们可以把它对应到 OSI 七层模型中的表示层,表示层负责数据格式的转换。

我们可以看下,分帧层把 HTTP 1.1 中的消息转换成了 HTTP/2 中的帧格式。

image.png 单一 TCP 连接的问题在于,一次只能发出一个请求,所以客户端必须等到收到响应后才能发出另一个请求。这就是 “线头阻塞” 问题。正如之前讨论的,典型的变通方案是打开多个连接;每个请求一个连接。但是,如果可以将消息分解为更小的独立部分并通过连接发送,此问题就会迎刃而解。 这正是 HTTP/2 希望达到的目标。将消息分解为帧,为每帧分配一个流标识符,然后在一个 TCP 连接上独立发送它们。此技术实现了完全双向的请求和响应消息复用,如下图所示。 image.png

如果是使用 HTTP/2 协议进行通信,介绍下 HTTP/1 和 HTTP/2 的区别,如何分帧。

会话层:应用程序通过 Socket 委托协议栈(TCP/IP)建立连接

我们可以把应用程序(应用层)通过 Socket 接口告知操作系统(传输层)建立连接的这个过程对应到 OSI 七层模型中的会话层,应用程序与操作系统的交互过程如图,更详细的内容建议查阅书籍《网络是怎样连接的》。 image.png  来自《网络是怎样连接的》消息收发操作

连接操作需要三次握手,具体实现在传输层完成。这里介绍下有传输层实现的三次握手建立连接、数据收发和四次挥手断开连接的过程。

image.png  TCP 的整体流程

数据收发操作的第一步是创建套接字。一般来说,服务器一方的应用程序在启动时就会创建好套接字并进入等待连接的状态。客户端则一般是在用户触发特定动作,需要访问服务器的时候创建套接字。在这个阶段, 还没有开始传输网络包。

创建套接字之后,客户端会向服务器发起连接操作。

  • 首先,客户端会生成一个 SYN 为 1 的 TCP 包并发送给服务器(①)。这个 TCP 包的头部还包含了客户端向服务器发送数据时使用的初始序号,以及服务器向客户端发送数据时需要用到的窗口大小 A。
  • 当这个包到达服务器之后,服务器会返回一个 SYN 为 1 的 TCP 包( ②)。和 ①一样,这个包的头部中也包含了序号和窗口大小,此外还包含表示确认已收到包①的

ACK 号 B。

  • 当这个包到达客户端时,客户端会向服务器返回一个包含表示确认的 ACK 号的 TCP 包( ③)。

到这里,连接操作就完成了,双方进入数据收发阶段。数据收发阶段的操作根据应用程序的不同而有一些差异,以 Web 为 例。

  • 首先客户端会向服务器发送请求消息。TCP 会将请求消息切分成一定大小的块,并在每一块前面加上 TCP 头部,然后发送给服务器(④)。TCP 头部中包含序号,它表示当前发送的是第几个字节的数据。
  • 当服务器收到数据时,会向客户端返回 ACK 号(⑤)。
  • 在最初的阶段,服务器只是不断接收数据,随着数据收发的进行,数据不断传递给应用程序,

接收缓冲区就会被逐步释放。这时,服务器需要将新的窗口大小告知客户端。当服务器收到客户端的请求消息后,会向客户端返回响应消息,这个过程和刚才的过程正好相反(⑥⑦)。

服务器的响应消息发送完毕之后,数据收发操作就结束了,这时就会开始执行断开操作。以 Web 为例:

  • 服务器会先发起断开过程。
  • 在这个过程中,服务器先发送一个 FIN 为 1 的 TCP 包(⑧),然后客户端返回一个表示确认收到的 ACK 号(⑨)。接下来,双方还会交换一组方向相反的 FIN 为 1 的 TCP 包(⑩)和包含 ACK 号的 TCP 包。
  • 最后,在等待一段时间后,套接字会被删除。

会话层: TLS 协商

在传输层通过三次握手建立连接之后,如果是使用 HTTPS 协议,则需要再进行 TLS 握手,建立安全连接。 这个握手过程,可以称为 TLS 协商,决定将使用哪个密码来加密通信,验证服务器,并在开始实际数据传输之前建立安全连接。因此在在实际发送内容请求之前,还需要 4 三次往返服务器。

每一个 TLS 连接都会以握手开始。如果客户端此前并未与服务器建立会话,那么双方会执行一次完整的握手流程来协商 TLS 会话。握手过程中,客户端和服务器将进行以下四个主要步骤。

  • (1) 交换各自支持的功能,对需要的连接参数达成一致。
  • (2) 验证出示的证书,或使用其他方式进行身份验证。
  • (3) 对将用于保护会话的共享主密钥达成一致。
  • (4) 验证握手消息并未被第三方团体修改。

image.png

(1) 客户端开始新的握手,并将自身支持的功能提交给服务器。 (2) 服务器选择连接参数。 (3) 服务器发送其证书链(仅当需要服务器身份验证时)。 (4) 根据选择的密钥交换方式,服务器发送生成主密钥的额外信息。 (5) 服务器通知自己完成了协商过程。 (6) 客户端发送生成主密钥所需的额外信息。 (7) 客户端切换加密方式并通知服务器。 (8) 客户端计算发送和接收到的握手消息的 MAC 并发送。 (9) 服务器切换加密方式并通知客户端。 (10) 服务器计算发送和接收到的握手消息的 MAC 并发送。

假设没有出现错误,到这一步,连接就建立起来了,可以开始发送应用数据。

传输层:封装成报文段

当连接建立之后,浏览器终于可以发出数据请求了。应用程序将调用 Socket 库中的 write 方法将应用层准备好的 HTTP 消息交给协议栈,应用程序的数据一般都比较大,因此在传输层会按照网络包的大小对数据进行拆分。将数据拆分成报文段,并添加 TCP 协议头。

image.png  应用程序数据的拆分发送

网络层:封装成数据报

IP 模块会接收 TCP 报文段,在前面添加 IP 头部和以太网的 MAC 头部后发送网络包。 image.png IP 数据报封装过程

数据链路层:封装成帧

IP 传给网络接口层的数据单元称做 IP 数据报,网卡驱动程序从 IP 模块获取数据报之后,在开头加上报头和起始帧分界符,在末尾加上用于检测错误的帧校验序列。加上报头、起始帧分界符和 FCS 之后,我们就可以将包通过网线发送出去了。这种通过以太网传输的比特流称做帧。 image.png 来自:《网络是怎样连接的》网卡发送出去的包

物理层: 数字转换为电信号

网卡的内部结构如下,网卡中的 MAC 模块从报头开始将数字信息按每个比特转换成电信号,然后由 PHY,或者叫 MAU 的信号收发模块发送出去。 image.png 来自《网络是怎样连接的》网卡

在这里,将数字信息转换为电信号的速率就是网络的传输速率。接下来,PHY(MAU)模块会将信号转换为可在网线上传输的格式,并通过网线发送出去。

image.png 网卡将数字信息转换成电信号或光信号

请求到达代理服务器

代理是一种介于客户端与 Web 服务器之间,对访问操作进行中转的机制,分为正向代理反向代理

如果把局域网外的互联网想象成一个巨大的资源库,那么资源就分布在互联网中的各个站点上。局域网内的客户端要访问这个库中的资源必须通过统一代理服务器才能对各个站点进行访,这称为正向代理。

即,正向代理服务器与客户端在同一个局域网中,正向代理服务器用来让客户端接入外网以访问外网资源,因此正向代理服务器可以用来做防火墙,利用正向代理服务器可以对局域网对外网的访问进行监控和管理。

正向代理主要用于几种场景:翻墙、对局域网对外网的访问进行监控和管理、缓存代理、隐藏访问者信息。

image.png

正向代理

如果局域网向互联网提供资源,让互联网上的其他用户可以访问局域网内的资源,也可以使用一个代理服务器,它提供的服务叫做反向代理服务。即,反向代理服务器与服务端在同一个局域网中,反向代理服务器用来让外网的客户端接入局域网中的站点以访问站点中的资源。

image.png

反向代理

反向代理主要用于几种场景:

  • 防火墙,保护和隐藏原始资源服务器
  • 负载均衡,当有多台服务器时,使用代理服务器转发请求
  • 反向代理缓存,用于缓存服务端的资源

我们可以通过在客户端与服务器之间添加代理服务器来分担 Web 服务器的负载, 3 种方法:

负载均衡

当用户访问量很高时,可以通过添加多台服务器来处理。通过负载均衡代理可以将请求均衡的转发到源服务器,以减轻源服务器的负载,提升服务端处理速度。

缓存代理服务器

代理服务器部署在服务端局域网中,代理服务器每次都要将请求转发至源服务器,当源服务器中资源位改变时返回 304,直接取缓存服务器中的资源,降低了源服务器的处理时间和响应数据的网络传输时间。

内容分发服务

一种将代理服务器部署在互联网边缘,且提供缓存刷新机制的服务,在代理服务器中有缓存且缓存有效时,直接返回缓存资源,而不需要再去源服务器请求,直接节约了网络传输时间和服务端处理时间。一般静态资源会发布到 CDN 缓存服务器上。

CDN 内容分发:返回缓存资源或回源

缓存本来的思路是像上面所说的缓存服务器的工作过程那样,将曾经访问过的数据保存下来,然后当再次访问时拿出来用,以提高访问操作的效率。不过,这种方法对于第一次访问是无效的,而且后面的每次访问都需要向原始服务器查询数据有没有发生变化,如果遇到网络拥塞,就会使响应时间恶化。

要改善这一点,有一种方法是让 Web 服务器在原始数据发生更新时,立即通知缓存服务器,使得缓存服务器上的数据一直保持最新状态,这样就不需要每次确认原始数据是否有变化了,而且从第一次访问就可以发挥缓存的效果。内容分发服务采用的缓存服务器就具备这样的功能。

内容分发服务的运营商会在互联网中部署很多缓存服务器,这些服务器既不在客户端的局域网中也不在服务端的局域网中,当客户端访问 Web 服务器时,会让客户端访问离用户最近的缓存服务器。

image.png 将缓存服务器部署在互联网的边缘

那如何找到离客户端最近的缓存服务器呢?我们可以在 DNS 服务器返回 Web 服务器 IP 地址时,对返回的内容进行一些加工,使其能够返回距离客户端最近的缓存服务器的 IP 地址。

另外,部署在缓存服务器上的一般都是静态的页面,动态页面是不能保存在缓存服务器上的。但是我们可以将每次内容都会发生变化的动态部分与内容不会发生变化的静态部分分开,只将静态部分保存在缓存中。

这里用腾讯云官网的一个例子来说明 CDN 的工作流程。假设您的业务源站域名为 www.test.com,域名接入 CDN 开始使用加速服务后,当您的用户发起 HTTP 请求时,实际的处理流程如下图所示:

image.png 来自腾讯云官网 详细说明如下:

  • 用户向 www.test.com 下的某图片资源(如:1.jpg)发起请求,会先向 Local DNS 发起域名解析请求。
  • 当 Local DNS 解析 www.test.com 时,会发现已经配置了 CNAME www.test.com.cdn.dnsv1.com,解析请求会发送至 Tencent DNS(GSLB),GSLB 为腾讯云自主研发的调度体系,会为请求分配最佳节点 IP。
  • Local DNS 获取 Tencent DNS 返回的解析 IP。
  • 用户获取解析 IP。
  • 用户向获取的 IP 发起对资源 1.jpg 的访问请求。
  • 若该 IP 对应的节点缓存有 1.jpg,则会将数据直接返回给用户(10),此时请求结束。若该节点未缓存 1.jpg,则节点会向业务源站发起对 1.jpg 的请求(6、7、8),获取资源后,结合用户自定义配置的缓存策略(可参考产品文档中的 缓存过期配置),将资源缓存至节点(9),并返回给用户(10),此时请求结束。

负载均衡器: 转发请求

当服务器的访问量上升时,所有访问都打到同一台服务器上,服务器性能问题随之而来。我们可以将应用部署在多台 Web 服务器上,然后使用一种叫作负载均衡器的设备,客户端将请求发到负载均衡器上,然后由负载均衡器来判断将请求转发给哪台 Web 服务器。

对于客户端来说,负载均衡器就是一台 Web 服务器,我们需要要用负载均衡器的 IP 地址代替 Web 服务器的实际地址注册到 DNS 服务器上,客户端会认为负载均衡器就是一台 Web 服务器,并向其发送请求。

负载均衡器的工作主要是根据 Web 服务器的负载状况决定将请求转发到哪台  Web 服务器上。

image.png 来自《网络是怎样连接的》用于对多台 Web 服务器分配访问的负载均衡器

请求转发到源服务器

当 CDN 回源或者请求到达负载均衡器时,都需要将请求转发到源服务器,这时候源服务器就需要接收请求、处理和返回响应。

拆包和组装

当网络包到达 Web 服务器之后,服务器就会接收这个包并进行处理。根据用途,服务器可以分为很多种类,其硬件和操作系统与客户端是有所不同的。但是,网络相关的部分,如网卡、协议栈、Socket 库等功能和客户端却并无二致。无论硬件和操作系统如何变化,TCP 和 IP 的功能都是一样的,或者说这些功能规格都是统一的。

现在,服务端接收到的网络包格式如下: image.png 来自《网络是怎样连接的》网卡发送出去的包

服务端需要进行拆包,取出客户端的数据(应用程序负责生成的部分),然后找到服务端的应用程序并交给它处理。具体过程如下:

链路层

网卡驱动会根据 MAC 头部判断协议类型,并将包交给相应的协议栈。

网络层

协议栈的 IP 模块会检查 IP 头部。

  • 检查接收方 IP 地址,判断是不是发给自己的;
  • 检查 IP

头部的内容确定包是否被分片,如果是分片的包,则将包暂时存放在内存中,等所有分片全部到达之后将分片组装起来还原成原始包;

  • 检查 IP 头部的协议号字段,并将包转交给相应的模块。例

如,如果协议号为 06(十六进制),则将包转交给 TCP 模块;如果是 11(十

六进制),则转交给 UDP 模块。

传输层

这里我们假设这个包被交给 TCP 模块处理,然后继续往下看。前面的步骤对于任何包都是一样的,但后面的 TCP 模块的操作则根据包的内容有所区别。

  • 如果收到的是发起连接的包,TCP 模块会:
    • 确认 TCP 头部的控制位 SYN;
    • 检查接收方端口号;
    • 为相应的等待连接套接字复制一个新的副本;
    • 记录发送方 IP 地址和端口号等信息。
  • 收到数据包时,TCP 模块会:
    • 根据收到的包的发送方 IP 地址、发送方端口号、接收方 IP 地址、接收方端口号找到相对应的套接字;
    • 将数据块拼合起来并保存在接收缓冲区中;
    • 向客户端返回 ACK

网络包经过从链路层到网络层再到传输层的拆包和组装过程,最终将用户数据传送给了服务端的 Web 服务器。   image.png 来自《网络是怎样连接的》分配接收到的包

处理请求

服务器接收到了 HTTP 请求消息,会根据收到的请求消息中的内容进行相应的处理。请求消息包括一个称为“方法”的命令,以及表示数据源的 URI(文件路径名),服务器程序会根据这些内容向客户端返回数据,但对于不同的方法和 URI,服务器内部的工作过程会有所不同。

最简单的一种情况如下图所示,请求方法为 GET,URI 为一个 HTML 文件名。这种情况只要从文件中读出 HTML 文档,然后将其作为响应消息返回就可以了。

image.png  来自《网络是怎样连接的》Web 的基本工作方式

但 URI 指定的文件内容不仅限于 HTML 文档,也有可能是一个程序,比如发起 Ajax 请求时,对应的可能是服务端的一个 Controller,需要经过程序中的一些业务逻辑从数据库中查询或更新数据,最后将结果返回。

在服务端的程序处理之前还可能会有访问控制,例如判断是否登录等,才允许下一步的查询或更新数据的操作,否则不返回结果,而返回一个 401,表示没有访问权限。

返回响应

当服务器完成对请求消息的各种处理之后,就可以返回响应消息了。这里的工作过程和客户端向服务器发送请求消息时的过程相同。

首先,Web 服务器调用 Socket 库的 write,将响应消息交给协议栈。这时,需要告诉协议栈这个响应消息应该发给谁,但我们并不需要直接告知客户端的 IP 地址等信息,而是只需要给出表示通信使用的套接字的描述符就可以了。套接字中保存了所有的通信状态,其中也包括通信对象的信息,因此只要有描述符就万事大吉了。

接下来,协议栈会将数据拆分成多个网络包,然后加上头部发送出去。这些包中包含接收方客户端的地址,它们将经过交换机和路由器的转发,通过互联网最终到达客户端。

浏览器接收响应

Web 服务器发送的响应消息会被分成多个包发送给客户端,然后客户端需要接收数据。首先,网卡将信号还原成数字信息,协议栈将拆分的网络包组装起来并取出响应消息,然后将消息转交给浏览器。这个过程和服务器的接收操作相同。

现在浏览器接收到了服务端的响应,先需要对响应进行解析。浏览器会根据响应的状态码和 HTTP 头部消息决定下一步操作。

状态码含义常见状态码
1xx信息提示:接收的请求正在处理101:Switching Protocals
服务器正在根据客户端的制定,将协议切换成 Update 首部所列的协议。可查阅 协议升级机制
2xx成功:请求正常处理完毕200:OK
请求成功,消息主体包含了所请求的资源
3xx重定向:需要进一步操作以完成请求301:Moved Permanently 永久重定向
302:Found 临时重定向(HTTP 1.0)
304:Not Modified 请求资源未修改,服务端不用返回资源,客户端可以使用本地资源
307:Temporary Redirect 临时重定向(HTTP 1.1) Chrome 用于内部重定向
4xx客户端错误401:Unanthorized 未授权
404:Not Found 服务器无法找到所请求 URL 对应的资源
5xx服务器错误500: Internal Server Error 服务端内部错误
502: Bad Gateway 代理网关错误

HTTP 重定向

当服务端资源被移动时,会告知客户端资源移动了,去别的地方找,但一般我们会尽量避免这种资源被移动的情况。另外几种我们较常见的重定向是:

  • 当在移动端输入网址时,服务端会通过重定向的方式返回给客户端移动端的资源以实现自适应。
  • 域名更改后需要重定向,例如 chang20159.github.io,我们申请了一个新的域名 chang20159.com 希望能重定向到 chang20159.github.io
  • http 协议网址重定向到 https 协议网址

恰好淘宝网把这几种情况都包含了,我们在浏览器中开启手机模式,然后在地址栏输入 taobao.com, 就可以看到 7 次重定向。

image.png

main.m.taobao.com/?sprefer=sy… 是资源最终存放的目的地址,由于此文档在服务端没有改动,服务端返回了 304, 告知浏览器从本地缓存获取。

image.png 访问 taobao.com 产生的 7 次重定向

我们会发现 7 次重定向中有 3 次 307 Internal Redirect,307 本来是临时重定向的意思,这里显示的确是内部重定向。这是为什么呢?

因为在访问 www.taobao.com 时,服务端在响应头中添加了这个:

strict-transport-security: max-age=31536000
复制代码

用于告知浏览器,服务器使用严格传输安全性(HSTS)来确保此站点只能使用 HTTPS 协议访问,浏览器发现这个响应头后,会以域名 www.taobao.com 为 key 存储这个设置,有效期就是 max-age 的值。

当我用 HTTP 协议访问 www.taobao.com 时,浏览器会发现有这个配置,而在内部直接重定向到 www.taobao.com

所以,这里并没有将请求发送到服务端, 307  Internal Redirect 是浏览器创建的一个虚拟响应。

如果我们打开 chrome://net-internals/#hsts,删除这个域名安全策略(Delete domain security policies),再来看:

image.png

会发现 www.taobao.com 请求响应变为永久重定向(301 Moved Permanently)。

需要向服务端再此发起请求的重定向过程跟前面介绍的一样,当重定向到资源最终存储的地址后,我们终于拿到了需要的响应数据。

接下来就是检测响应体并做进一步处理了。

检测响应体

常见的 HTTP 请求是请求 HTML 文档、静态资源(JS/CSS/图片)以及 Ajax 数据请求等,当我们拿到 HTTP 响应消息后,需要知道响应消息是否完整、有没有被压缩、它是什么类型等等,浏览器才能进行下一步的操作,例如如果是 HTML 文档,就需要渲染它。

检测截尾(Content-Length)

Content-Length 指示报文中实体主体的字节大小,这个大小是包含了所有的内容编码的,比如对文本文件进行了 gzip 压缩的话,Content-Length 表示的就是压缩后的大小,而不是原始大小。

客户端收到响应实体消息后,需要通过 Content-Length 检测报文截尾。客户端可以通过 Content-Length 来判断报文是否结束。以此来检测出服务器崩溃而导致的报文截尾,并为共享持久连接的多个报文进行正确分段。

检测内容编码(Content-Encoding)

服务器在返回响应时,可能会对响应内容进行压缩,这样有助于减少传输内容的时间,服务器还可以把内容搅乱或加密,以此来防止未经授权的第三方看到文档的内容。

Content-Encoding 响应头说明了服务端编码使用的算法,当客户端接收到响应内容之后,需要根据编码时使用的算法对其解码。

image.png 来自《HTTP 权威指南》内容编码示例

常用的一些内容编码如下,具体可查阅  Content-Encoding | MDN

检测媒体类型和字符集(Content-Type)

Web 可以处理的数据包括文字、图像、声音、视频等多种类型,每种数据的显示方法都不同,因此必须先要知道返回了什么类型的数据,否则无法正确显示。这时,我们需要一些信息才能判断数据类型,原则上可以根据响应消息开头的 Content-Type 头部字段的值来进行判断。

Content-Type 响应头说明了响应实体的 MIME 类型,这个值一般是下面这样的字符串。

Content-Type: text/html
复制代码

其中“/”左边的部分称为“主类型”,表示数据的大分类;右边的“子类型”表示具体的数据类型。在上面的例子中,主类型是 text,子类型是 html。上面例子中的数据类型表示遵循 HTML 规格的 HTML 文档。下面列出了其中主要的一些类型。

image.png 来自《网络是怎样连接的》消息的 Content-Type 定义的数据类型

此外,当数据类型为文本时,还需要判断编码方式,这时需要用 charset 附加表示文本编码方式的信息,内容如下,这里的 utf-8 表示编码方式为 Unicode

Content-Type: text/html; charset=utf-8
复制代码

需要注意的是,Content-Type 首部说明的是原始响应实体的媒体类型。例如,如果实体经过内容编码的话,Content-Type 首部说明的是编码之前的类型。

综合判断响应实体的数据类型

Content-Type 字段使用的表示数据类型的方法是在 MIME 规范中定义的,MIME 是一个统一的标准,但也只不过是一种原则性的规范,要通过 Content-Type 准确判断数据类型,就需要保证 Web 服务器正确设置 Content-Type 的值,但现实中并非总是如此。如果 Web 服务器管理员不当心,就可能会因为设置错误导致 Content-Type 的值不正确。因此,根据原则检查 Content-Type 并不能确保总是能够准确判断数据类型。

因此,有时候我们需要结合其他一些信息来综合判断数据类型,例如请求文件的扩展名、数据内容的格式等。比如,我们可以检查文件的扩展名,如果为 .html 或 .htm 则看作是 HTML 文件,或者也可以检查数据的内容,如果是以开头的则看作是 HTML 文档。

不仅是 HTML 这样的文本文件,图片也是一样。图片是经过压缩的二进制数据,但其开头也有表示内容格式的信息,我们可以根据这些信息来判断数据的类型。不过,这部分的逻辑并没有一个统一的规格,因此不同的浏览器以及不同的版本都会有所差异

例如在 Chrome 浏览器的源码中对 MIME 的判断逻辑:

检测 MIME 类型是一项棘手的工作,因为我们需要平衡兼容性问题和安全性问题。下面是对其他浏览器行为的调查,然后是对我们打算如何行为的描述。

HTML payload, no Content-Type header:

  • IE 7: Render as HTML
  • Firefox 2: Render as HTML
  • Safari 3: Render as HTML
  • Opera 9: Render as HTML

Here the choice seems clear: => Chrome: Render as HTML

HTML payload, Content-Type: "text/plain":

  • IE 7: Render as HTML
  • Firefox 2: Render as text
  • Safari 3: Render as text (Note: Safari will Render as HTML if the URL has an HTML extension)          * Opera 9: Render as text

...

处理响应体

判断完数据类型,接下来需要根据数据类型调用用于显示内容的程序,将数据显示出来。对于 HTML 文档、纯文本、图片这些基本数据类型,浏览器自身具有显示这些内容的功能,因此由浏览器自身负责显示。

像 HTML 文档和图片等浏览器可自行显示的数据,就会按照上述方式委托浏览器在屏幕上显示出来。不过,Web 服务器可能还会返回其他一些类型的数据,如文字处理、幻灯片等应用程序的数据。这些数据无法由浏览器自行显示,这时浏览器会调用相应的程序。

这些程序可以是浏览器的插件,也可以是独立的程序,无论如何,不同类型的数据对应不同的程序,这一对应关系是在浏览器中设置好的,只要按照这一对应关系调用相应的程序,并将数据传递给它就可以了。然后,被调用的程序会负责显示相应的内容。

后面我们详细介绍浏览器是如何显示 HTML 文档的。

小结

好啦,终于说完了一个页面的加载过程,其中很多细节没有仔细去研究,但作为寻找加载过程的性能瓶颈的 Timeline 已经足够了。 现在让我们来画一下这个加载时间线吧~ 页面加载过程.png 页面加载过程

我们会发现,从网络加载主文档资源需要经过这么长的步骤,其中会影响性能的因素主要有:

  • 发送数据之前需要先有 8 次客户端-服务器往返过程
    • 1 次 DNS 解析
    • TCP 3 次握手
    • TLS 4 次握手
  • 每个应用层数据都是分段发送
    • 数据越大,发送时间越长
    • TCP 慢启动
  • 加载文档时,文档中的资源请求并发
    • HTTP/1.1 
    • HTTP/2
    • HTTP/3
  • 首屏和非首屏的加载策略
    • 预加载
      • 文档预加载
      • 静态资源预加载
      • 数据预加载
    • 懒加载
      • 图片懒加载
      • JS 按需加载

如果不针对性的做优化,首屏就几乎不可能达到秒开的目标。后面将会根据网络加载的过程,从缓存策略、加载策略、网络优化和资源优化 4 个方向对网络中出现的性能问题提出一些优化策略。

思考

写的头秃,已无法思考🤣

😇 系列篇

文章分类
前端
文章标签