C#网络编程

HTTP 缓存

作者:陈广 日期:2018-12-10


前面我们在学习 HTTP 协议的过程中已经了解到,网页网面一般由 HTML、CSS、JavaScript 及图片等多个文件组成,每下载一个文件都需要向服务器发送一次请求。另外,我们也知道,浏览器是有本地缓存的,也就是说,我们浏览一个网页,这个网页的所有内容都会存储在本地硬盘上。这时我们会想到,如果再次打开之前浏览过的网页,而在服务器中,有关此网页的内容并没有改变,是不是可以直接从本地缓存读取网页内容,而不必再从服务器下载了呢?

HTTP 缓存是指当 Web 请求抵达缓存时,如果本地有已缓存的副本,就可以从本地存储设备而不是从原始服务器中提取这个文档。缓存的好处显而易见:

  • 减少了冗余的数据传输,节省了传输时间。
  • 减少了服务器的负担,大大提高了网站的性能。
  • 加快了客户端加载网页的速度。

HTTP 缓存机制

缓存有客户端缓存、CDN、服务器端缓存几种方式。客户端缓存就是浏览器的本地缓存。CDN 则是专门架设一个服务器为一个区域的用户提供缓存服务,也称为共享缓存。在客户端第一次请求数据时,缓存中没有对应的缓存数据,需要请求服务器,服务器返回后,浏览器将数据放至缓存。

图 1: 第一次请求数据

对于 HTTP 的缓存机制来说,规则体现在 HTTP 的 header 信息的字段上,而这些规则根据是否需要重新向服务器端发起请求可以分为强制缓存协商缓存两大类。接下但介绍这两种缓存机制。

强制缓存

已存在缓存数据时,仅基于强制缓存,请求数据的流程如下:

  1. 如果缓存命中,则流程如下图所示:

图 2: 强制缓存规则下,缓存命中
  1. 如果缓存未命中,则流程如下图所示:

图 3: 强制缓存规则下,缓存未命中

强制缓存紧密联系着一个缓存时间期限,当浏览器请求资源的时候会查看缓存中的资源是否存在并且确定该缓存的资源是否过了“保质期”,若没有超过保质期则将取得缓存中的资源进行下一步处理。

协商缓存

已存在缓存数据时,仅基于对比缓存,请求数据的流程如下:

  1. 如果缓存命中,则流程如下图所示:

图 4: 协商缓存规则下,缓存命中
  1. 如果缓存未命中,则流程如下图所示:

图 5: 协商缓存规则下,缓存未命中

可见协商缓存无论如何都会和服务器交互,比较强制缓存稍微要复杂一点,但是二者是相辅相成并且可以共同存在的,强缓存优先级较高,意味着请求一个资源时会先比较强缓存的 header,如果命中则不会再执行接下来的协商缓存的过程。

HTTP 1.0 缓存方案

在 HTTP 1.0 时代,给客户端设定缓存方式可通过两个字段:PragmaExpires来规范。虽然这两个字段早可抛弃,但为了做 HTTP 协议的向下兼容,你还是可以看到很多网站依旧会带上这两个字段。

header 名称 说明
Pragma 控制缓存行为,如果设置为no-cache,表示禁用缓存。
Expires 过期时间,用的是服务器的时间,如果客户端和服务器时间不一致,则会存在缓存时间误差,缓存变得没有意义。

如果设置了Expires之后,客户端在需要请求数据的时候,首先会对比当前系统时间和这个Expires时间,如果没有过那个时间,则直接读取本地磁盘中的缓存数据,不发送请求。此时执行的是强制缓存规则。Pragma的优先级比Expires更高。

由于 HTTP 1.0 协议现在已经很少使用,所以此处不再做详细介绍。

HTTP 1.1 缓存方案

针对之前Expires的无法保证客户端和服务器端时间统一的问题,HTTP 1.1 新增了Cache-Control来定义缓存过期时间。若报文中同时出现了ExpiresCache-Control,则以Cache-Control为准。也就是说,优先级从高到低分别是:

Pragma -> Cache-Control -> Expires

响应报文中的缓存控制

服务器在响应报文中设置Cache-Control对缓存行为进行控制。可将Cache-Control的取值拆解为三部分,如下图所示:

图 6:Cache-Control 取值

三个部分都是可选的。

  • 第一部分决定是否使用缓存,或者在哪里缓存,这条指定一共有4个取值:
指令 说明
public 可以缓存,而且即可以使用本地缓存,也可以使用共享缓存(如CDN)
private 可以缓存,但只允许本地缓存
no-cache 告诉客户端,不能直接使用缓存。需要经过服务器再次验证后,才决定是否使用缓存(服务器返回304后才能使用缓存)
no-store 不允许缓存,暗示请求或响应中包含机密信息
  • 第二部分决定缓存的有效时间 ,以秒为单位,有两种取值:
指令 说明
max-age 客户端缓存有共享缓存的有效时间,如 max-age=2000 表示缓存 2000 秒
s-maxage 同 max-age,但仅用于共享缓存
  • 第三部分控制客户端向服务器发起再次验证,它有三个值:
指令 说明
must-revalidate 告诉客户端必须向服务器发起再次验证,即使本地缓存还没过期
proxy-revalidate 告诉共享缓存必须向源服务器发起再验证,即使共享缓存还未过期
immutable 指明文档是不可更改的

请求报文中的缓存控制

浏览器可以通过 Refresh(刷新)按钮或【Ctrl + F5】快捷键,控制缓存行为。

创建一个简单的示例页面

下面以 IIS Web 服务器为基础,简单演示下浏览器本在请求报文中控制缓存的一些行为。

新建一个 Web 文件夹,在文件夹上点击鼠标右键,在弹出菜单中选择【Open with Code】打开此文件夹。在 Web 文件夹下新建一个 Index.cshtml 文件。输入如下代码:

<!DOCTYPE html>
<html lang="en">
<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>Document</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <img src="Anders.png" alt="Delphi/C#之父:Anders Hejlsberg">
</body>
<script src="index.js"></script>
</html>

接下来在 Web 文件夹下新建一个名为 style.css 的文件,输入如下代码:

body{
    background-color: cornflowerblue;
}

在 Web 文件夹下新建一个名为 Index.js 的文件,输入如下代码:

//这是一个 JavaScript 文件

将一个名为 Anders.png 的图片文件拷贝到 Web 文件夹下。

运行示例并分析

在 vscode 中按下【Ctrl + Shift + p】键打开命令面板,并使用【IIS Express: Start Website】命令运行 Web 服务器,从而打开浏览器并访问 Index.html 页面。这里我使用的是 Edge 浏览器,在浏览器中按下【Alt + X】快捷键打开下拉菜单,并选择最下方【设置】,在弹出的设置面板中点击【选择要清除的内容】按钮,确保选中【缓存的数据和文件】并点击【清除】按钮清除缓存。

第一次访问

复制浏览器地址栏的地址,然后关闭浏览器。再次打开浏览器,并按下【F12】打开开发者工具窗口。在地址栏粘贴刚才访问的地址(我这里是 http://localhost:3255/),然后按下回车访问网站。结果如下图所示:

图 7:第一次访问

可以观察到,第一次访问服务器,所有文件均返回 200 状态码,表示所有内容均来自服务器。而且在请求 header 中并没有出现任何有关Cache-Control的内容。

在地址栏按下回车键

用鼠标点进地址栏,并按下回车键。这里会有两种情况:

  1. 如果离上次访问的时间足够久,具体没测算过,应该超过 1 分钟就可以了,表现如下图所示:

图 8:第二次按回车键访问

我们可以观察到,返回 【304 Not Modified】状态码,表示使用了协商缓存规则,经服务器验证后使用本地缓存。在请求 header 中并没有出现任何有关Cache-Control的内容。

  1. 如果离上次访问时间很短,最好 10 秒内再按回车键,则表现如下图所示:

图 9:短时间内第二次按回车键访问

可以观察到,虽然返回的状态码是 200,但内容全部来自缓存,而且请求和响应 header 中无任何内容。表明根本没有访问服务器,直接从缓存内读取内容。此时使用的是强制缓存规则。

使用刷新按钮

点击浏览器的刷新按钮或按【F5】快捷键,结果如下图所示:

图 10:使用刷新按钮

可以看到,结果与之前在地址栏按回车键(离上次访问较长时间,图5-8)类似,但请求 header 中出现:

Cache-Control: max-age=0

无论两次刷新的间隔时间长短,结果都一样。浏览器使用这条指令告诉服务器要强制验证缓存文件是否过期,无论服务器是怎么设置的。

强制刷新

在浏览器中按下【Ctrl + F5】快捷键,结果如下图所示:

图 11:使用强制刷新

可以看到结果和第一次访问类似,所有内容均从服务器返回(后面两条再次访问缓存不知为何出现,Edge 独有),而且这次在请求 header 中出现:

Cache-Control: no-cache

浏览器通过这条指令告诉服务器,不使用缓存,所有内容均从服务器重新下载。

服务器再验证

如果首次验证发现缓存已经超过有效期(Cache-Control: max-age已经过期),此时缓存有可能依然存在,但不能直接使用。需要向服务器发请验证,由服务器决定是否可用,这个过程称为服务器再验证。

假设浏览器要求验证一张图片,如果浏览器缓存中的上次所请求的图片和服务器中的一样,那么服务器就返回 304 让浏览器直接从缓存读取;如果不一样,则从服务器返回。那么,服务器又是如何判断浏览器缓存中的图片是否和服务器的图片是否一致呢?有两种方法。

Last-Modified

第一种方法是通过时间判断,过程如下图所示:

图 12:使用时间判断数据新鲜度

我们可以通过示例来观察它的运行机制,打开上一小节中的示例,并重复“第一次访问”步骤,观察响应 header,如下图所示:

图 13:第一次访问

我们看到这句代码:

Last-Modified: Sun, 09 Dec 2018 09:26:19 GMT

Last-Modified的值是一个时间,它由服务器通过响应 header 传送给浏览器,表示此资源的最后修改时间。

接下来在浏览器中按下【F5】键刷新,查看请求 header

图 14:第二次访问

我们看到这句代码:

If-Modified-Since: Sun, 09 Dec 2018 09:26:19 GMT

If-Modified-Since的值正是之前从服务器传过来的Last-Modified的值。当服务器收到这个值时,将它与所请求资源的Last-Modified值进行对比,如果一致,则返回 304;如果不一致,则返回 200,并将最新资源一起传回客户端。

Last-Modified存在一定问题:

  1. 某些服务器不能精确得到文件的最后修改时间,这样就无法通过最后修改时间来判断文件是否更新了。
  2. 某些文件的修改非常频繁,在以秒为单位以下的时间内进行修改,而Last-Modified只能精确到秒。
  3. 一些文件的最后修改时间改变了,但是内容并未改变,我们不希望客户端认为这个文件修改了。

ETag

第二种方法是通过一个唯一标志符 为了解决上述Last-Modified可能存在的不准确的问题,HTTP 1.1 还推出了ETag header 字段。 服务器会通过某种算法,给资源计算得出一个唯一标志符(看着很像哈希码),在把资源响应给客户端的时候,会连同此标志一起返回。

在第一次访问资源时,查看图13,我们可以看到以下响应 header:

ETag: "f4ec503ea18fd41:0"

在第二次访问资源时,查看图14,我们可以看到以下请求 header:

If-None-Match: "f4ec503ea18fd41:0"

If-None-Match的值与之前从服务器收到的ETag值一致。当服务器收到这个值时,将它与所请求资源的ETag值进行对比,如果一致,则返回 304;如果不一致,则返回 200,并将最新资源一起传回客户端。

使用 ASP.NET Core 控制缓存行为

要在服务器端控制缓存行为,可以设置 Web 服务器,也可以通过后台程序实现。这里我们介绍下如何使用 ASP.NET Core 控制缓存行为。

创建项目

新建一个 Cache 文件夹,在右键菜单中选择【Open with Code】打开此文件夹。按下【Ctrl + ~】打开终端,输入命令 dotnet new empty 创建一个新的空白项目。打开 Properties 文件夹下的 launchSettings.json 文件,将sslPort项的值更改为0以关闭 HTTPS。将applicationUrl项的值更改如下:

"applicationUrl": "http://localhost:5000",

接下来在Startup类中添加 MVC 及路由服务,更改 Startup.cs 代码如下:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace Cache
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseStaticFiles(new StaticFileOptions
            {
                OnPrepareResponse = ctx =>
                  {   //在响应 header 中添加缓存控制 header
                      ctx.Context.Response.Headers.Append("Cache-Control", "public,max-age=10");
                  }
            });
            app.UseMvcWithDefaultRoute();
        }
    }
}

我们如果要使用静态文件,需要在 Startup.cs 文件的Configure方法中添加静态文件服务:app.UseStaticFiles。而在这个方法中,我们可以设置 ASP.NET Core 的静态文件的缓存行为。这是通过直接在响应 header 中加入相应的缓存字段来实现的。

添加控制器

接下来添加控制器。在项目根目录中添加一个 Controllers 文件夹,并在其中新建一个 HomeController.cs 文件,输入代码如下:

using Microsoft.AspNetCore.Mvc;

namespace Cache.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }
    }
}

添加视图

在项目根目录下新建 Views 文件夹,在 Views 文件夹下新建 Home 文件夹。然后在 Home 文件夹下新建 Index.cshtml 文件,并输入如下代码:

<!DOCTYPE html>
<html lang="en">
<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>
</head>
<body>
    <h1>缓存测试页</h1>
    <img src="Anders.png"/>
</body>
</html>

最后在 wwwroot 文件夹下拷贝一张名为 Anders.png 的图片。

运行程序

很遗憾,Edge 还是有一些问题,无论发如何刷新,返回的都是 304。如果查看图片的报文,刷着刷着,就都完全返回 200,并从服务器下载文件了。没办法,最后只能使用 Chrome。

运行程序后,打开 Chrome 浏览器,按【F12】打开开发者工具,在浏览器地址栏中输入:localhost:5000,按回车访问网页,然后查看 Anders.png 文件的报文,如下图所示:

图 15:第一次运行

可以看到,max-age已经被设置为 10 秒,表示 10 秒内浏览器再打开此图片,都不会再访问服务器了。

多次点击刷新按钮进行观察,我们会发现,在某一次返回 304 后,如下图:

图 16:返回 304

开始计时,在 10 秒内,点击多少次刷新按钮,返回的都是 200,而且是来自缓存,如下图:

图 17:返回 200

这表明max-age=10会让浏览器在 10 秒内重用一个资源都不会再去访问服务器。

;

© 2018 - IOT小分队文章发布系统 v0.3