[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
   3:     //其他成员
   5:  
   7:     void CheckConditionalRetrieve(int entityTag);
   9:     string entityTag);
  11:     public DateTime? IfModifiedSince { get; }
  13: }

IncomingWebRequestContext还具有IfModifiedSince和IfNoneMatch这两个只读属性,它们分别返回请求消息的If-Modified-Since和if-none-match报头。而服务端针对回复消息的ETag报头的设置可以通过OutgoingWebResponseContext的四个SetETag方法来完成。

   2: {
   5:     void SetETag(   8: }

对于客户端来说,它可以通过当前WebOperationContext的IncomingResponse属性得到表示入栈回复上下文的IncomingWebResponseContext对象,并通过其只读属性ETag获取当前HTTP回复的ETag报头。

string ETag { get; }
class OutgoingWebRequestContext
string IfNoneMatch { get; set; }
class EmployeesService : IEmployees
   5:     {
   7:         "002",1)">"李四",1)">"人事部",1)">"G6"}
public IEnumerable<Employee> GetAll()
  11:         int hashCode = employees.GetHashCode();
  13:         WebOperationContext.Current.OutgoingResponse.SetETag(hashCode);
  15:     }
static void GetAllEmployees(string ifNoneMatch,out string eTag)
   4:     Uri address = new Uri("http://127.0.0.1:3721/employees/all");
if (!string.IsNullOrEmpty(ifNoneMatch))
   8:         request.Headers.Add(HttpRequestHeader.IfNoneMatch,ifNoneMatch);
  10:     request.Method = "GET";
  12:     {
  14:         eTag = response.Headers[HttpResponseHeader.ETag];
  16:             new StreamReader(response.GetResponseStream(),Encoding.UTF8))
  18:             Console.WriteLine(reader.ReadToEnd() + Environment.NewLine);
  20:     }
  22:     {
  24:         if (null == response)
  26:             throw;
  28:         if (response.StatusCode == HttpStatusCode.NotModified)
  30:             Console.WriteLine("服务端数据未发生变化");
  32:         }
  34:     }
string etag;
   3: GetAllEmployees("",1)">out etag);
   5: GetAllEmployees(etag,1)" id="lnum6">   6: Console.Read();

在服务成功寄宿的情况下调用这段程序会在控制台上输出如下的结果,从中我们可以看到员工列表数据只在第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>
   4: 第2次服务调用: