前言:
在web上面经常会使用大量的缓存服务器来加速一些静态页面和静态资源的访问,比如CDN服务器加速,DNS负载均衡等技术,常用的服务有nginx,squid等。那么Android客户端能不能也借助这些技术加快静态页面的访问,降低服务器负载呢?当然是可以的,本文就从代码的角度讨论如何加速Android客户端静态页面和静态资源的访问速度。
一些变化不大的静态页面和json数据没有必要每次打开都请求一遍网络,既增加了服务器压力,占用网络带宽,增加用户等待时间,也耗费了用户的网络流量,尤其在现在手机客户端用户一半以上都在使用流量上网,为用户节省流量是保住用户的一个关键。所以,做好Android APP的本地网络缓存非常的重要。
okhttp是一个非常优秀的http网络框架,它可以不仅可以实现网络文件下载后的自动缓存至sd卡,还可以对普通的以json字符串为body的GET请求。Android客户端上常用的网络请求缓存我把它分为两类,第一大类是需要联网才能调用的缓存,第二种是无条件的缓存,在缓存过期前只要调用相同的url就会使用上次获得的结果。
做这项研究始于一个偶然的bug,在公司开发项目的时候,为了方便,搭建了一个linux的代理服务器,使用的软件是polipo,然后发现app上一些刷新功能不好用了,现象是每次刷新时间特别短,几乎瞬间完成,但是根本没有取得最新的数据。从以往的经验来看,绝对不会这么快就能完成一次网络请求。当把wifi关掉切换4g或者重新登录,刷新功能又恢复正常。于是我怀疑是http代理服务器的缓存所致。既然如此,不如直接发回http代理服务器的缓存功能来缓存一些静态页面,达到CDN一样的网络加速功能。
给大家推荐一篇文章科普一下基础知识,并盗图一张
http://www.codeceo.com/article/http-cache-control.html
所需工具:
1、局域网linux主机一台。
2、nginx,squid,polipo或其他功能相似的http代理软件,本文是围绕polipo展开的(polipo适合小型网络代理环境搭建,不适合生产环境,这里仅做测试用)。
3、Android app一款,我测试过基于HttpURLConnection和Okhttp的网络请求框架均支持代理缓存。
4、真实数据服务器一台,用于向Android客户端返回json字符串。
科普一下使用Polipo作为 代理服务器在缓存功能上的优势:
1、代理和缓存
代理是一种同时作为客户端和服务器端工作的程序,它监听客户端的请求并发送给服务器,然后将服务器的响应发回客户端。
一个http 能够通过缓存服务器响应来优化网络流量,它将服务器的响应数据存储在内存中用来应付其他相同的请求。如果一个响应被缓存下来了,在此以后的客户端请求在某些体定条件下将不需要再次重新请求服务器。
代理能够比普通客户端应用产生更好的网络流量控制,进而提升网络性能。
代理同时也能用于连接一个客户端本身不能连接的服务器,比如有一个防火墙在客户端和服务器中间。或者客户端和服务器采用用了不同等级的协议,比如ipv4和ipv6.
还有一个代理的常见应用是修改客户端发给服务器的数据,比如删减暴露了太多客户端信息的Http头或者移除服务器发回来的广告。
polipo是一个带缓存的Http代理,设计用于个人使用。原则上他应该用于单个用户或者一小组用户。但是它也曾成功的被用于数量大的用户组。
2、延迟时间和吞吐量
多数网络跑分软件认为吞吐量,或者单位时间的平均数据发送量非常重要。但是平均吞吐量对于网络使用体验几乎没啥关系,重要的是中间传输延迟时间,简单的说就是数据在用户已经等得不耐烦的时候才开始的传输。
典型的web缓存优化了吞吐量--比如他们在连接远程服务器之前首先查找一下本地缓存。通过这个步骤,代理能够极大的减少中间延迟时间,polipo就是设计用于最小化延迟时间的工具。
3、网络流量
web是由那些喜欢做文字传输的人而不是做网络传输的人开发的。不意外的是,第一个HTTP协议版本并没有很好的利用网络传输资源。主要的问题是在HTTP/0.9和比HTTP/1.0更早的版本中,分开的TCP连接在每一个实体传输的时候被创建。
开启多重的TCP连接对性能有巨大的影响。连接建立和释放需要额外的包交换并会明显的增加网络使用量,更恐怖的是,增加了延迟时间。
不容易被察觉的一点是:TCP对小请求做优化,TCP的目标是避免网络拥堵。一个正确的TCP实现应该是非常小心的在每次开始连接的时候探测网络状况,这也就是说TCP在刚开始传输几Kb的时候是非常缓慢的,并且会慢慢的提速。
但是由于多数HTTP实体非常小(在1Kb-10Kb)之间,HTTP/0.9使用TCP的方式是非常没有效率的。
长连接:后期版本的HTTP协议允许一个连接通过多个实体。一个连接携带多个实体就被称为“长连接”或者是“keep-alive”。但不幸的是,长连即使在HTTP/1.1接也是一个HTTP可选的功能。(博主测试HttpURLConnection不会发送keep-alive报文,也不会保持连接,在一次请求结束之后会立即发送挥手报文关闭连接,而okHttp会等待服务器发送挥手报文)
HTTP流水线技术(pipelining):
它是指在一个tcp链接内,多个HTTP请求可以并行,下一个HTTP请求在上一个HTTP请求的应答完成之前就发起。因为这个技术向服务器发送请求更快,它减少了延迟。并且,因为多重请求经常能够封在单个包里进行发送,管线化也减少了网络流量。
管线化是一个非常常见的技术,但是它不被HTTP/1.0所支持。HTTP/1.1让每一个服务器必须实现管线化,所以才能使用长连接,但是又一批有bug的服务器他们虽然使用的是HTTP/1.1但是并没有支持管线化。
使用这个技术必须要求客户端和服务器端都能支持,目前仅有部分浏览器完全支持,而服务器端的支持仅需要按HTTp请求顺序正确饭胡Response(也就是请求&响应采用FIFO模式),只要服务器能够正确处理使用HTTP pipelining的客户端的客户端请求,就算支持了流水线技术。由于服务端返回响应数据的顺序必须跟客户端请求时的顺序一致,这样也就是要求FIFO,这容易导致Head-of-line blocking,第一个请求的响应发送影响到了后边的请求,因此导致HTTP流水线技术性能的提升并不明显。
polipo会小心地探测服务器的管线化支持,并且在确保可靠的情况下使用管线话技术。polipo也非常希望客户端也采用管线化技术。
多路复用技术:
Http协议的一个主要弱点就是她无法通过分享一个链接给多个同时的事务使用。在HTTP协议中,一个客户端既能够同步请求所有的实例,这将会极大的挺假延迟,或者也可以开启多个同时的连接,这样会引起短链接过多的问题。
多路复用技术(PMM)是一个模拟多重请求并通过多个segments只请求一个实例的技术;因为segemtns被服务器通过独立的事务返回。他们能够通过多个请求交叉存取其他的资源。
如果资源时动态获取来的,那么在segments之前很有可能会发成改变;因此一个PMM的实现必须能够在探测到动态资源的时候重新切换资源。
polipo支持PMM技术,但是默认是关闭的如果变量pmmSize被设置成一个证书,那么polipo就会启用PMM,向一致的支持管线化的服务器发送请求。他会请求资源使用pmmSize规定的segment大小,而且首个segment的大小将会是pmmFirstSize规定的大小,默认是两倍的pmmSize。
PMM是一个本质上不可靠的技术。polipo进行了史诗般的努力来让它能用。如果要请求一个不支持PMM的服务器或者动态输出会让PMM失效。尽管如此,除非服务器合作,否则你会在使用PMM的时候看见一些失败提示,比如返回空页面和损坏的图片icon;敲击浏览器的刷新按钮会提示polipo注意到发生了错误并且修正这些问题。
缓存部分实例:
所谓的部分实例是被缓存到本地内存但是只有部分是可用的实例。有三种方式可以长生部分实例:客户端应用只请求部分实例(比如Adobe’s Acrobat Reader就很擅长做这个),服务器丢弃了一个正在传输的连接,客户端丢弃了一个连接。
当一个请求实例指示部分缓存,那么很有可能用HTTP的一个功能叫range请求只获取缺失部分的数据。但是服务器端接收range请求的支持是可选的,许多服务器标榜他们防备静态数据。
缓存部分实例具备一些列的正面意义,最明显的,它可以减少数据传输的数量因为有些本地可获取的数据不需要被重新获取。由于它便免了部分数据被丢弃,它让代理适当的无条件丢弃一个用户请求的下载并因此减少了网络流量。
poplipo缓存了主管的部分实例在内存中。但他只会存储segment初始化的部分在他的磁盘缓存中,因此,他将会尝试使用range请求去获取缺失的数据。
内存缓存:
内存缓存包含一系列的根据时间顺序维持HTTP和DNS数据。一个内存的索引被一个hash表维护。
当内存缓存的到校超出了某个值,或者hash表发生冲突,资源会从内存写到磁盘中去
磁盘缓存:
磁盘缓存包含一个文件系统子树,该目录的位置由变量diskCacheRoot指定,默认是
/var/cache/polipo/
,这个目录应该具有读写权限。不要使用NFSv2文件系统,否则会导致缓存崩溃
如果设定
diskCacheRoot
为一个空字符串,那么就没有磁盘缓存了
变量
当达到这个限制以后,polipo会在最近使用的文件中关掉文件描述。这个值应该略大于你期望一次性获取资源的大小。maxDiskEntries
(默认32)是打开的磁盘文件file descriptors的最大值,
变量diskCacheWriteoutOnClose
(默认64KB)是当关闭一个磁盘文件的时候polipo写出数据到磁盘的大小,当关闭一个文件的时候能够避免在随后重新打开它。但是如果实例后来被取代,会引起没有必要的工作。
变量maxDiskCacheEntrySize指定了实例以字节为单位的存到磁盘缓存的最大值,如果设为-1(默认),那么所有的数据都会被存储到磁盘上。
第一种:需要联网才能调用的缓存,需要服务器返回响应码304,但不返回全部数据
该种方式是服务器在将一个静态资源文件返回的同时,发回ETag,Age,Last-Modified,Cache-Control等信息,
其中ETag相当于这个文件发送时的指纹,可以理解为HASH码,用来给客户端标记这个文档摘要的一个指纹,
而Age是代理缓存服务器上这个文件已经缓存了的时间
Last-Modified是这个文件在缓存服务器上上次更新的时间,这些值需要客户端记下来,下次请求的时候用上。
Cache-Control:max-age=xxx是数据服务器高速缓存代理服务器该资源你应该缓存多久之后再来跟我要,这个时间之前就不要再找我了
比如客户端第二次发起了相同资源的请求,此时客户端会添加http头部如下字段:
If-Modified-Since:值为第一次请求时的Last-Modified,意思是如果这个资源在该时间之后发生了更新,就把更新后的文件给我,响应码200,否则给我个304就行了,我会取本地缓存的上次的文件
If-None-Match:发送值为服务器第一次给我的ETag,如果缓存服务器上的文件的ETag和我发给你的这个不一样,说明我这里的文件已经过时了,那么缓存服务器就把他的文件给我,响应码200,否则给我一个304我取本地的缓存就行了
Cache-Control:max-age=xxx 告诉缓存代理服务器,在某段时间内,即使我请求多次同一个资源,你缓存一次并把缓存给我就行了,该设置不会影响其他客户端的请求。
上面的配图是chrome访问css资源的304结果,下面看看我在使用polipo服务做缓存代理的实验中,在okhttp3中设置Cache-Contro :max-age=60后的请求结果对比(请求数据为数据库查询结果的json字符串)
首先是直连tomcat服务器的响应头,
Content-Type : application/json;charset=UTF-8
Date : Thu, 25 Aug 2016 06:28:03 GMT
OkHttp-Received-Millis : 1472106598236
OkHttp-Sent-Millis : 1472106598101
Server : Apache-Coyote/1.1
Transfer-Encoding : chunked
下面是开启了Cache-Contro之后连接HTTP缓存代理服务器之后的响应头
Age : 40
Connection : keep-alive
Content-Length : 15191
Content-Type : application/json;charset=UTF-8
Date : Thu, 25 Aug 2016 06:32:00 GMT
OkHttp-Received-Millis : 1472106867680
OkHttp-Sent-Millis : 1472106867658
Server : Apache-Coyote/1.1
在这里可以很清楚的看出使用缓存代理服务器之后的变化:
1、多出了Content-Length 属性,标注了该返回json字符串的长度,而直连tomcat(在默认配置下)是不返回Content-Length属性的,也就是我们在接收的时候不知道要接受的数据的大小,也就无法使用Keep-Alive长连接的复用加速。
2、多出了Connection : keep-alive 该header是为了兼容HTTP1.0的代理服务器而添加的,对于常见的HTTP 1.1协议来说属于多余的选项,因为HTTP 1.1默认使用长链接而HTTP 1.0不是,需要添加此header告知其他代理需要使用长链接。
3、去掉了Transfer-Encoding : chunked,该字段的去掉和Content-Length 的添加是统一的,因为Transfer-Encoding : chunked 代表服务器没有告诉客户端该资源的长度,客户端在不知道长度的情况下也无法开启keep-alive,而添加了代理缓存服务器之后,缓存服务器会先行下载,它就知道了资源的长度,并且将Content-length告诉客户端
4、多出了Age这个代表缓存服务器缓存该资源的时间,单位是秒,因为客户端发出请求时添加了头部:Cache-Contro :max-age=60,所以polipo缓存代理服务器会缓存这个json字符串最多60s,在这段时间之内不管我请求多少次该url返回的都是第一次请求的结果,但是超过60s之后缓存服务器会在请求到来之时更新缓存,客户端也就可以拿到新的数据了。该设置只对一个客户端有效,并不会影响其他客户端。
下面介绍一下该实验的细节:
linux端代理服务器的搭建
首先下载和安装polipo代理服务
安装方法:
$apt-get install polipo
修改配置文件:
$sudo gedit /etc/polipo/config
添加如下几行
设置完之后可以用polipo -v检查有没有语法错误proxyAddress = "0.0.0.0"#使用本机作为代理服务器 proxyPort = 8888#提供http代理服务的端口
然后重启polipo服务
$ /etc/init.d/polipo restart
加入linux服务器的局域网地址是192.168.1.109,那么现在就可以使用手机连接本机的http代理:192.168.1.109:8888进行代理上网了
这个时候HttpURLConnection或OkHttp的默认GET方式请求一段返回为json字符串的url,比如:http://staging.qraved.com:8033/app/home/timeline?max=10&cityId=1
这里面只有两个参数,一个是max,一个是cityId,而且几乎都是固定不变的,
此时在客户端会出现第一次请求成功,得到了全新的数据,但是之后每次再请求这个url都会返回同样的json字符串,即使服务器已经把数据改变了,但是每次重新请求都是获取的代理服务器中的缓存,我们可以看一下polipo的运行截图:
这上面介绍了该代理服务器的内存缓存占用情况,可以看到随着请求数量的增加,object,atom和chunks会一直增加。
那么现在已经实现了缓存,但是下拉刷新的功能还是不好用的,那么如何对动态页面去掉缓存以获取实时数据呢,可以采用以下几种方式
1、将GET请求换为POST请求
2、修改GET请求的http header中的“Cache-Control”,设置缓存模式为无缓存或者设置缓存的超时时间
3、在GET请求中加入随机数做为参数
其中我最推荐的方式就是第二种,修改请求头,这样既方便了客户端,也有利于减少http代理服务器的内存开销,Android上面实现方法如下,这里以OkHttp为例(使用okhttpfinal这个框架,其他框架修改header大同小异)
cn.finalteam.okhttpfinal.RequestParams params = new cn.finalteam.okhttpfinal.RequestParams((HttpCycleContext)mContext);//请求参数
params.addHeader("Cache-Control","no-cache");
使用HttpURLConnection添加header可能会复杂一点,大家根据各自使用的请求框架添加“Cache-contro”为“no-cache”就好了
顺便在提一下“Cache-Control”的常见参数
引用原文:http://www.cnblogs.com/cuixiping/archive/2008/05/04/1181056.html
网页的缓存是由HTTP消息头中的“Cache-control”来控制的,常见的取值有private、no-cache、max-age、must-revalidate等,默认为private。其作用根据不同的重新浏览方式分为以下几种情况:
(1) 打开新窗口
如果指定cache-control的值为private、no-cache、must-revalidate,那么打开新窗口访问时都会重新访问服务器。而如果指定了max-age值,那么在此值内的时间里就不会重新访问服务器,例如:
Cache-control: max-age=5
表示当访问此网页后的5秒内再次访问不会去服务器
(2) 在地址栏回车
如果值为private或must-revalidate(和网上说的不一样),则只有第一次访问时会访问服务器,以后就不再访问。如果值为no-cache,那么每次都会访问。如果值为max-age,则在过期之前不会重复访问。
(3) 按后退按扭
如果值为private、must-revalidate、max-age,则不会重访问,而如果为no-cache,则每次都重复访问
(4) 按刷新按扭
无论为何值,都会重复访问
当指定Cache-control值为“no-cache”时,访问此页面不会在Internet临时文章夹留下页面备份。
另外,通过指定“Expires”值也会影响到缓存。例如,指定Expires值为一个早已过去的时间,那么访问此网时若重复在地址栏按回车,那么每次都会重复访问:
Expires: Fri, 31 Dec 1999 16:00:00 GMT
1、将动态页面,静态页面,变动不频繁的请求接口区分开来,便于分类请求;
2、对于需要从代理服务器获取缓存的静态接口,使用GET方法,并且尽量减少参数设置,尽量让所有客户端的请求都一模一样
3、对于动态页面,设置http请求头为no-cache,或者使用POST接口
4、对于偶尔会变化的接口,但是又想缓存的,设置请求头为max-age=时间,在一段时间内使用缓存,然后更新缓存
5、在不同的地区部署多台Http代理缓存服务器,实现CDN加速的效果
6、图片请求的url要保持所有客户端均相同
第二种:使用okhttp3做无条件的GET请求缓存
如果有一些GET请求好多天的返回值都不会有变化,而且即使有变化也不要紧的请求可以直接让okhttp将这样的GET返回结果缓存在磁盘上,每次调用的时候无需联网,从本地直接读取即可,此种方法和第一种方法的区别是,第一种是拿着静态资源去服务器问该资源是否有更新,如果有更新就抛弃之前的缓存重新下载,反之就使用之前的资源。这样做法的弊端是需要链接服务器,并增加连接数和握手数,而第二种方法则完全是像本地数据库一样保存GET请求的返回值,方法是自定义okhttp3的拦截器
private static OkHttpClient defaultOkHttpClient(File cacheDir, long maxSize) {
Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = new Interceptor() {
@Override
public okhttp3.Response intercept(Chain chain) throws IOException {
okhttp3.Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder()
.removeHeader("Pragma")//去掉一个header
.header("Cache-Control", String.format("max-age=%d", 480))//添加本地缓存过期时间,单位是秒
.build();
}
};
return new OkHttpClient.Builder()
.cache(new okhttp3.Cache(cacheDir, maxSize))
.addNetworkInterceptor(REWRITE_CACHE_CONTROL_INTERCEPTOR)
.build();
}