[WCF REST] 提高性能的一個有效的手段:條件資源獲取(Conditional Retrieval)


條件獲取(Conditional Retrieval)旨在解決這樣的問題:客戶端獲取某個資源並對其進行緩存,當再次獲取相同資源時,如果資源數據與之前獲取的一致,則不再返回真正的資源數據,而是在回復中設置一個“標識”表明獲取的資源並未發生改變。[源代碼從這里下載]

一、 HTTP對條件獲取的支持

HTTP對條件獲取提供了原生的支持。具體的實現是這樣的:服務端接收到客戶端針對某個資源的第一次獲取請求時,除了將資源數據作為HTTP回復主體返回之外,還會設置一個叫做ETag的回復報頭。這個ETag與資源本身關聯並且可以對資源進行對等性判斷,比如我們可以將資源內容的哈希碼作為這個ETag報頭。

客戶端接收到資源后對其進行緩存,並從回復中獲取到這個ETag報頭值。當再次對相同的資源進行請求時,它會為HTTP請求添加一個名為If-None-Match報頭,而該報頭的值就是這個緩存的ETag值。服務端接收到該請求之后會通過If-None-Match請求報頭確認最新的資源數據是否與該報頭值代表的數據一致,如果一致則回復一個狀態為“304 (Not Modified)”的空消息,否則將新的資源置於回復消息的主體並附上基於新資源數據的ETag報頭。

除此之外,條件獲取還支持另一種基於“最近修改時間”的資源改變判斷機制。這種機制也很簡單:服務端記錄下資源最近一次修改的時間,並被作為客戶端第一次訪問請求的ETag回復報頭。客戶端針對相同資源的后續請求會將此ETag表示的時間作為一個名為If-Modified-Since的報頭,而服務端則將該報頭的時間和資源最近一次修改的時間進行比較從而確定請求的資源是否被改變。如果資源尚未改變則同樣回復以狀態為“304 (Not Modified)”的空消息,否則將新的資源置於回復消息的主體並附上新的ETag報頭。條件獲取僅僅針對方法類型為GET和HEAD的HTTP請求。

二、 WebOperationContext與條件獲取

對於Web HTTP編程模型來說,通過當前WebOperationContext可以很容易地進行條件獲取的檢測和相相關HTTP報頭的設置和獲取。具體來說,服務端通過表示入棧請求上下文的IncomingWebRequestContext對象的CheckConditionalRetrieve方法進行條件獲取的檢測。其中參數類型為DateTime的重載用采用“最近修改時間”的資源改變判斷機制。如果確資源尚未改變,則直接拋出一個HTTP狀態為NotModified的WebFaultException,並將lastModified參數表示的時間作為回復消息的ETag報頭。

對於其他的4個CheckConditionalRetrieve方法,作為參數的entityTag(ETag)將與請求消息的If-None-Match進行比較,如果不一致也會拋出HTTP狀態為NotModified的WebFaultException,並將該參數值作為回復消息的ETag報頭。

   1: public class IncomingWebRequestContext
   2: {    
   3:     //其他成員
   4:     public void CheckConditionalRetrieve(DateTime lastModified);
   5:  
   6:     public void CheckConditionalRetrieve(Guid entityTag);
   7:     public void CheckConditionalRetrieve(int entityTag);
   8:     public void CheckConditionalRetrieve(long entityTag);
   9:     public void CheckConditionalRetrieve(string entityTag);
  10:  
  11:     public DateTime? IfModifiedSince { get; }
  12:     public IEnumerable<string> IfNoneMatch { get; }
  13: }

IncomingWebRequestContext還具有IfModifiedSince和IfNoneMatch這兩個只讀屬性,它們分別返回請求消息的If-Modified-Since和If-None-Match報頭。而服務端針對回復消息的ETag報頭的設置可以通過OutgoingWebResponseContext的四個SetETag方法來完成。

   1: public class OutgoingWebResponseContext
   2: {
   3:     //其他成員
   4:     public void SetETag(Guid entityTag);
   5:     public void SetETag(int entityTag);
   6:     public void SetETag(long entityTag);
   7:     public void SetETag(string entityTag);
   8: }

對於客戶端來說,它可以通過當前WebOperationContext的IncomingResponse屬性得到表示入棧回復上下文的IncomingWebResponseContext對象,並通過其只讀屬性ETag獲取當前HTTP回復的ETag報頭。

   1: public class IncomingWebResponseContext
   2: {
   3:     //其他成員
   4:     public string ETag { get; }
   5: }

如果客戶端需要為請求設置If-Modified-Since和If-None-Match報頭,則可以通過當前WebOperationContext的OutgoingRequest屬性得到表示出棧請求上下文的OutgoingWebRequestContext對象,然后分別設置IfModifiedSince和IfNoneMatch屬性即可。

   1: public class OutgoingWebRequestContext
   2: {
   3:     //其他成員
   4:     public string IfModifiedSince { get; set; }
   5:     public string IfNoneMatch { get; set; }
   6: }

需要注意的是,如果采用WCF客戶端進行服務調用,一旦接收到狀態為“304(Not Modified)”的回復會拋出如下圖所示的ProtocolException異常,並提示“遠程服務器返回了意外響應: (304) Not Modified”。

image

三、實例演示:創建基於條件獲取的REST服務

接下來我們按照條件獲取的方式來改造之前演示的用於管理員工信息的EmployeesService。假設我們的員工數量比較多,用於獲取所有員工列表的GetAll操作將會返回一個龐大的數據。如果客戶端對第一次獲取到的員工列表進行緩存,那么對有后續針對GetAll操作的請求,在員工信息沒有任何改變的情況下服務端只需要回復一個狀態為304(Not Modified)的HTTP消息即可。

為此我們對EmployeesService的GetAll操作方法進行了如下的改造:我們通過當前WebOperationContext得到表示入棧請求上下文的IncomingWebRequestContext對象,並調用其CheckConditionalRetrieve進行條件獲取檢驗,而傳入的參數是最新員工列表對象的哈希碼。在返回員工列表之前我們將此哈希碼作為了回復消息的ETag報頭。

   1: public class EmployeesService : IEmployees
   2: {
   3:     //其他成員
   4:     private static IList<Employee> employees = new List<Employee>
   5:     {
   6:         new Employee{ Id = "001", Name="張三", Department="開發部", Grade = "G7"},    
   7:         new Employee{ Id = "002", Name="李四", Department="人事部", Grade = "G6"}
   8:     };
   9:     public IEnumerable<Employee> GetAll()
  10:     {
  11:         int hashCode = employees.GetHashCode();
  12:         WebOperationContext.Current.IncomingRequest.CheckConditionalRetrieve(hashCode);
  13:         WebOperationContext.Current.OutgoingResponse.SetETag(hashCode);
  14:         return employees;
  15:     }
  16: }

我們通過手工發送HTTP請求的方式來調用EmployeesService的GetAll操作,為此我們創建了如下一個GetAllEmployees方法。該方法的參數ifNoneMatch和eTag分別表示請求消息的If-None-Match報頭和回復消息的ETag報頭。我們通過調用HttpWebRequest的靜態方法Create基於服務操作地址創建一個HttpWebRequest對象,並設置該請求的If-None-Match報頭的HTTP方法(GET)。

我們通過調用HttpWebRequest對象的GetResponse發送請求並得到回復,在打印回復內容之前我們獲取了回復的ETag報頭。在回復狀態為“304 (Not Modified)”的情況下,GetResponse方法會 拋出一個WebException異常,所以我們對該類型的異常進行的捕獲。如果WebException異常的StatusCode屬性返回的HTTP狀態是我們預知的NotModified,則意味着獲取的員工列表未曾改變,於是我們在控制台上打印“服務端數據未發生變化”字樣。

   1: static void GetAllEmployees(string ifNoneMatch, out string eTag)
   2: {
   3:     eTag = ifNoneMatch;
   4:     Uri address = new Uri("http://127.0.0.1:3721/employees/all");
   5:     var request = (HttpWebRequest)HttpWebRequest.Create(address);
   6:     if (!string.IsNullOrEmpty(ifNoneMatch))
   7:     {
   8:         request.Headers.Add(HttpRequestHeader.IfNoneMatch, ifNoneMatch);
   9:     }
  10:     request.Method = "GET";
  11:     try
  12:     {
  13:         var response = (HttpWebResponse)request.GetResponse();
  14:         eTag = response.Headers[HttpResponseHeader.ETag];
  15:         using(StreamReader reader = 
  16:             new StreamReader(response.GetResponseStream(), Encoding.UTF8))
  17:         {
  18:             Console.WriteLine(reader.ReadToEnd() + Environment.NewLine);
  19:         }
  20:     }
  21:     catch (WebException ex)
  22:     {
  23:         HttpWebResponse response = ex.Response as HttpWebResponse;
  24:         if (null == response)
  25:         {
  26:             throw;
  27:         }
  28:         if (response.StatusCode == HttpStatusCode.NotModified)
  29:         {
  30:             Console.WriteLine("服務端數據未發生變化");
  31:             return;
  32:         }
  33:         throw;
  34:     }
  35: }

然后我們通過如下的代碼調用上面定義的GetAllEmployees方法進行兩次服務調用,並將第一次調用返回的ETag報頭作為第二次調用的If-None-Match報頭。

   1: string etag;
   2: Console.WriteLine("第1次服務調用:");
   3: GetAllEmployees("", out etag);
   4: Console.WriteLine("第2次服務調用:");
   5: GetAllEmployees(etag, out etag);
   6: Console.Read();

在服務成功寄宿的情況下調用這段程序會在控制台上輸出如下的結果,從中我們可以看到員工列表數據只在第1次服務調用中返回。

   1: 第1次服務調用:
   2: <ArrayOfEmployee xmlns="http://www.artech.com/" xmlns:i="http://www.w3.org/2001/XMLSchema-instance"><Employee><Department>開發部</Department><Grade>G7</Grade><Id>001</Id><Name>張三</Name></Employee><Employee><Department>人事部</Department><Grade>G6</Grade><Id>002</Id><Name>李四</Name></Employee></ArrayOfEmployee>
   3:  
   4: 第2次服務調用:
   5: 服務端數據未發生變化


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM