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

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

ExpandedBlockStart.gif 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代码:

ExpandedBlockStart.gif 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,  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
    }
}

 

 

ExpandedBlockStart.gif 应用示例
[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

 

转载于:https://www.cnblogs.com/wJiang/archive/2010/04/04/1704360.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值