Skip to main content
 首页 » 编程设计

python之CherryPy之静态文件的缓存

2025年05月04日15落叶无声

我有一个服务于大量静态内容的服务器。只要支持 gzip 内容,就可以启用 CherryPy 工具 tools.gzip 来压缩文件。

问题:CherryPy 是在每次请求时对静态文件进行 gzip 压缩,还是对内容进行 gzip 一次并将 gzip 后的副本提供给所有请求?

如果 CherryPy 当前在每次请求时都对文件进行 gzip 压缩,启用 tools.caching 是否会阻止这种情况,或者是否有更好的方法?

请您参考如下方法:

首先,我想指出,尽管 HTTP 的广泛传播和针对每种语言的良好客户端库的存在使 HTTP 看似简单,但实际上 HTTP 是一个涉及多个交互层的复杂协议(protocol)。缓存也不异常(exception),RFC 2616 Section 13 .下面说的是Last-Modified/If-Modified-Since,因为ETag用gzip是another story .

设置

#!/usr/bin/env python 
# -*- coding: utf-8 -*- 
 
 
import os 
 
import cherrypy 
 
 
path   = os.path.abspath(os.path.dirname(__file__)) 
config = { 
  'global' : { 
    'server.socket_host' : '127.0.0.1', 
    'server.socket_port' : 8080, 
    'server.thread_pool' : 8 
  }, 
  '/static' : { 
    'tools.gzip.on'       : True, 
    'tools.staticdir.on'  : True, 
    'tools.staticdir.dir' : os.path.join(path, 'static') 
  } 
} 
 
 
if __name__ == '__main__': 
  cherrypy.quickstart(config = config) 

然后将一些纯文本或 HTML 文件放在 static 目录中。

实验

Firefox 和 Chromium 不会在第一次请求时发送与缓存相关的 header ,即 GET/static/some.html:

Accept-Encoding: gzip, deflate 
Host: 127.0.0.1:8080 

响应:

Accept-Ranges: bytes 
Content-Encoding: gzip 
Content-Length: 50950 
Content-Type: text/html 
Date: Mon, 15 Dec 2014 12:32:40 GMT 
Last-Modified: Wed, 22 Jan 2014 09:22:27 GMT 
Server: CherryPy/3.6.0 
Vary: Accept-Encoding 

在后续请求中,使用以下缓存信息 (Firebug) 可以避免任何网络连接:

Data Size: 50950 
Device: disk 
Expires: Sat Jan 17 2015 05:39:41 GMT 
Fetch Count: 6 
Last Fetched: Mon Dec 15 2014 13:19:45 GMT 
Last Modified: Mon Dec 15 2014 13:19:44 GMT 

因为默认情况下 CherryPy 不提供过期时间(Expires 或 Cache-Control),Firefox(也可能是 Chromium)根据 RFC 2616 Section 13.2.4 使用启发式算法:

If none of Expires, Cache-Control: max-age, or Cache-Control: s- maxage (see section 14.9.3) appears in the response, and the response does not include other restrictions on caching, the cache MAY compute a freshness lifetime using a heuristic...

Also, if the response does have a Last-Modified time, the heuristic expiration value SHOULD be no more than some fraction of the interval since that time. A typical setting of this fraction might be 10%.

这是证明 Expires 值的启发式性质的代码:

import email.utils 
import datetime 
 
s  = 'Wed, 22 Jan 2014 09:22:27 GMT' 
lm = datetime.datetime(*email.utils.parsedate(s)[0:6]) 
 
print datetime.datetime.utcnow() + (datetime.datetime.utcnow() - lm) / 10 

当您刷新页面时,浏览器会将 Cache-Control 附加到请求中:

Accept-Encoding: gzip,deflate 
Cache-Control: max-age=0 
Host: 127.0.0.1:8080 
If-Modified-Since: Wed, 22 Jan 2014 09:22:27 GMT 

如果文件未更改,CherryPy 会回复304 Not Modified。这是它的工作原理:

cherrypy.lib.static.serve_file

def serve_file(path, content_type=None, disposition=None, name=None, debug=False): 
    # ... 
 
    try: 
        st = os.stat(path) 
    except OSError: 
        if debug: 
            cherrypy.log('os.stat(%r) failed' % path, 'TOOLS.STATIC') 
        raise cherrypy.NotFound() 
 
    # ... 
 
    # Set the Last-Modified response header, so that 
    # modified-since validation code can work. 
    response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime) 
    cptools.validate_since() 
 
    # ... 

cherrypy.lib.cptools.validate_since

def validate_since(): 
    """Validate the current Last-Modified against If-Modified-Since headers. 
 
    If no code has set the Last-Modified response header, then no validation 
    will be performed. 
    """ 
    response = cherrypy.serving.response 
    lastmod = response.headers.get('Last-Modified') 
    if lastmod:   
        # ... 
 
        since = request.headers.get('If-Modified-Since') 
        if since and since == lastmod: 
            if (status >= 200 and status <= 299) or status == 304: 
                if request.method in ("GET", "HEAD"): 
                    raise cherrypy.HTTPRedirect([], 304) 
                else: 
                    raise cherrypy.HTTPError(412) 

总结

使用 tools.staticdir CherryPy 不发送文件内容,也不压缩它们,对于带有有效 If-Modified-Since header 的请求,但仅响应304 Not Modified 询问文件系统修改时间。如果没有页面刷新,它甚至不会收到请求,因为当服务器不提供时,浏览器会使用启发式来确定到期时间。当然,让您的配置更具确定性,提供缓存生存时间不会有什么坏处,例如:

'/static' : { 
  'tools.gzip.on'       : True, 
  'tools.staticdir.on'  : True, 
  'tools.staticdir.dir' : os.path.join(path, 'static'), 
  'tools.expires.on'    : True, 
  'tools.expires.secs'  : 3600 # expire in an hour 
}