在上篇博客【细说 ASP.NET Cache 及其高级用法】中, 我给大家介绍了ASP.NET Cache,这种服务端使用的缓存API 。在我们开发一个ASP.NET网站的过程中,其实有很多地方都是可以使用缓存的, 只是由于ASP.NET是一种基于服务端的开发平台,自然我们也经常在服务端的代码中使用各类缓存技术, 然而,由于WEB应用程序的服务对象是客户端的浏览器,通常来说,我们并不能直接控制浏览器的行为,但是, 浏览器却可以根据后台网站的指示,采取一些优化的方式来更快地呈现页面。 客户端浏览器也有自己的缓存机制,通常浏览器也使用缓存来优化一些页面的显示过程, 不过,我们并不能直接使用C#代码控制浏览器的缓存操作,但我们可以告诉浏览器如何使用缓存,从而达到优化网站性能的目的。
这次博客的主题是:用ASP.NET控制HTTP请求过程中浏览器缓存的一些方法。
正常的HTTP请求过程
在开始介绍浏览器在HTTP请求过程前,我想有必要先来看一下浏览器请求一个普通ASPX页面的过程。
说明:本文在介绍HTTP请求过程时,会大量使用Fiddler来分析具体的请求过程。
上图是一个普通的ASPX页面的请求过程,说它普通是因为:我在创建这个页面后,没对它做任何缓存方面的处理。
图片中我们可以可以看到服务器的响应状态为:HTTP/1.1 200 OK
,这是一个服务器成功响应的标志。
另外,要注意图片中的Cache
响应头部分,我之所以就红线框出来,是想提醒您注意这块的内容将在后面的小节中发生改变, 到时候请注意对比它们。而这里所反映的情况其实也只是默认值而已,它并不表示此页面需要缓存。
缓存页的请求过程
下面再来看一个缓存页面的请求过程:
对比上一张图片中可以看出,现在多了【max-age】,【Expires】以及【Last-Modified】这三个响应头。
这三个头的含义如下:
1. max-age,Expires:要表达的意思基本差不多。max-age表示某次HTTP的响应结果应该缓存多少秒。
而Expires是说某次HTTP的响应结果应缓存到什么时候过期,此时间是一个UTC时间。
另一个Date头表示HTPP响应的发出时间,我们可以发现 Date + max-age = Expires
2. Last-Modified:服务端告诉客户端本次响应返回的HTTP文档的最后修改时间。这个头与304的实现有关,后面再来解释。
分析了HTTP请求过程后,我们再来看一下服务端的页面是什么样子的:
<%@ Page Language="C#" %>
<%@ OutputCache Duration="10" VaryByParam="None" %>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>http://www.cnblogs.com/fish-li/</title>
</head>
<body>
<p>页面生成时间:<%= DateTime.Now.ToString() %></p>
<p><a href="Default.aspx">回到首页</a>
<a href="<%= Request.RawUrl %>">刷新本页</a>
</p>
</body>
</html>
注意:上面代码中最关键的一行代码为:
<%@ OutputCache Duration="10" VaryByParam="None" %>
正是由于使用了这个OutputCache指令,最后才会输出上面那几个响应头,用来告诉浏览器此页面需要缓存10秒钟。
说到这里,可能有些人想有疑惑了:缓存页在什么时候会起到什么作用呢?
为了演示缓存页所带来的现实意义,我将点击页面的这些链接并以截图的形式来说明:在一系列请求过程中页面的显示情况, 并以页面的显示结果来分析缓存所起的作用。
先来看看这个页面的显示截图:
页面很简单,主要是显示了页面的生成时间与一个刷新链接。 从上面提供的页面代码,我们应该能知道这个页面如果是由服务端生成的,则会显示当前的时间。
不过呢,当我一直(频繁)点击【刷新本页】那个链接时,页面的时间并没有发生改变,当我发现时间改变时,页面已显示成这个样子了:
由于测试过程中,我一直打开了Fiddler,正好我也把Fiddler监视到的请求结果截图下来了:
从Fiddler中,我看到FireFox其实只发生了二次请求,但我点击那个【刷新本页】起码超过10次。
以上的这一切,只说明一个事实:如果页面需要跳转到某个缓存页时,且那个缓存页还没过期,那么浏览器并不会发起到服务器的请求,而是使用缓存页。
小结:页面缓存所带来的好处是:缓存页面在过期前,用户通过点击跳转链接所引发的后续访问,并不会再次请求服务器。 这对服务器来说可以减少许多访问次数,因此使用这个特性可以很好地改善程序性能。
缓存页的服务端编程
前面演示了使用OutputCache指令所产生的缓存页的效果,由于那些指令需要在页面的设计阶段就写到页面上,因此显得不够灵活, 不能在运行时调整,虽然ASP.NET也允许我们使用CacheProfile来引入定义在Web.config中的配置,但配置还是没有运行时的代码灵活。 我们再来看看如何用代码来实现上面的效果。
其实用代码实现缓存页也很简单,只需要这样就可以了:
protected void Page_Load(object sender, EventArgs e)
{
Response.Cache.SetCacheability(HttpCacheability.Public);
Response.Cache.SetExpires(DateTime.Now.AddSeconds(10.0));
}
其实关键也就是对Response.Cache的调用。
注意:Response.Cache与我上篇 【细说 ASP.NET Cache 及其高级用法】博客所讲的Cache不是一回事,二者完全不相干。
Response.Cache提供:用于设置缓存特定的 HTTP 标头的方法和用于控制 ASP.NET 页输出缓存的方法。
我们还是来说前面的二段示例代码。可能有些人会想,它们最终的结果真的会是一致的吗?
要想回答这个问题,我想有必要看一下前面用OutputCache指令的那个页面最终运行的代码是个什么样子的。
在ASP.NET的临时编译目录中,我找到了前面那个文件的一个由ASP.NET处理后的版本:
private static System.Web.UI.OutputCacheParameters @__outputCacheSettings = null;
public demo1_aspx() {
// ........ 删除掉一些与Cache无关的代码
if ((global::ASP.demo1_aspx.@__outputCacheSettings == null)) {
System.Web.UI.OutputCacheParameters outputCacheSettings;
outputCacheSettings = new System.Web.UI.OutputCacheParameters();
outputCacheSettings.Duration = 20;
outputCacheSettings.VaryByParam = null;
global::ASP.demo1_aspx.@__outputCacheSettings = outputCacheSettings;
}
}
protected override void FrameworkInitialize() {
base.FrameworkInitialize();
// ........
this.InitOutputCache(global::ASP.demo1_aspx.@__outputCacheSettings);
this.Request.ValidateInput();
}
我们可以看到页面针对OutputCache指令的设置,最终会调用Page类定义一个方法中:
protected internal virtual void InitOutputCache(OutputCacheParameters cacheSettings)
那个方法实在太长,最终的处理方式也还是在调用this.Response.Cache,有兴趣的可以自己去看看那个方法。 至于这个方法的参数为什么是OutputCacheParameters,我想这个容易理解:方便将OutputCache指令的参数全部一起传入嘛。
所以,也正因为这个缘故,我们也可以直接在代码中调用Response.Cache的一些方法来实现相同的效果, 由于代码可以在运行时根据各种参数调整缓存策略,因此会更加灵活,而且可以采用基类的继承方式来简化实现。
注意:如果使用OutputCache指令再配合OutputCache Module的使用,可以实现304的效果。
什么是304应答?
通过前面的示例,我们已经看到缓存带来的好处:那就是可以减少到服务器的访问,由于不访问服务器就能显示页面,这对于服务器来说, 能减轻一定的访问压力。但是,如果用户强制刷新浏览器,那么浏览器将会忽略缓存页,直接向服务器重新发起请求。
也就是说:缓存页在用户强制刷新浏览器时会无效。
但是,我们之所以使用缓存页,是因为我们希望告诉浏览器:这些数据在一定时间内,并不会发生变化,因此根本不需要再次请求服务器了。 然而,我们不能阻止用户的行为。由于浏览器的重新访问,我们原来设想的缓存想法将会落空,最后的结果是: 页面在服务器中重新执行,产生的HTML代码将重新发送到客户端。而这一重新刷新的结果可能也是无意义的,因为数据可能根本没有发生变化, 因此得到的页面也是不可能有变化的。
再来举个简单的例子来说吧:客户端要浏览一张图片。 当浏览器第一次要访问图片时,浏览器肯定是没有它的任何缓存记录的,此时它去访问服务器,服务器也返回图片的内容了。 但由于图片可能会被多个页面所引用,而它被修改的可能性是很小的。 因此没有必要为同一浏览器的多次请求都去读取图片并返回图片的内容,这样做既影响性能也学浪费带宽。 于是,像IIS这样服务器软件针对这类静态文件的访问时,都会在响应头上输出一些标记,用来告之浏览器这个文件你可以缓存起来了。
还是回到前面所说的【用户强制刷新】问题,此时的IIS又会如何处理呢?请看下图:
注意哦,此时除了HTTP状态码变成304之外,没有任何数据返回哦。
为了让您对304应答有个深刻的印象,我截了一张状态码为200的图片响应结果:
通过这二张图片的对比,现在看清楚了吧:304和200并不只是数字上的差别,最重要的差别在于有没有返回结果。
没有返回结果,浏览器该如何显示?
您会有这样的疑虑吗?
其实不用担心,此时浏览器会使用它缓存版本来显示。也就是说:不管用户如何强制刷,服务器就是不返回结果,但仍然可以正常显示。
显然,这个效果就是我们想要的。
前面所说的缓存页遭用户强刷的问题,如果采用这种方法,就比较完美了。
不过,有一点我要提醒您:Visual Studio自带的那个WebDev.WebServer.exe
不支持304应答,所以您就不要拿它试验了,不会有结果的。
如何编程实现304应答
前面我们看到了304应答的效果。不过,在ASP.NET中,我们开发的程序,是动态页面,而不是图片, 我们更希望某个页面能以这种方式缓存一段时间,我想这个需求或许会更有意义。
下面,我就来演示如何通过编程的方式实现它。
接下来的示例中,页面的显示还是那个样,显示页面在服务器上产生的时间,时间变化了,说明页面被重新执行了。
重新截一系列的图片,我认为意义也不大,我就截一张图片展现多次强刷而产生的过程
上图反映了我多次请求某个ASPX页面的过程,从图片中可以看出,只有第一次是200的响应,后面全是304,是您所期待的结果吧。
再来看看它的实现代码吧:
public partial class Demo304 : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
DateTime dt;
if( DateTime.TryParse(Request.Headers["If-Modified-Since"], out dt) ) {
// 注意:如果是20秒内,我就以304的方式响应。
if( (DateTime.Now - dt).TotalSeconds < 20.0 ) {
Response.StatusCode = 304;
Response.End();
return;
}
}
// 注意这个调用,它可以产生"Last-Modified"这个响应头,浏览器在收到这个头以后,
// 在后续对这个页面访问时,就会将时间以"If-Modified-Since"的形式发到服务器
// 这样,上面代码的判断就能生效。
Response.Cache.SetLastModified(DateTime.Now);
}
}
虽然代码并不复杂,但我还是打算来解释一下:
在浏览器第一次请求页面时,会执行SetLastModified的调用,它会在响应时输出一个”Last-Modified”这个响应头, 然后,当浏览器再次访问这个页面时,会将上次请求所获取的”Last-Modified”头的内容 , 以”If-Modified-Since”这个请求头的形式发给服务端,此时服务器就可以根据具体逻辑来判断要不要使用304应答了。
在前面的请求图片的示例中,服务器以图片文件的最后修改时间做为”Last-Modified”发给浏览器, 浏览器在后续请求那张图片时,又以”If-Modified-Since”的形式告之服务端,此时服务端只要再次检查一下这张图片就知道图片在上次访问后有没有发生修改, 如果没有修改,当然就以304的形式告之浏览器:继续使用缓存版本。
还是前面的请求图片的示例,其实服务端还使用了另一对【请求/响应】头:
这二个头的使用方式是:服务端输出一个ETag头,浏览器在接收后,以If-None-Match的形式在后续请求中发送到服务端, 供服务端判断是否使用304应答。
“Last-Modified”与”ETag”这二者,事实上只需要使用一个就够了,关键还是看服务端如何处理它们,浏览器只是在接收后,下次再发出去而已。
不过,前面的示例代码并没有使用缓存头,事实上,也可以带上它,这样可以尽量减少对服务器的访问,毕竟用户不会一直强刷浏览器。 这二种方式虽然有较大差别,但它们绝对是可以互补的。
为了能形象的描绘缓存页(或者其它文档)的请求过程,我画了张示意图供大家参考:
如何避开HTTP缓存
前面小节中,介绍了二种方法使用浏览器的缓存。但有些时候可能反而希望浏览器能放弃它缓存的结果。 现在的浏览器都有缓存功能,尤其是对一些静态文件,比如:图片,JS,CSS, HTML文件,都能缓存。 但有时候我们需要更新CSS, JS文件呢,浏览器如果还使用它的缓存版本,显然就有问题了。 而且有些网站使用了URL重写,使原来的动态页面扩展名也变成静态的HTML文件了, 因此,仍然希望浏览器在某些时候能够不要缓存这些伪静态页面。
此时,我们就希望浏览器放弃从HTTP请求所获得的结果了。 一般说来,浏览器在处理(它认为的)静态文件时,会按照URL为kEY来保存那些缓存结果, 因此,通常的解决办法也就是修改URL,比如:原来是请求abc.js的,要改成abc.js?t=23434,后面要跟上一个参数, 让以前的缓存不起作用。至于参数t的取值可以根据文件的最后修改时间,也可以手工指定,总之只要改变它就可以了。
但是,对于伪静态的页面,我们不能再使用这种方法了,原因就不用解释了吧。
那么,可以采用在服务端输出一个响应头,通过响应头的方式告之浏览器,不要缓存此文件。 比如,可以调用这个方法:
Response.Cache.SetNoStore();
它会生成这样的响应头内容:
Cache-Control: private, no-store
许多浏览器都能识别它。还有另一种方法是设置一个已过期的过期时间。
前面所说的在URL中加额外参数的做法,在JS中也比较常用,比如 JQuery就支持让某个Ajax请求不缓存, 它的方式就是设置{cache: false},最终它便会在生成的URL中加上一个临时参数,以保证后面的请求的地址是不重复的, 最终达到避开缓存的目的。JQuery的使用太简单,我就不再给出示例代码了。