原文地址:Web開發中的緩衝技術:通過ETag實現緩衝處理(Asp.Net)
作者:Selience
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代碼
ContentCacheFilter
namespace Sopaco.Lib.Web.Mvc.Filters
{
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.Filters.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
}
}
應用樣本
[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的實現可能略顯不妥,大家有更好的方案希望多多分享哈^^