WCF4.0 –- RESTful WCF Services (5) (缓存)

缓存是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)
[ServiceContract] [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)] [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] public class Service1 { private static List<Task> _tasks = null; private static DateTime? _lastModified = null; static Service1() { _tasks = new List<Task> { new Task { ID="001", Content="Task1", Title="Title1"}, new Task { ID="002", Content="Task2", Title="Title2"}, new Task { ID="003", Content="Task3", Title="Title3"}, }; _lastModified = DateTime.UtcNow; } [WebGet(UriTemplate="Tasks", ResponseFormat=WebMessageFormat.Json)] public List<Task> GetTasks() { var req = WebOperationContext.Current.IncomingRequest; var resp = WebOperationContext.Current.OutgoingResponse; var modifiedSince = req.IfModifiedSince; if (modifiedSince.HasValue) req.CheckConditionalRetrieve(_lastModified.Value); resp.LastModified = _lastModified.Value; return _tasks; } [WebInvoke(UriTemplate = "NewTask", Method = "POST", RequestFormat = WebMessageFormat.Json)] public void AddTask(Task task) { lock (_tasks) { task.ID = (_tasks.Count + 1).ToString("000"); _tasks.Add(task); _lastModified = DateTime.UtcNow; } WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HttpStatusCode.Accepted; } //[WebGet(UriTemplate = "Tasks", ResponseFormat = WebMessageFormat.Json)] //[AspNetCacheProfile("CacheFor5Seconds")] //public List<Task> GetTasks() //{ // return _tasks; //} } public class Task { public string ID { get; set; } public string Title { get; set; } public string Content { get; set; } }
通过浏览器连续调用2次:http://localhost:52533/Service1/tasks 用 Fiddler 拦截可以看到
第一次返回200, 第二次返回304而且没有Response Body (hoho,这样网络传输的代价也减少了)
上面的代码中:CheckConditionalRetrieve 方法的一个重载接受上次修改的日期并根据请求的 If-Modified-Since 标头检查该日期。如果该标头存在并且自该日期以来尚未修改资源,将引发 WebFaultException 并返回 HTTP 状态代码 304。而 CheckConditionalRetrieve 之后的“查询操作”(return _tasks)实际也就没有进行。
WCF4.0 –- RESTful WCF Services (5) (缓存)_第1张图片
WCF4.0 –- RESTful WCF Services (5) (缓存)_第2张图片


上面只是浏览器的行为结果,下面看看客户端如何模拟调用:
Tip: WebClient 中对于 Last-Modified-Since 有限制,因此无法使用 WebClient.Headers.Add() 方法来添加头信息。(会抛出异常)
var url = "http://localhost:20000/service1/tasks"; var req = WebRequest.Create(url) as HttpWebRequest; req.Method = "GET"; if (!string.IsNullOrEmpty(lastModified)) req.IfModifiedSince = DateTime.Parse(lastModified); var result = ""; using (var resp = req.GetResponse()) { lastModified = resp.Headers["Last-Modified"]; using (var sr = new System.IO.StreamReader(resp.GetResponseStream())) { result = sr.ReadToEnd(); } } var tasks = JsonConvert.DeserializeObject<List<Task>>(result); if (tasks == null) return; 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 方法修改如下:
[WebGet(UriTemplate = "Tasks", ResponseFormat = WebMessageFormat.Json)] [AspNetCacheProfile("CacheFor5Seconds")] public List<Task> GetTasks() { return _tasks; }
CacheFor5Seconds 对应的在 Web.config 中加上配置:
<system.web> <compilation debug="true" targetFramework="4.0" /> <caching> <outputCacheSettings> <outputCacheProfiles> <add name="CacheFor5Seconds" duration="5" varyByParam="none" /> </outputCacheProfiles> </outputCacheSettings> </caching> </system.web>
缓存配置文件中最重要的特性是 cacheDurationvaryByParam。这两个特性都是必需的。cacheDuration 设置应缓存响应的时间(以秒为单位)。使用 varyByParam 可指定用于缓存响应的查询字符串参数。对于使用不同查询字符串参数值发出的所有请求,将单独进行缓存。例如,对 http://MyServer/MyHttpService/MyOperation?param=10 发出初始请求后,使用同一 URI 发出的所有后续请求都将返回已缓存的响应(只要缓存持续时间尚未结束)。对于形式相同但具有不同参数查询字符串参数值的类似请求的响应,将单独进行缓存。如果不需要此单独缓存行为,请将 varyByParam 设置为“none”。

客户端:
// 获取Tasks static void GetTasks() { var url = "http://localhost:20000/service1/tasks"; var client = new WebClient(); var result = client.DownloadString(url); var tasks = JsonConvert.DeserializeObject<List<Task>>(result); if (tasks == null) return; tasks.ForEach(t => Console.WriteLine(t)); Console.WriteLine("------"); } // 添加Tasks static void AddTask() { var url = "http://localhost:20000/service1/NewTask"; var client = new WebClient(); var task = new Task { ID = "", Title = "Title_Test", Content = "Content_Test" }; var json = JsonConvert.SerializeObject(task); client.Headers["Content-Type"] = "application/json"; client.UploadString(url, "POST", json); }  
为了证明服务端缓存的效果,我在1次GetTasks()之后,执行一次AddTask(),再每隔1秒进行一次GetTasks()连续两次,再隔4秒进行一次GetTasks()。这样,前3次都在AddTask()之后5秒内查询,最后一次是在AddTask()之后第6秒查询。 
GetTasks(); AddTask(); Thread.Sleep(1000); GetTasks(); Thread.Sleep(1000); GetTasks(); Thread.Sleep(4000); GetTasks();
运行结果:
WCF4.0 –- RESTful WCF Services (5) (缓存)_第3张图片

可以看到,虽然AddTask成功了,但是在缓存期内,服务端并没有真正的执行“查询操作"(return _tasks),返回的只是缓存数据。

你可能感兴趣的:(String,浏览器,url,WCF,etag,compilation)