缓存系列(1)——浏览器缓存协商

       “缓存”在计算机技术中被大量使用,比如从计算机体系结构中存储层次结构到Web系统中的web缓存、反向代理缓存以及浏览器缓存等。缓存主要用来解决费时操作的重复计算,主要是把费时操作的计算结果保存至磁盘、内存等介质中,下一个请求到来时直接将结果返回,这就避免了重复的操作,节省了cpu、带宽等资源。当然,万物都是有利弊的,缓存的缺点就是不能保证时效性,雪崩效应等。

        对于高性能的Web应用,缓存是至关重要,这里涉及到的技术包括:页面缓存、动态内容缓存、opcode缓存、浏览器缓存、web缓存以及反向代理缓存等。本文介绍一下最贴近用户的浏览器缓存,后续会逐步介绍其他技术。

        Web缓存是介于web服务器和浏览器中间的一层,用于缓存web服务器的http响应,而浏览器本身也是带有缓存功能的。比如,IE、FireFox或者Chrome会把浏览器http响应的内容缓存在硬盘或者内存中。而如何使浏览器去使用缓存这个是依赖于HTTP协议的,我们知道浏览器与web服务器通信是通过HTTP协议的,所以在web服务器端的程序需要通过HTTP协议来控制浏览器去使用本地的缓存。下面所有的例子采用的都是python的轻量级的web框架web.py

        首先写一个server端程序cache_tst.py,用于返回当前时间:

import web
import datetime

urls = (
	'/index', 'index'		
)

class index:
	def GET(self):
		return datetime.datetime.now()

app = web.application(urls, globals())

if __name__ == '__main__':
	app.run()
        这个程序很简单,监听8080端口,只会处理url为/index的请求,然后返回当前时间。通过浏览器访问以下localhost:8080/index,结果显示:

2012-03-13 16:12:58.729000
        响应结果就是当前的时间。下面通过FireFox或者Chrome的js工作台查看一下http请求和响应。


        响应头中只有Date、Server和Transfer-Encoding三个字段,没有与缓存相关的,下面看如何实现缓存协商。

1. Last-Modified和If-Modified-Since

        我们知道缓存是有失效时间的,所以浏览器必须知道当前的HTTP响应的时间戳,这样才能用这个时间戳去判断该响应是否失效。这就需要用到HTTP协议的Last-Modified属性,web服务器通过设置Last-Modified属性可以让浏览器知道这个HTTP响应的时间戳。而浏览器知道这个时间戳之后,当再次请求时会在请求头中添加If-Modified-Since属性。

        比如,修改cache_tst.py如下:

import web
import datetime

urls = ('/index', 'index')

class index:
        def GET(self):
                # web.lastmodified用于设置Last-Modified属性
                web.lastmodified(datetime.datetime.now())	
                return datetime.datetime.now()

app = web.application(urls, globals())

if __name__ == '__main__':
	app.run()

         再访问localhost:8080/index,我们看一下HTTP请求和响应。 
        响应头中多了Last-Modified属性,请求头中没有变化,然后按一下F5刷新一下浏览器,请求有了变化。
        再次请求可以发现,多了If-Modified-Since属性,随后的值就是上次响应的Last-Modified的属性值。这样就可以在web服务器端判断缓存是否失效,就避免了重复计算。我们修改一下cache_tst.py,实现和浏览器端的交互,这里要实现将响应结果缓存30s。
import web
import datetime

urls = (
	'/index', 'index'		
)

def cache_valid(cache_time):
	'''
		cache_time: 缓存时间
		根据http协议,判断缓存是否失效。如果缓存失效则设置Last-Modified属性,
		否则返回Http响应状态码304 Not Modified
	'''
	last_time_str = web.ctx.env.get('HTTP_IF_MODIFIED_SINCE', '')
	last_time = web.net.parsehttpdate(last_time_str)
	now = datetime.datetime.now()
	if last_time and last_time + datetime.timedelta(seconds = cache_time) > now:
		web.notmodified()
		return True
	else:
		web.lastmodified(now)
		return False
	

class index:
	cache_time = 30
	def GET(self):
		if not cache_valid(self.cache_time):
			return datetime.datetime.now()


app = web.application(urls, globals())

if __name__ == '__main__':
	app.run()
        然后请求两次localhost:8080/index,http的状态码变为304:
        如果多次按F5刷新浏览器可以发现状态码一直都是304,并且页面的内容不会变化,直到30秒后才会改变。通过Last-Modified和If-Modified-Since可以避免服务器的计算,但是,浏览器仍然需要向服务器发送请求,看一下服务器的日志:

2. Expires

        缓存失效判断这一操作完全可以由浏览器自身实现,没必要由服务器完成,这样每次建立、释放连接以及网络开销很耗费资源,下面我们来彻底消灭这种请求。这里需要用到Expires属性,标记http响应的失效的时间。修改cache_tst.py:

import web
import datetime

urls = (
	'/index', 'index'		
)

class index:
	cache_time = 30
	def GET(self):
		now = datetime.datetime.now()
		web.http.expires(datetime.timedelta(seconds = self.cache_time, days = 1))# 设置Expires属性,传入timedelta对象
		return now

app = web.application(urls, globals())

if __name__ == '__main__':
	app.run()
        参看一下请求头和响应头:


        有Expires属性指示失效时间。然后在chrome下打开一个新的窗口,输入localhost:8080/index,点击enter(这么操作的原因见后文)。响应内容和前面的内容一致,再看一下请求和响应:


        提示响应内容是从cache中读取的,也就是说这一次访问没有经过http连接,是在本地由浏览器完成的缓存失效检验。但是,这里有个问题,Expires返回的是精确的时间,如果浏览器和web服务器的时区不同,那么这个过期时间是没有用的,下面看一下Cache-Control是如何解决这个问题的。

3. Cache-Control

        Cache-Control通过max-age=xxx指定缓存时间,然后具体的失效时间由浏览器计算得到,这样就不存在时区不同导致的错误,下面试验一下。

import web
import datetime

urls = (
	'/index', 'index'		
)

class index:
	cache_time = 30
	def GET(self):
		now = datetime.datetime.now()
		web.header('Cache-Control', 'max-age=30')
		return now

app = web.application(urls, globals())

if __name__ == '__main__':
	app.run()
        请求和响应信息:


         再像实验Expires时一样重新打开一个窗口访问服务器。


        同样是从缓存中读取的。

4. 缓存失效时浏览器的行为

        上面在介绍Expires时,要求必须新建一个窗口,然后再访问,下面介绍一下,不同浏览器对于用户的不同操作所做的处理。这些行为,对Expires和Cache-Control在缓存失效时的行为是不同的。

表 1. 当用户打开一个新的浏览器窗口时的失效操作

  Firefox 3.5 IE 8 Chrome 3 Safari 4
内容没有失效 浏览器呈现来自缓存的页面 浏览器重新发送请求到服务器。返回代码是 200 浏览器呈现来自缓存的页面 浏览器呈现来自缓存的页面
内容失效 浏览器重新发送请求到服务器。返回代码是 200 浏览器重新发送请求到服务器。返回代码是 200 浏览器重新发送请求到服务器。返回代码是 200 浏览器重新发送请求到服务器。返回代码是 200

表 2. 当用户在原始浏览器窗口中单击 Enter 按钮时的失效操作


Firefox 3.5 IE 8 Chrome 3 Safari 4
内容没有失效 浏览器呈现来自缓存的页面 浏览器呈现来自缓存的页面 浏览器重新发送请求到服务器。返回代码是 304 浏览器重新发送请求到服务器。返回代码是 304
内容失效 浏览器重新发送请求到服务器。返回代码是 200 浏览器呈现来自缓存的页面 浏览器重新发送请求到服务器。返回代码是 200 浏览器重新发送请求到服务器。返回代码是 200

表 3. 当用户按 F5 键刷新页面时的失效操作

  Firefox 3.5 IE 8 Chrome 3 Safari 4
内容没有失效 浏览器重新发送请求到服务器。返回代码是 304 浏览器重新发送请求到服务器。返回代码是 304 浏览器重新发送请求到服务器。返回代码是 304 浏览器重新发送请求到服务器。返回代码是 304
内容失效 浏览器重新发送请求到服务器。返回代码是 200 浏览器重新发送请求到服务器。返回代码是 200 浏览器重新发送请求到服务器。返回代码是 200 浏览器重新发送请求到服务器。返回代码是 200

表 4. 当用户单击 Back 或 Forward 按钮时的失效操作

  Firefox 3.5 IE 8 Chrome 3 Safari 4
内容没有失效 浏览器呈现来自缓存的页面 浏览器呈现来自缓存的页面 浏览器呈现来自缓存的页面 浏览器呈现来自缓存的页面
内容失效 浏览器呈现来自缓存的页面 浏览器呈现来自缓存的页面 浏览器呈现来自缓存的页面 浏览器重新发送请求到服务器。返回代码是 200

        由于浏览器在用户执行不同操作时行为是不同的,并且某些行为在设置了Expires和Cache时不会执行缓存协商。所以为了使浏览器缓存作用最大化,比如在用户点击F5刷新,浏览器会重新发起请求到服务器这样的情况下。最好同时使用Last-Modified和Cache-Control,这样可以保证在浏览器端缓存穿透后,服务器端可以不用计算,而只是返回304状态码。
        这里还是用web.py举例:
import web
import datetime

urls = (
	'/index', 'index'		
)

def cache_valid(cache_time):
	'''
		cache_time: 缓存时间
		根据http协议,判断缓存是否失效。如果缓存失效则设置Last-Modified属性,
		否则返回Http响应状态码304 Not Modified
	'''
	last_time_str = web.ctx.env.get('HTTP_IF_MODIFIED_SINCE', '')
	last_time = web.net.parsehttpdate(last_time_str)
	now = datetime.datetime.now()
	if last_time and last_time + datetime.timedelta(seconds = cache_time) > now:
		web.notmodified()
		return True
	else:
		# 同时使用Last-Modified和Cache-Control
		# 在用户新开窗口等情况下,会使用Cache-Control缓存
		# 而当用户进行某些不进行缓存检测的操作(按F5刷新)时,
		# 浏览器会向服务器发送请求,由于使用了Last-Modified属性
		# 所以服务器端仍然可以返回304状态码,避免重复的费时计算。
		web.lastmodified(now)
		web.header('Cache-Control', 'max-age=30')
		return False
	

class index:
	cache_time = 30
	def GET(self):
		if not cache_valid(self.cache_time):
			return datetime.datetime.now()


app = web.application(urls, globals())

if __name__ == '__main__':
	app.run()
        作为一个高性能web应用,缓存体系的构建是必不可少的,而浏览器缓存只是这个体系中的一小部分,对于一些图片,js脚本,css以及一些更新频率很低的页面(1天,1周,1月等)可以很好的发挥作用,而像其他的一些动态内容,更新频率比较高的,浏览器缓存就并不适用了。这时就是页面缓存,web缓存,分布式缓存上场的时候了,后续会分别介绍如何构建一个缓存体系。

        

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值