基于HTTP缓存轻松实现客户端应用的离线支持及网络优化

标签: Development Android App Cache HTTP | 发表时间:2012-12-18 16:39 | 作者:oasisfeng
出处:http://blog.oasisfeng.com

常规的客户端应用开发实践中,为了支持离线特性,往往需要引入本地数据存储并增加相应的『离线状态』逻辑分支。本地存储的大量使用对数据结构的前后向兼容设计提出了很高的要求,一旦考虑不足,往往不得不引入复杂的版本间数据升降级处理,进一步加剧开发和维护成本。而且针对『离线』与『在线』状态这两条并行的处理分支,对业务逻辑的清晰性和可维护性有一定的破坏,常常容易在后续开发中造成处理遗漏,给测试和维护带来更多的痛苦。

在此前的一个客户端开发项目中,我们另辟蹊径的借助HTTP协议层的缓存机制(Cache-Control),实现了一个简洁高效的离线支撑框架。一般HTTP缓存运用在客户端开发中大多是应对图片等静态资源的缓存,而我们更进一步将API也纳入缓存管理的模式。相比上述传统思路,它具有以下独到的优势:

  • 基本消除了离线相关的业务数据存储需求,免除了考虑数据结构前后向兼容性及版本间数据升降级处理的痛苦。
  • 大幅度减少了离线特性对现有业务实现的侵入性,只要API接口设计得当,现有特性实现只需要作微小的调整即可直接支持离线。
  • 在网络状况不佳的情况下,提供无缝的用户体验。(优先显示缓存内容,异步刷新)
  • 同时也能优化在线状态下的网络传输,减少不必要的重复网络请求。

基本的实现思路是,在API client层透明的管理所有API请求,对于占绝大部分比例的GET类(不影响业务状态的)API,根据当前网络状态智能的协调真正的网络请求与缓存的响应,实现对业务处理层基本透明的离线状态应对。这样,业务处理层的代码只需按照在线状态下的场景实现相应的业务逻辑,即可同时支持离线的场景。

下面以几个典型的业务场景为例说明这个框架的工作方式:

1. 用户信息的离线展现

假定业务后端提供了获取用户信息的API:『/api/v1/profile』,客户端启动后,会通过这个API获取用户的基本信息(如用户名、头像、积分等),并展现在主界面中。当启动主界面时,客户端发起API调用,API client首先判断本地是否已缓存了此前该API的响应,如果已有缓存则直接返回缓存的响应。这个缓存响应对业务处理层的代码而言,跟一个正常的服务端响应基本没有差别,因此只需当作在线状态处理即可。

在返回缓存响应的同时,API client还会根据当前的网络可用性及数据时效性,决定是否发起一次异步的重新请求。在这个例子中,我们可以为Profile API配置一个默认的时效期,比如『10分钟』。如果缓存的响应数据尚在时效期内,则不再发出这一额外的异步请求。这个策略有助于减少在线状态下客户端的重复请求频率,降低流量浪费。基于这一策略,我们在业务实现中完全抛弃了Profile数据的本地存储,简化了实现流程,在每次界面展现时均发起API调用,获取实际数据,让HTTP缓存同时充当业务数据存储的角色。这样就完全不必担心本地存储的数据结构前后向兼容和升降级问题了,而服务端的API接口URL协议中含有的API版本部分(如『/api/v2/profile』)确保了不同版本API响应的隔离。

时效的引入必然涉及到数据的一致性风险,因此除了根据业务场景合理为不同API设置各自的时效期外,客户端的业务逻辑中还需要在进行了显性影响此数据的操作后,使用『强制网络请求』的方式忽略缓存发起API调用。典型的场景如用户修改了头像或昵称后的个人信息刷新。

另一个应对数据时效性延滞的策略是采用服务端主动push数据变化的方式:为API设置一个较长的默认时效期(比如1小时),当数据发生变化时,服务端主动push一次响应。为了兼容API client使用的HTTP缓存机制,可以采用前端开发中比较成熟的『长连接挂起响应body』的push实现方式。

以上是最为常见且相对简单的离线需求场景,下面再以一个稍微复杂的例子说明这个框架的高级用法。

2. 可翻页清单的离线浏览

这是一个相对比较复杂的场景,『可翻页』意味着相关的清单数据具有关联延续性,比如搜索结果页、消息收件箱。支持此类数据的无缝离线体验,对API的设计会有一定的要求,才能确保URL在不同起点或页长下的一致性,使缓存能正确发挥作用。(其实这也是HTTP协议中URL结构的一个最初约束——『resource path』,只不过发展到今天,很多Web应用的URL规划早已忽视了这些基本原则)

搜索结果页和消息收件箱分别代表了两种不同的翻页需求场景,前者是起点固定,向后延续;而后者是终点固定,起点浮动。(假定清单的相对顺序在短期内不变)

(1) 搜索结果页

先说搜索结果页。确保URL一致性的最简单办法是固定页长(由服务端控制),URL中传递页码,例如:『/search/iphone+5/page/2』。这样,就可以保持离线搜索时的URL一致性。

这时需要解决的另一个关键问题是缓存的连带失效。对于没有关联延续性的单一页面,可以直接通过失效期和覆盖缓存的方式控制失效,但引入关联延续性之后,就需要连带失效多个关联页面的缓存了。比如在重新搜索相同的关键字后,原先缓存的后续几页就必须连带失效,以避免出现类似『新的第一页+老的第二页』所导致的清单内容混乱。

解决这个问题的方式有很多,在经过广泛的研究后,我们选择了使用Vary+特殊header的策略。这个header在每次刷新第一页时由客户端重新生成一个随机的token,并在连续的翻页期间保持不变。这个token的作用相当于一个session标识,借助HTTP协议的Vary header确保不同session的页面自动失效。这个实现方式可以很好的兼容HTTP协议的标识实现,而且对服务端没有特别的开发需求。

(2)信息收件箱

信息收件箱相比搜索结果页的复杂性在于,每次浏览的起点可能不固定(假定我们以常见的时间倒序方式浏览),但已缓存的清单条目具有相对不变性(那些旧消息)。如果我们仍然采用URL中传递页码的策略,那么就可能出现刷新后因新条目增加而顺延现有条目所造成的『页面错位』,倘若简单粗暴的连带失效后续页面,就太浪费实际上可缓存的不变内容了。

在这样的场景下,设计一个可充分利用缓存的API URL具有相当高的挑战。在经过多次尝试和摸索之后,我们最终选择了『等间隔区间』读取的API URL策略,形如:『/api/messages?last_id=120』这里的『last_id』是以20为间隔的最近区间终点ID,服务端返回ID在120之上的最多20条消息(比如121~126)。如果用户向后翻页,则发起的API请求为『/api/messages?last_id=100』,此时服务端返回ID从101~120的20条消息。首次请求时,可以不携带『last_id』参数,而第一次翻页时取首页ID范围内为20整倍数的ID作为这次请求的『last_id』。例如首页获取的ID范围为『117~126』,则第一次翻页时请求『last_id=120』。为了优化最终用户的体验,实际显示在UI中的消息清单仍是以最新消息开始的每20条分页,比如此例中的『126~117』、『116~107』,UI逻辑层对偏移映射进行了的包装。

3. 具体实现层面

Android和iOS下均有直接可用的成熟框架支持HTTP Cache机制。iOS的NSURLCache从2.0开始就提供给开发者,而Android的HttpResponseCache要到4.0版本才能直接使用。不过开源社区已经有 其back-port项目,可以运用在Android 2.x版本中。

需要特别一提的是,Android虽然从4.0版本开始提供了HttpResponseCache,但其中有 一个对IO性能影响较大的问题,直到4.2版本才得以解决。因此,建议运行在4.2之前的版本上时,仍旧使用开源社区的back-port(已包含了解决上述性能问题的补丁)。

业务框架层只需少量的工作就可以将其集成到现有的API library中,考虑到不同的API library接口设计,可能需要引入适当的调整以支持『优先缓存、异步请求』的机制。以我们Android ApiClient的部分片段为例:

	switch (cache_policy) {
	case NeverFromCache:
		connection.addRequestProperty("Cache-Control", "no-cache");
		break;
	case OnlyFromCache:
		connection.addRequestProperty("Cache-Control", "only-if-cached, max-stale=" + KMaxStale);
		break;
	case Default:	// Controlled by server response header
		break;
	}

这是直接指挥HTTP cache的部分,其中的三种策略(NeverFromCache、OnlyFromCache和Default)需要结合业务场景作出区分选择。

通常的原则,我们应当将API的缓存策略交由服务端根据业务需求确定,这时直接使用『Default』即可,减少客户端对业务变化的依赖。对于服务端而言,可以为不同的API指定不同的缓存策略,分别通过『Cache-Control』header指定:

不允许客户端缓存:『Cache-Control: no-cache』
在指定时效内缓存:『Cache-Control: max-age=3600』 (1小时内有效)

注:这里服务端指定的是『在线』时的缓存时效策略,影响的是客户端在主动失效缓存前可以不必请求新数据而直接使用缓存的时限。此处不必担心离线条件下超出时效的数据不可用,因为客户端可以通过前述的『max-stale』在『max-age』基础上延长时效性。(通常客户端可将『max-stale』设置的足够大以保证缓存的数据始终可用。

如果只是实现简单的离线支持,不考虑在线期间的缓存省流,那么服务端并不需要作任何调整,客户端的相应逻辑也很简单:

	cache_policy = is_offline ? OnlyFromCache : Default

但如果App中包含有显式展示最新状态的界面(陈旧或缓存的信息可能影响用户判断)时,则需要使用『NeverFromCache』。

除了上面提到的特殊场景外,在上层的业务代码中一般大部分的业务需求均不必涉及到cache策略的选择,往往只需为离线状态增加一些全局性的体验优化即可(如无缓存时的友好提示)。

相关 [http 缓存 客户端] 推荐:

基于HTTP缓存轻松实现客户端应用的离线支持及网络优化

- - Oasis Feng
常规的客户端应用开发实践中,为了支持离线特性,往往需要引入本地数据存储并增加相应的『离线状态』逻辑分支. 本地存储的大量使用对数据结构的前后向兼容设计提出了很高的要求,一旦考虑不足,往往不得不引入复杂的版本间数据升降级处理,进一步加剧开发和维护成本. 而且针对『离线』与『在线』状态这两条并行的处理分支,对业务逻辑的清晰性和可维护性有一定的破坏,常常容易在后续开发中造成处理遗漏,给测试和维护带来更多的痛苦.

HTTP缓存算法

- - PHP源码阅读,PHP设计模式,PHP学习笔记,项目管理-胖胖的空间
HTTP协议缓存的目标是去除许多情况下对于发送请求的需求和去除许多情况下发送完整请求的需求. 以不发送请求或减少请求传输的数据量来优化整个HTTP架构,此目标的实现可以产生如下好处:. 降低对原始服务器的请求量. 减少了传送距离,降低了因为距离而产生的时延. 缓存基本处理过程包括七个步骤. 接收 – 缓存从网络中读取抵达的请求报文.

合理使用 HTTP 缓存

- - Harttle Land
HTTP 缓存 使用 HTTP 缓存:Etag, Last-Modified 与 Cache-Control 一文对 HTTP 协议的缓存机制进行了简单的讨论,本文集中讨论实际操作中的最佳实践. 谨慎地使用过期时间,最好配合 MD5 一起使用. Last-Modified,动态内容采用. 分离经常变化的部分,也会提高缓存的命中率.

http协议:Web前端-HTTP Cache-control/浏览器缓存(转)

- - 互联网 - ITeye博客
HTTP协议分别在 1.0 / 1.1 两个时代推出了 Expires / Cache-control 两种cache策略,这里我们无需了解全部的细节,无需记住整个RFC内容,但是当我们需要使用HTTP cache策略时,我们需要注意以下细节:. Expires 是HTTP 1.0 那个时代的东西了,目前来看,可以不使用了,因为HTTP 1.0 的user agent占有率在 0.1% 以下(我们主要面向的web浏览器均默认使用HTTP 1.1),Cache-control 是 HTTP 1.1 的新特性,也是我们主要做文章使用cache策略的工具.

浅析http协议与缓存

- - 博客园_Ruby's Louvre
最近几天在复习http协议中headers,缓存等相关知识,发现些新知识点. 这篇文章注重结合PHP去理解这些内容,也就是比较注重实践部分. 一、http headers. NO1:对于web应用,用户群在客户端 (各种浏览器)点击任何一个连接向服务器发送http请求,这过程肯定需要3次握手,建立连接,服务器响应返回数据.

JBoss_053:使用JDG外部缓存HTTP Session

- - 热爱Java ,热爱生活
环境:JBoss EAP 6.4.0 + JBoss Data Grid 6.5.0. JBoss Data Grid 6.5.0 有个新特性:可以把JDG 作为外部缓存来存储HTTP Session,这样做的好处是:. (1)减轻了应用服务器的内存压力以及集群节点间的 Session 复制. (2)利用JDG的横向扩展能力,可以容纳更多的 Session 对象.

HTTP中的ETag在移动客户端的应用

- - SegmentFault 最新的文章
绝大多数移动客户端在设计网络模块时,都会选用HTTP作为客户端和服务端通信的网络协议. 随着业务的不断发展以及用户量的持续增长,整个客户端的稳定性和性能会逐渐成为关注的焦点,其中网络的性能优化更是重中之重,本文介绍的 ETag 缓存技术,可以在缓存数据的同时做到数据的实时更新,适用于对数据实效性要求较高的业务.

web基础-web工作原理,http协议,浏览器缓存

- - 浏览器 - 互联网 - ITeye博客
4,cookie和session. 平时用浏览器,输入网址后回车,页面响应我们想要浏览的内容,简单操作的背后蕴涵了什么原理. 当输入url回车后,客户端(浏览器)会去请求DNS服务器,通过DNS获取域名对应的IP地址,然后通过这个地址找到对应的服务器,要求建立TCP连接,建立连接,客户端发送httpRequest(请求包)后,服务器接收并开始处理请求,调用自身服务,返回httpResponse(响应包),客户端收到响应包后开始渲染body主体,等到全部接收,断开与该服务器端的TCP连接.

聊聊高并发系统之HTTP缓存(转)

- - 企业架构 - ITeye博客
原文网址:http://jinnianshilongnian.iteye.com/blog/2319573. 最近遇到很多人来咨询我关于浏览器缓存的一些问题,而这些问题都是类似的,因此总结本文来解答以后遇到类似问题的朋友. 因本文主要以浏览器缓存场景介绍,所以非浏览器场景下的一些用法本文不会介绍,而且本文以chrome为测试浏览器.

(转)Oracle JDBC Memory Management中的客户端缓存

- - jackyrong
  对于oracle jdbc中,一个容易忽略的参数是:prepared-statement-cache-size,这次转来. http://xulingbo.net/?p=109这篇好文,详细讲解了这个参数用法. 从Oracle10g开始在JDBC驱动中,增加了对执行每个Statement的缓存.