Web开发中的缓存技术之三:通过ETag实现缓存处理(ASP.NET MVC版)
2010-04-04 21:46 姜 萌@cnblogs 阅读(2731) 评论(2) 编辑 收藏 举报IIS已经为我们提供了其内置的缓存功能。但显得比较死板,对于更高的要求,IIS的缓存功能显然就有些不够灵活了。
在mvc风格的开发中我们可以通过Filter来定制缓存方式。
本篇介绍借助ETag响应头实现缓存,没有完美的缓存方案,这种方式能够准确判断客户浏览器缓存是否需要更新,但不会避免服务器再次生成页面的过程,它的主要用意在于避免不必要的数据传输,减少流量缓解带宽压力。
何为ETag,以及Is-Non-Match
您可以把ETag理解为HTTP通信中存在的一个附加信息,服务器产生ETag,客户机浏览器下一次再访问此页面时会在将此值放在Request Headers中的Is-Non-Match里。ETag、Is-Non-Match的一个经典用途就是用于缓存实现。下面会为您详细说明如何通过ETag在ASP.NET MVC中实现缓存处理。
关于ETag,您可以去看看W3C的说明:http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
基本原理
服务器将数据传给客户浏览器前会对页面数据进行哈希计算,并将哈希值转为Base64编码的ASCII字符以存放在Response响应头的ETag信息中。客户机浏览器在接收到来自服务器的信息后会将ETag值缓存到本地,下一次再访问这个页面时就会将此ETag值放在Request头的Is-Non-Match中,服务器仍然会生成页面,但要与此ETag值进行比较,如果相同就说明客户机浏览器中缓存的页面与即将传输的内容一致,进而就不会将重复的页面传输过去而是将状态置为304,这样就减少了不必要的带宽占用。
如何实现
我们要做的是让服务器去接受响应并生成数据,但是在将其写入到流中之前要对即将写入的内容进行检查,看其经过Hash计算的值是否与Is-Non-Match中的值相同,如果相同就将HTTP状态置为304,否则更新ETag并将数据写入到流中。为实现这个目的,我定义了一个包装流,将HttpResponseBase.Filter替换为包装流来方便我们在数据送到客户机之前获得数据。
(说明:asp.net mvc的ActionFilter与java里的那个Filter不太一样,一开始我以为执行到OnResultExecuted时写在response里的内容就已经存在了,后来才发现向Stream里写入数据的过程要在OnResultExecuted之后,于是我只好变相在自定义的ResponseWrapper这个Stream包装流里去处理response。)
效果展示
如图:用户首次第一次访问时ETag(_eTag)值为空,对生成当前内容的哈希计算结果为“s5vIKNWvqipDyVM46aVWn6QQ0Vg=”,我们在firebug中也能看到返回状态为200,响应头中设有正确的etag值。
再次刷新(注意在IE和firefox中不要按CTRL + F5刷新,否则浏览器会将缓存清除后发送请求),我们可以看到这次request头中已经包含了ETag值,如果页面内容没有改变的话计算出的ETag值与此值相同。在firefox中可以看到HTTP状态为304,ETag值保持不变。
源代码
ActionFilter-ContentCacheFilter代码
{
public class ContentCacheFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
filterContext.HttpContext.Response.Filter =
new InternalUse.ResponseWrapper(filterContext.HttpContext.Response.Filter,
filterContext.HttpContext.Request.Headers["If-None-Match"]);
}
}
}
包装流-ResponseWrapper代码:
{
public class ResponseWrapper : Stream
{
#region Fields
private Stream _innerStream;
private MemoryStream _memStream;
private string _eTag = string.Empty;
#endregion
#region Constructors
public ResponseWrapper(Stream stream, string eTag)
{
_innerStream = stream;
_memStream = new MemoryStream();
_eTag = eTag;
}
#endregion
#region Properties
public byte[] Data
{
get
{
_memStream.Position = 0;
byte[] data = new byte[_memStream.Length];
_memStream.Read(data, 0, (int)_memStream.Length);
return data;
}
}
#endregion
#region overrides of Stream Class
public override bool CanRead
{
get { return _innerStream.CanRead; }
}
public override bool CanSeek
{
get { return _innerStream.CanSeek; }
}
public override bool CanWrite
{
get { return _innerStream.CanWrite; }
}
public override void Flush()//可能会有这样一种情况:如果数据比较大则可能在未真正传输结束前就要Flush
{
var httpContext = HttpContext.Current;
string currentETag = generateETagValue(Data);
if(_eTag != null)
{
if(currentETag.Equals(_eTag))
{
httpContext.Response.StatusCode = 304;
httpContext.Response.StatusDescription = "Not Modified";
return;
}
}
httpContext.Response.Cache.SetCacheability(HttpCacheability.Public);
httpContext.Response.Cache.SetETag(currentETag);
httpContext.Response.Cache.SetLastModified(DateTime.Now);
httpContext.Response.Cache.SetSlidingExpiration(true);
copyStreamToStream(_memStream, _innerStream);
_innerStream.Flush();
}
public override long Length
{
get { return _innerStream.Length; }
}
public override long Position
{
get
{
return _innerStream.Position;
}
set
{
_innerStream.Position = value;
}
}
public override int Read(byte[] buffer, int offset, int count)
{
return _innerStream.Read(buffer, offset, count);
}
public override long Seek(long offset, SeekOrigin origin)
{
return _innerStream.Seek(offset, origin);
}
public override void SetLength(long value)
{
_innerStream.SetLength(value);
}
public override void Write(byte[] buffer, int offset, int count)
{
//_innerStream.Write(buffer, offset, count);
_memStream.Write(buffer, offset, count);
}
public override void Close()
{
_innerStream.Close();
}
#endregion
#region private Helper Methods
private void copyStreamToStream(Stream src, Stream target)
{
src.Position = 0;
int nRead = 0;
byte[] buf = new byte[128];
while((nRead = src.Read(buf, 0, 128)) != 0)
{
target.Write(buf, 0, nRead);
}
}
private string generateETagValue(byte[] data)
{
var encryptor = new System.Security.Cryptography.SHA1Managed();
byte[] encryptedData = encryptor.ComputeHash(data);
return Convert.ToBase64String(encryptedData);
}
#endregion
}
}
public class HomeController : Controller
{
[ContentCacheFilter]
//[LazyCacheFilter]
public ActionResult Index()
{
//ViewData["Message"] = DateTime.Now.ToString();
ViewData["Message"] = "this is from asp.net mvc development server";
return View();
}
public ActionResult About()
{
return View();
}
}
ResponseWrapper的实现可能略显不妥,大家有更好的方案希望多多分享哈^^
源码放在skydriver上了,下载页面链接(这是个页面链接迅雷会误认为是文件,刚发现skydriver给的直接下载链接都是带时间戳的,过了一天就不能用了。。。):