[WCF REST] 提高性能的一个有效的手段:条件资源获取(Conditional Retrieval)

本文介绍WCF REST服务中实现条件获取的方法,包括HTTP协议支持的两种资源改变判断机制,以及通过WCF编程模型进行条件获取的具体实现。

原文地址: http://www.cnblogs.com/artech/archive/2012/02/13/wcf-rest-conditional-retrieval.html


条件获取(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: 服务端数据未发生变化

以下转自: http://blog.youkuaiyun.com/fangxinggood/article/details/6540307

缓存是Web开发中的重要技术,在开发RESTful服务也需要重视。合理的利用缓存可以大大提高服务的响应能力。从技术实现上,有客户端缓存和服务端缓存两大部分组成。而无论在哪边进行缓存,都需要一些数据来比较是否过期,Http协议中控制缓存的规则有:Cache-Control, ETag, Expires, Last-Modified。Expires是一种无条件缓存(通过过期时间控制),Last-Modified,ETag是一种有条件缓存(通过数据的标识(时间或者ID)来控制。 如果是无条件缓存客户端浏览器会检查Expires过期时间判断是否发出请求,如果是有条件客户端缓存,则会提交Last-Modified-Since或者ETag供服务端检查是否发生变化,返回304(Not Modified)提示客户端查询的数据没有变化,这需要客户端自己保留缓存。如果是服务端缓存,则返回200(OK)但数据从服务端自己的缓存中读取。另外通常说的缓存策略主要是针对查询即GET操作而言的。 不同的缓存策略有不同的适用场景,对于WCF REST来说客户端缓存需要在客户端额外的控制编码。服务端缓存则要考虑数据量尽量只对共享数据进行缓存,如果对于每个客户端的私有数据都缓存对存储空间来说是一个考验。 来看看有条件客户端缓存的示例: 服务端提供一个 GetTasks 方法,返回 Task 数组。一个 AddTask 方法,添加Task。每次添加Task都会修改_lastModifed(DateTime)

[c-sharp] view plain copy
  1. [ServiceContract]  
  2. [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]  
  3. [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]  
  4. public class Service1  
  5. {  
  6.     private static List<Task> _tasks = null;  
  7.     private static DateTime? _lastModified = null;  
  8.     static Service1()  
  9.     {  
  10.         _tasks = new List<Task>   
  11.         {   
  12.             new Task { ID="001", Content="Task1", Title="Title1"},  
  13.             new Task { ID="002", Content="Task2", Title="Title2"},  
  14.             new Task { ID="003", Content="Task3", Title="Title3"},  
  15.         };  
  16.         _lastModified = DateTime.UtcNow;  
  17.     }  
  18.     [WebGet(UriTemplate="Tasks", ResponseFormat=WebMessageFormat.Json)]  
  19.     public List<Task> GetTasks()  
  20.     {  
  21.         var req = WebOperationContext.Current.IncomingRequest;  
  22.         var resp = WebOperationContext.Current.OutgoingResponse;  
  23.         var modifiedSince = req.IfModifiedSince;  
  24.         if (modifiedSince.HasValue)  
  25.             req.CheckConditionalRetrieve(_lastModified.Value);  
  26.         resp.LastModified = _lastModified.Value;  
  27.         return _tasks;  
  28.     }  
  29.     [WebInvoke(UriTemplate = "NewTask", Method = "POST", RequestFormat = WebMessageFormat.Json)]  
  30.     public void AddTask(Task task)  
  31.     {  
  32.         lock (_tasks)  
  33.         {  
  34.             task.ID = (_tasks.Count + 1).ToString("000");  
  35.             _tasks.Add(task);  
  36.             _lastModified = DateTime.UtcNow;  
  37.         }  
  38.         WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HttpStatusCode.Accepted;  
  39.     }  
  40.     //[WebGet(UriTemplate = "Tasks", ResponseFormat = WebMessageFormat.Json)]  
  41.     //[AspNetCacheProfile("CacheFor5Seconds")]  
  42.     //public List<Task> GetTasks()  
  43.     //{  
  44.     //    return _tasks;  
  45.     //}  
  46. }  
  47. public class Task  
  48. {  
  49.     public string ID { getset; }  
  50.     public string Title { getset; }  
  51.     public string Content { getset; }  
  52. }  
通过浏览器连续调用2次:http://localhost:52533/Service1/tasks 用 Fiddler 拦截可以看到 第一次返回200, 第二次返回304而且没有Response Body (hoho,这样网络传输的代价也减少了) 上面的代码中:CheckConditionalRetrieve 方法的一个重载接受上次修改的日期并根据请求的 If-Modified-Since 标头检查该日期。如果该标头存在并且自该日期以来尚未修改资源,将引发 WebFaultException 并返回 HTTP 状态代码 304。而 CheckConditionalRetrieve 之后的“查询操作”(return _tasks)实际也就没有进行。 上面只是浏览器的行为结果,下面看看客户端如何模拟调用: Tip: WebClient 中对于 Last-Modified-Since 有限制,因此无法使用 WebClient.Headers.Add() 方法来添加头信息。(会抛出异常)
[c-sharp] view plain copy
  1. var url = "http://localhost:20000/service1/tasks";  
  2. var req = WebRequest.Create(url) as HttpWebRequest;  
  3. req.Method = "GET";  
  4. if (!string.IsNullOrEmpty(lastModified))  
  5.     req.IfModifiedSince = DateTime.Parse(lastModified);  
  6. var result = "";  
  7. using (var resp = req.GetResponse())  
  8. {  
  9.     lastModified = resp.Headers["Last-Modified"];  
  10.     using (var sr = new System.IO.StreamReader(resp.GetResponseStream()))  
  11.     {  
  12.         result = sr.ReadToEnd();  
  13.     }  
  14. }  
  15. var tasks = JsonConvert.DeserializeObject<List<Task>>(result);  
  16. if (tasks == nullreturn;  
  17. tasks.ForEach(t => Console.WriteLine(t));  

这段代码从Response里取得 Last-Modified,并添加到下一次请求的 Last-Modified-Since 中,如果服务端检查发现数据没有变化,则在 GetResponse() 时抛出 WebException (No Modify)。很显然这需要我们自己在客户端维护这样的数据,以便在发现 No Modify 时可以使用。 这无疑会加大客户端编码的复杂程度,我想实际运用时也可以把“数据没有发生变化”的异常抛给UI直接显示。 这里又引申出服务端缓存的概念,客户端无论如何都希望获得200(OK)的Response。在 WCF 4.0 里利用 ASP.NET 服务的兼容模式,还可以利用 [AspNetCacheProfile] 特性。上面的 GetTasks 方法修改如下:

[c-sharp] view plain copy
  1. [WebGet(UriTemplate = "Tasks", ResponseFormat = WebMessageFormat.Json)]  
  2. [AspNetCacheProfile("CacheFor5Seconds")]  
  3. public List<Task> GetTasks()  
  4. {  
  5.     return _tasks;  
  6. }  
CacheFor5Seconds 对应的在 Web.config 中加上配置:
[xhtml] view plain copy
  1. <system.web>  
  2.   <compilation debug="true" targetFramework="4.0" />  
  3.   <caching>  
  4.     <outputCacheSettings>  
  5.       <outputCacheProfiles>  
  6.         <add name="CacheFor5Seconds" duration="5" varyByParam="none" />  
  7.       </outputCacheProfiles>  
  8.     </outputCacheSettings>  
  9.   </caching>  
  10. </system.web>  
缓存配置文件中最重要的特性是 cacheDurationvaryByParam。这两个特性都是必需的。cacheDuration 设置应缓存响应的时间(以秒为单位)。使用 varyByParam 可指定用于缓存响应的查询字符串参数。对于使用不同查询字符串参数值发出的所有请求,将单独进行缓存。例如,对 http://MyServer/MyHttpService/MyOperation?param=10 发出初始请求后,使用同一 URI 发出的所有后续请求都将返回已缓存的响应(只要缓存持续时间尚未结束)。对于形式相同但具有不同参数查询字符串参数值的类似请求的响应,将单独进行缓存。如果不需要此单独缓存行为,请将 varyByParam 设置为“none”。 客户端:
[c-sharp] view plain copy
  1. // 获取Tasks  
  2. static void GetTasks()  
  3. {  
  4.     var url = "http://localhost:20000/service1/tasks";  
  5.     var client = new WebClient();  
  6.     var result = client.DownloadString(url);  
  7.     var tasks = JsonConvert.DeserializeObject<List<Task>>(result);  
  8.     if (tasks == nullreturn;  
  9.     tasks.ForEach(t => Console.WriteLine(t));  
  10.     Console.WriteLine("------");  
  11. }  
  12. // 添加Tasks  
  13. static void AddTask()  
  14. {  
  15.     var url = "http://localhost:20000/service1/NewTask";  
  16.     var client = new WebClient();  
  17.     var task = new Task { ID = "", Title = "Title_Test", Content = "Content_Test" };  
  18.     var json = JsonConvert.SerializeObject(task);  
  19.     client.Headers["Content-Type"] = "application/json";  
  20.     client.UploadString(url, "POST", json);  
  21. }  
  为了证明服务端缓存的效果,我在1次GetTasks()之后,执行一次AddTask(),再每隔1秒进行一次GetTasks()连续两次,再隔4秒进行一次GetTasks()。这样,前3次都在AddTask()之后5秒内查询,最后一次是在AddTask()之后第6秒查询。 
[c-sharp] view plain copy
  1. GetTasks();  
  2. AddTask();  
  3. Thread.Sleep(1000);  
  4. GetTasks();  
  5. Thread.Sleep(1000);  
  6. GetTasks();  
  7. Thread.Sleep(4000);  
  8. GetTasks();  
运行结果: 可以看到,虽然AddTask成功了,但是在缓存期内,服务端并没有真正的执行“查询操作"(return _tasks),返回的只是缓存数据。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值