不同浏览器环境的抓包验证

缓存类型

HTTP缓存有很多种,广义上分为共享缓存(public)和私有缓存(private)。共享缓存包括代理缓存、网关缓存、CDN、反向代理缓存和负载均衡器等,本文讨论的是私有缓存:浏览器缓存。从H5游戏开发的角度看,如何在解决不同浏览器环境下缓存带来的热更新问题的同时,最大化利用缓存是追求的目标,为此需要了解缓存机制,制定一个合适的缓存策略并验证分析其正确性。

先了解浏览器的缓存逻辑

1. 第一次请求

第一次请求,因为浏览器没有缓存该内容,所以缓存不会命中。向服务器请求资源返回的响应头中,会有缓存结果和缓存指示(directives),浏览器将内容和缓存指示一同存入缓存中。

2. 第二次请求同样的资源

浏览器在缓存中找到该资源的缓存指示,根据指示选择之后的缓存策略。根据是否需要向服务器重新发起HTTP请求,我们可以将缓存过程分为两个部分,分别是强缓存和协商缓存。

1)强缓存阶段

不会向服务器发送请求,直接从缓存中读取资源,在chrome控制台的Network选项中可以看到该请求返回200的状态码,并且Size栏显示from disk cache(从硬盘缓存读取)或from memory cache(从内存缓存读取)。
图1,强缓存命中
强缓存可以通过设置三种 HTTP Header 实现:ExpiresPragmaCache-Control

Expires: Wed, 19 Jun 2019 15:00:00 GMT

Expires :
HTTP/1.0的产物,属于HTTP响应的头部字段,值为HTTP Date类型,指定了所请求的资源到期的具体时间。浏览器在下一次请求时会判断本地时间,如果没有超过Expires设置的过期时间,则直接从缓存中读取,否则需要发起HTTP请求。但是需要说明的是,Expires已经被Cache-Control替代,如果响应头中存在Cache-Control控制缓存过期时间的字段,则Expires会被忽略。P.S. 搭建的HTTP服务器使用的HTTP/1.1协议,抓包没有看到响应中有Expires字段。

Pragma:no-cache

Pragma :
HTTP/1.0的产物,属于HTTP请求与响应通用的头部字段,只有一个no-cache命令,行为与Cache-Control: no-cache一致,优先级低于Cache-Control。作为请求头时作用是强制要求缓存服务器在返回缓存的版本之前将请求提交到源头服务器进行验证,确保返回最新资源(强制刷新时浏览器会在请求头中加入Pragma:no-cache)。作为响应头时行为不统一,依赖浏览器具体实现,所以该字段不可靠。MDN建议只在需要兼容 HTTP/1.0 客户端的场合下应用 Pragma 首部。

强刷新请求头中的Pragma字段与Cache-Control字段

Cache-control: must-revalidate,no-cache,no-store,no-transform,public,private,proxy-revalidate,max-age=<seconds>,s-maxage=<seconds>

Cache-Control :
HTTP/1.1的产物,属于HTTP请求与响应通用的头部字段,命令可用于控制内容可缓存性、到期时间、是否重新验证和重新加载等。可通过组合命令实现具体的缓存策略。

no-cache: 作为请求头时同Pragma字段行为一致,强制要求缓存服务器在返回缓存的版本之前将请求提交到源头服务器进行验证,确保返回最新资源,强制刷新请求会携带该字段。作为响应头时表现为客户端在使用缓存前需要发送请求验证缓存可用,即跳过强缓存,进行协商缓存。

max-age=<seconds>: 设置缓存有效的最大周期,超过这个时间缓存被认为过期(单位秒),需要重新向服务器请求资源。

must-revalidate : 只能作为响应头字段,指示一旦资源过期(比如已经超过max-age),在成功向原始服务器验证之前,缓存服务器不能用该资源响应后续请求。

public/private: public表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存,private表明响应只能被用户缓存,不能作为共享缓存(即代理服务器不能缓存)。

其他字段含义可参考MDN

2)协商缓存阶段

协商缓存就是强制缓存失效后,浏览器携带缓存指示向服务器发起请求,由服务器根据缓存指示决定是否使用缓存的过程。协商缓存使用的缓存指示存在请求和响应的HTTP头部中,包括响应头Last-Modified/请求头If-Modified-Since响应头ETag/请求头If-None-Match。协商缓存的结果有两种:

  1. 协商缓存失效,服务器返回200和最新资源
  2. 协商缓存有效,服务器返回304 Not Modified,浏览器从缓存中读取对应资源
Last-Modified: Wed, 19 Jun 2019 15:00:00 GMT
If-Modified-Since: Wed, 19 Jun 2019 15:00:00 GMT

Last-Modified/If-Modified-Since:
服务器会在响应头部添加Last-Modified字段,值为资源在服务器上的最后修改时间。浏览器将该缓存指示同缓存结果一同存入缓存中。浏览器请求资源时,如果存在Last-Modified字段,会把该值通过If-Modified-Since字段添加到请求头中。服务器再次收到这个资源请求,会根据If-Modified-Since中的值与服务器中该资源的最后修改时间对比。如果没有变化,返回304和空的响应体,浏览器直接从缓存读取。如果If-Modified-Since的时间小于服务器中这个资源的最后修改时间,说明文件有更新,于是返回新的资源文件和200。

Last-Modified Example

ETag: W/"2251799814418149-5669-2019-06-18T12:14:19.152Z"
If-None-Match: W/"2251799814418149-5669-2019-06-18T12:14:19.152Z"

ETag/If-None-Match:
ETag是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成)。只要资源有变化,ETag就会重新生成。服务器通过比较客户端传来的If-None-Match跟自己服务器上该资源的ETag是否一致来决定返回结果。如果不一致,GET返回200,新资源和新ETag,一致则返回304 Not Modified。

ETag与Last-Modified对比
  1. 精确度上ETag要优于Last-ModifiedLast-Modified的时间单位是秒,如果某个文件在1秒内改变了多次,那么他们的Last-Modified其实并没有体现出来修改,但是ETag每次都会改变确保了精度;如果是负载均衡的服务器,各个服务器生成的Last-Modified也有可能不一致。
  2. 性能上ETag要逊于Last-Modified,毕竟Last-Modified只需要记录时间,而ETag需要服务器通过算法来计算出一个hash值。
  3. 优先级上服务器校验优先考虑ETag

打开页面的方式影响浏览器缓存的使用逻辑

  1. 地址栏回车:浏览器先查看请求资源有无缓存,无缓存则向服务器请求资源。若该资源有缓存则根据缓存指示采用对应缓存策略。先查找强缓存,强缓存有效则直接使用缓存200(from cache),强缓存无效则走协商缓存返回304 Not Modified或返回最新资源200
  2. F5/ctrl+R:刷新页面。仅请求该页面,并跳过强缓存阶段,走协商缓存,所以会向服务器发送协商请求,返回304 Not Modified或返回最新资源200。注意:该页面所引用的资源不会跳过强缓存,且对Chrome而言,在页面通过地址栏回车的方式打开与页面相同URL的行为等同于刷新
    图7:F5刷新逻辑
  3. ctrl+F5/ctrl+shift+R:强制刷新页面及页面所引用的资源,跳过所有缓存阶段,无论如何都会向服务器发送请求,强制返回最新资源200
    图8强刷

H5游戏应该选择怎样的缓存策略

游戏入口index.html确保不被缓存,游戏依赖的资源文件通过gulp构建工具做版本管理

每次更新文件内容时,更改文件名加上md5后缀。因为入口引用的资源名称变了,浏览器会直接请求新的资源。当没有变化时,又会直接读取缓存。这一步一些H5游戏引擎如laya已经实现了,通过修改laya的gulp脚本,可以自定义修改构建的效果。

如何确保入口不被缓存?我们可以使用meta标签的http-equiv属性把content内容添加到HTTP响应的头部信息,但是默认的HTTP服务器(比如自己搭建的node http服务器)不会在响应时根据meta标签来处理头部信息。这种情况下需要通过nginx设置显示地在请求index.html的响应头中加入这些字段。

<!--meta标签-->
<meta http-equiv="Cache-Control" content="no-cache,max-age=0,must-revalidate">
<meta http-equiv="Expires" content="0">  
<meta http-equiv="Pragma" content="no-cache"> 
// nginx设置
add_header Pragma no-cache;
expires    epoch;

// 注:expires epoch语法等同于设置Expires字段为Thu, 01 Jan 1970 00:00:01 GMT以及Cache-Control字段为no-cache

nginx设置后
*上图为nginx设置应用在内网H5RPG项目的结果*

不同浏览器环境的表现验证

缓存机制的逻辑最终还是由浏览器来实现,部分浏览器不一定严格遵守W3C的规范,为此需要验证不同浏览器环境下我们选择的缓存策略表现。我们选择内网H5RPG项目来进行多种浏览器环境的测试。nginx设置如下:
nginx设置

验证方法:

PC开热点让手机连接,在不同的浏览器环境中测试我们使用的缓存策略,使用Wireshark抓包查看请求和响应。

  1. 打开/允许使用缓存
  2. 第一次请求资源
  3. 第二次请求资源(不使用刷新)查看是否会向服务器发送请求(检查是否使用强缓存),只要发送了请求,则使用的协商缓存,达到预期的效果,实现了热更新。

Chrome

测试环境: Chrome 74.0.3729.169(Release)(64 bit)
是否有请求:
Chrome第二次请求
结论: 只有两个请求,分别是index.html以及version.json,并且使用协商缓存返回304,其余资源都是直接读取缓存。符合预期。

Safari

测试环境: iPhone 7 iOS 12.2
是否有请求:
Safari第二次请求
结论: 只有两个请求,分别是index.html以及version.json,并且使用协商缓存返回304,其余资源都是直接读取缓存。符合预期。

使用iPhone Safari测试,需要关闭无痕模式

Wechat Browser

测试环境: WeChat 7.0.4
是否有请求:
微信浏览器第二次请求 结论: 同样只有两个请求,并且使用协商缓存返回304,其余资源都是读取缓存。符合预期。

X5 Core(TBS) apk

测试环境: TBS V4.5 43646
是否有请求:
X5内核默认设置不缓存
*demo默认设置不使用缓存,相当于强刷*

修改CacheMode后符合预期
*设置WebView CacheMode后使用协商缓存*

结论: X5 WebView默认情况下不使用缓存,所有GET请求都会带上no-cache请求头去获取最新资源,每次进入apk都相当于强制刷新。需要设置CacheMode,设置后正常走协商缓存,符合预期。

webSetting.setCacheMode(IX5WebSettings.DEFAULT_CACHE_CAPACITY);

最后制作一张流程图

GET请求浏览器缓存流程图

参考链接