代码改变世界

Web开发中的缓存技术之三:通过ETag实现缓存处理(ASP.NET MVC版)

2010-04-04 21:46  姜 萌@cnblogs  阅读(2723)  评论(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。)

效果展示

image image

如图:用户首次第一次访问时ETag(_eTag)值为空,对生成当前内容的哈希计算结果为“s5vIKNWvqipDyVM46aVWn6QQ0Vg=”,我们在firebug中也能看到返回状态为200,响应头中设有正确的etag值。

image image

再次刷新(注意在IE和firefox中不要按CTRL + F5刷新,否则浏览器会将缓存清除后发送请求),我们可以看到这次request头中已经包含了ETag值,如果页面内容没有改变的话计算出的ETag值与此值相同。在firefox中可以看到HTTP状态为304,ETag值保持不变。

源代码

ActionFilter-ContentCacheFilter代码

ContentCacheFilter
namespace Sopaco.Lib.Web.Mvc.Performance.Cache
{

    
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代码:

ResponseWrapper
namespace Sopaco.Lib.Web.Mvc.InternalUse
{
    
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, 0128)) != 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
    }
}

 

 

应用示例
[HandleError]
    
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给的直接下载链接都是带时间戳的,过了一天就不能用了。。。):

http://cid-ebf46737d420f3e0.skydrive.live.com/self.aspx/By%20Sopaco/Sopaco.Lib.Web.Mvc.Filters/Filters.rar