Tornado源码分析 --- 静态文件处理模块
每个web框架都会有对静态文件的处理支持,下面对于Tornado的静态文件的处理模块的源码进行分析,以加强自己对静态文件处理的理解。
先从Tornado的主要模块 web.py 入手,可以看到在Application类的 __init__() 方法中对静态文件的处理部分:
1 class Application(ReversibleRouter): 2 if self.settings.get("static_path"): 3 path = self.settings["static_path"] 4 handlers = list(handlers or []) 5 static_url_prefix = settings.get("static_url_prefix", 6 "/static/") 7 static_handler_class = settings.get("static_handler_class", 8 StaticFileHandler) 9 static_handler_args = settings.get("static_handler_args", {}) 10 static_handler_args['path'] = path 11 for pattern in [re.escape(static_url_prefix) + r"(.*)", 12 r"/(favicon\.ico)", r"/(robots\.txt)"]: 13 handlers.insert(0, (pattern, static_handler_class, 14 static_handler_args))
从第二行可以看到,需要处理静态文件的话,需要在settings设置关于静态环境的值:static_path
参数介绍:
- static_url_prefix:静态文件的URL前缀,可以对静态文件的访问路径进行设置,默认为 "/static/"
- static_handler_class:处理静态文件的类,可以自定义处理静态文件的动作,默认的为
tornado.web.StaticFileHandler
- static_handler_args:处理静态文件的参数,如果设置了,应该有一个字典被传入到static_handler_class类的
initialize
方法中
默认的静态文件处理模块:class StaticFileHandler(RequestHandler)
介绍和用法:
- 处理来自某个目录的静态文件内容的模块,如果在“Application”中传递了“static_url”关键字参数的话,“StaticFileHandler” 会被自动配置,当然该处理模块也可以定制上面介绍的 “static_url_prefix”、“static_handler_class”、“static_handler_args”。
- 如果想为静态文件目录映射一个额外的路径,可以参考如下方法实例:
1 application = web.Application([ 2 (r"/content/(.*)", web.StaticFileHandler, {"path": "/var/www"}), 3 ])
这样,当你访问 ”/content/“ 目录下资源的话,就是定向到 ”/var/www“下寻找。
- 该静态文件处理模块需要一个 ”path“ 参数,其指定需要被该模块服务的本地目录
- 当一个目录被请求的时候,为了自动的处理类似”index.html“的文件,做法是在 Application 中的 settings中 设置 ”static_handler_args=dict(default_filename="index.html")“,或者为 ”StaticFileHandler“ 添加一个 初始化(initializer) 参数 ”default_filename“
- 为了最大化的利用浏览器的缓存,"StaticFileHandler" 类支持版本化的 URL(默认在URL中使用参数: ``?v=``),如果给出了该参数,那么浏览器将会无期限的对该静态文件进行缓存(其实有期限,其定义了一个变量:CACHE_MAX_AGE = 86400 * 365 * 10,期限为10年)。`make_static_url`(也可以使用`RequestHandler.static_url`)可以用于构建版本化的URL。
- StaticFileHandler类模块主要用于开发轻型文件服务,对于那些繁重的文件服务,使用专用静态文件服务器(如nginx或Apache)效率会更高。该模块也支持 HTTP“Accept-Ranges”机制来返回请求实体的部分内容(因为一些浏览器需要这个功能来展示HTML5音频或视频)。
子类扩展注意项:
- 这个类被设计为可以通过子类去进行扩展,但是由于该静态URL方法是通过类方法生成并非通过实例方法,它的继承模式不太寻常。当要覆盖重写一个类方法的时候,请务必使用 "@classmethod" 装饰器,实例方法可以使用 "self.path"、”self.absolute_path“、”self.modified“ 属性。
- 子类仅仅能够覆盖重写该注意项讨论的方法,不然覆盖重写其他的方法将会非常容易出错,特别是覆盖重写 ”StaticFileHandler.get()“ 方法将会导致很严重的问题,因为它和 ”compute_etag“ 和其他方法耦合性很高。
- 为了改变静态URL的生成方式(例如:为了匹配其他服务器和CDN的行为),可以覆盖重写 ”make_static_url“、”parse_url_path“、”get_cache_time“、以及”get_version“。
- 为了替换和文件系统的交互(例如:服务于来自数据库中的静态数据),可以覆盖重写 ”get_content“、"get_content_size"、”get_modified_time“、"get_absolute_path"、”validate_absolute_path“
源码分析:
从主要的 StaticFileHandler.get() 方法开始入手:
1 def get(self, path, include_body=True): 2 self.path = self.parse_url_path(path) 3 del path 4 absolute_path = self.get_absolute_path(self.root, self.path) 5 self.absolute_path = self.validate_absolute_path( 6 self.root, absolute_path) 7 if self.absolute_path is None: 8 return 9 10 self.modified = self.get_modified_time() 11 self.set_headers() 12 13 if self.should_return_304(): 14 self.set_status(304) 15 return 16 17 request_range = None 18 range_header = self.request.headers.get("Range") 19 if range_header: 20 request_range = httputil._parse_request_range(range_header) 21 22 size = self.get_content_size() 23 if request_range: 24 start, end = request_range 25 if (start is not None and start >= size) or end == 0: 26 self.set_status(416) # Range Not Satisfiable 27 self.set_header("Content-Type", "text/plain") 28 self.set_header("Content-Range", "bytes */%s" % (size, )) 29 return 30 if start is not None and start < 0: 31 start += size 32 if end is not None and end > size: 33 end = size 34 if size != (end or size) - (start or 0): 35 self.set_status(206) # Partial Content 36 self.set_header("Content-Range", 37 httputil._get_content_range(start, end, size)) 38 else: 39 start = end = None 40 41 if start is not None and end is not None: 42 content_length = end - start 43 elif end is not None: 44 content_length = end 45 elif start is not None: 46 content_length = size - start 47 else: 48 content_length = size 49 self.set_header("Content-Length", content_length) 50 51 if include_body: 52 content = self.get_content(self.absolute_path, start, end) 53 if isinstance(content, bytes): 54 content = [content] 55 for chunk in content: 56 try: 57 self.write(chunk) 58 yield self.flush() 59 except iostream.StreamClosedError: 60 return 61 else: 62 assert self.request.method == "HEAD"
1. 通过 parse_url_path(path) 将静态URL路径转换为所在文件系统的路径:
1 def parse_url_path(self, url_path): 2 if os.path.sep != "/": 3 url_path = url_path.replace("/", os.path.sep) 4 return url_path
2. 之后为了确保 传入进来的path 不会替代 self.path, 所以执行了 del path 将该对象删除。
3. 调用 get_absolute_path(self.root, self.path) 将静态URL路径转换为系统的绝对路径:
这里注意到,self.root这个参数,其在 初始化函数 initialize() 中已经进行了定义(self.root 为未进行文件系统路径转换的路径):
1 def initialize(self, path, default_filename=None): 2 self.root = path 3 self.default_filename = default_filename
绝对路径转换函数 get_absolute_path():
1 def get_absolute_path(cls, root, path): 2 abspath = os.path.abspath(os.path.join(root, path)) 3 return abspath
通过 os.path.join() 将 path与root合为一个路径,然后通过 os.path.abspath() 获取该路径的绝对路径,并返回。
4. 调用 validate_absolute_path(self.root, absolute_path) 函数对前面返回的绝对路径 self.absolute_path 进行验证,看该路径文件是否有效存在:
1 def validate_absolute_path(self, root, absolute_path): 2 root = os.path.abspath(root) 3 if not root.endswith(os.path.sep): 4 root += os.path.sep 5 if not (absolute_path + os.path.sep).startswith(root): 6 raise HTTPError(403, "%s is not in root static directory", 7 self.path) 8 if (os.path.isdir(absolute_path) and 9 self.default_filename is not None): 10 if not self.request.path.endswith("/"): 11 self.redirect(self.request.path + "/", permanent=True) 12 return 13 absolute_path = os.path.join(absolute_path, self.default_filename) 14 if not os.path.exists(absolute_path): 15 raise HTTPError(404) 16 if not os.path.isfile(absolute_path): 17 raise HTTPError(403, "%s is not a file", self.path) 18 return absolute_path
函数介绍:
- 对于该函数,参数来说,root(self.root)是 ”StaticFileHandler“ 的配置路径,absolute_path(absolute_path)是前面调用 ”get_absolute_path“ 的结果。
- 而且这是在请求处理的时候所调用的实例方法,所以它也许会返回 ‘HTTPerror’ 或者使用像 ‘RequestHandler.redirect’(重定向后会返回None,然后停止进一步进行处理) 这样的方法,此时404错误(丢失文件)就会被生成。
- 此方法可能会在返回之前修改路径,但请注意任何这样的修改都不会被`make_static_url`所理解。
- 在实例方法中,该方法的结果可用作 ``self.absolute_path``。(在该StaticFileHandler类模块的处理中,使用到了该特性)
注:该方法用到了大量的os模块,对os模块不太熟悉,可以参考:http://www.cnblogs.com/dkblog/archive/2011/03/25/1995537.html
5. 获取该绝对路径文件最后修改时间 get_modified_time():
1 def get_modified_time(self): 2 stat_result = self._stat() 3 modified = datetime.datetime.utcfromtimestamp( 4 stat_result[stat.ST_MTIME]) 5 return modified
其在处理过程中调用了 _stat() :
1 def _stat(self): 2 if not hasattr(self, '_stat_result'): 3 self._stat_result = os.stat(self.absolute_path) 4 return self._stat_result
调用了 os.stat() 获取该 self.absolute_path 路径文件的系统信息;之后在 get_modified_time() 中获取 ST_MTIME 属性获取最后修改时间。
注:os.stat模块可以参考:http://www.cnblogs.com/maseng/p/3386140.html
6. 调用 set_headers() 设置HTTP的Response头部header信息:
1 def set_headers(self): 2 self.set_header("Accept-Ranges", "bytes") 3 self.set_etag_header() 4 5 if self.modified is not None: 6 self.set_header("Last-Modified", self.modified) 7 8 content_type = self.get_content_type() 9 if content_type: 10 self.set_header("Content-Type", content_type) 11 12 cache_time = self.get_cache_time(self.path, self.modified, 13 content_type) 14 if cache_time > 0: 15 self.set_header("Expires", datetime.datetime.utcnow() + 16 datetime.timedelta(seconds=cache_time)) 17 self.set_header("Cache-Control", "max-age=" + str(cache_time)) 18 19 self.set_extra_headers(self.path)
函数分析:
- 首先,通过 set_header() 对 Response中的 ”Accept-Ranges“ 进行设置(Accept-Ranges:表明服务器是否支持指定范围请求及哪种类型的分段请求)。
该 set_header() 函数会调用 _convert_header_value() 方法,对 参数 'name', 'value' 进行相应格式的转换:
- 如果给出了一个datetime,我们会根据它自动格式化HTTP规范;
- 如果值不是字符串,我们将其转换为一个字符串;
- 然后将所有标题值编码为UTF-8。
- 并且对 python3 和 python2 的编码有对应的处理
注:有兴趣可以查看:https://github.com/tornadoweb/tornado/blob/master/tornado/web.py 第361行
- 然后,对头信息header中的 etag 进行设置,详情可以参考 前面一篇博文:Tornado源码分析--Etag实现
- 接着,self.modified is not None 表明绝对路径文件有改变,则在字段 ”Last-Modified“ 中记录最新的修改时间。
- 之后,调用 get_content_type() 设置header头信息中的 字段”Content-Type“:
注:有兴趣可以查看:https://github.com/tornadoweb/tornado/blob/master/tornado/web.py 第2638行
- 缓存时间 cache_time 设置,调用 get_cache_time() 进行设置:
1 def get_cache_time(self, path, modified, mime_type): 2 return self.CACHE_MAX_AGE if "v" in self.request.arguments else 0
这里对最开始介绍的在URL中使用 参数 ”?v=” 来持久化浏览器缓存进行了判断,该CACHE_MAX_AGE参数在类最开始进行了定义(CACHE_MAX_AGE = 86400 * 365 * 10 # 10 years);没有定义该参数的话,就可以自己进行定义,否则为0。
7. should_return_304() 函数还是对 header头信息中 etag 的判断,如果没有改变则返回状态码304。
8. 下面就是对 Request中的 字段“Range” 进行处理了(Range:只请求实体的一部分,指定范围):
第一步,我们先从 resquest请求的头信息header中获取到 字段“Range” 内容,如果含有该字段,则调用 httputil.py 文件中的 _parse_request_range(range_header) 函数进行Range的解析:
解析实例:
1 >>> _parse_request_range("bytes=1-2") 2 (1, 3) 3 >>> _parse_request_range("bytes=6-") 4 (6, None) 5 >>> _parse_request_range("bytes=-6") 6 (-6, None) 7 >>> _parse_request_range("bytes=-0") 8 (None, 0) 9 >>> _parse_request_range("bytes=") 10 (None, None)
注:具体实现方法有兴趣可以查看:https://github.com/tornadoweb/tornado/blob/master/tornado/httputil.py 第640行
第二步,调用 get_content_size() 获取给定路径上面文件资源的总大小:
1 def get_content_size(self): 2 stat_result = self._stat() 3 return stat_result[stat.ST_SIZE]
函数分析:
该函数同样调用了上文提到的 _stat() 函数,来获取到给定路径上文件资源的系统信息,然后通过 ST_SIZE 属性获取到文件的大小。
第三步,就是对请求的资源范围和文件大小进行判断了:
- 如果 请求范围中开始位置比文件size大,则返回状态码416(请求范围不满足);并且写好头信息的字段 "Content-Type" 和 "Content-Range"(字段值为请求文件的大小)
- 如果 start 字段小于 0 的话,最终的 start 为 start + size 得出该请求字段的范围
- 如果 end 字段比文件的最大值还要大的话,那么为了防止客户端盲目使用大范围进行设置请求范围,则以实际文件大小来返回该请求
- 如果 请求范围符合要求,在实际文件大小范围内,那么返回状态码206;调用 _get_content_range() 返回值为:"bytes %s-%s/%s" % (start, end-1, total) 并且把文件的 起始位置、结束位置以及文件大小信息写入字段"Content-Range"中
第四步,就开始对返回头中的响应体长度字段”content_length“进行设置:
利用上述请求范围的 start,end进行计算,从而返回符合要求的内容,之后调用上文分析的 set_header() 函数写入头信息header
第五步,对 include_body 进行判断,在最开始 def get(self, path, include_body=True) 函数中,有一个字段是 include_body = True,然后注意到源码上面还有一个函数 def head(self, path):
1 def head(self, path): 2 return self.get(path, include_body=False)
然后在 def get(self, path, include_body=True) 中,注意到最后一行代码(为方便阅读和理解,将上述 if 语句简化截取下来):
1 if include_body: 2 ...... 3 else: 4 assert self.request.method == "HEAD"
如果客户端request请求中,是发送的 ”HEAD“请求,那么执行上述的head函数,include_body=False,则只返回头部信息给客户端;否则发送的是”GET“请求,那么include_body=True,则会将请求的静态文件数据根据上述的范围,调用 self.flush() 函数把缓存中的数据写入到网络中,传输给客户端。
注:HEAD:只请求页面的头部信息
GET: 请求指定的页面信息,并返回实体主体
转载于:https://www.cnblogs.com/ShaunChen/p/6636122.html
Tornado源码分析 --- 静态文件处理模块相关推荐
- tornado源码分析
tornado源码分析 本源码为tornado1.0版本 源码附带例子helloworld import tornado.httpserver import tornado.ioloop import ...
- MyBatis 源码分析 - 映射文件解析过程
1.简介 在上一篇文章中,我详细分析了 MyBatis 配置文件的解析过程.由于上一篇文章的篇幅比较大,加之映射文件解析过程也比较复杂的原因.所以我将映射文件解析过程的分析内容从上一篇文章中抽取出来, ...
- v52.05 鸿蒙内核源码分析(静态站点) | 五一哪也没去在干这事 | 百篇博客分析HarmonyOS源码
颜渊死,门人欲厚葬之,子曰:"不可."门人厚葬之.子曰:"回也视予犹父也,予不得视犹子也.非我也,夫二三子也." <论语>:先进篇 百篇博客系列篇. ...
- v54.04 鸿蒙内核源码分析(静态链接) | 一个小项目看中间过程 | 百篇博客分析HarmonyOS源码
子曰:"回也其庶乎,屡空.赐不受命,而货殖焉,亿则屡中." <论语>:先进篇 百篇博客系列篇.本篇为: v54.xx 鸿蒙内核源码分析(静态链接篇) | 一个小项目看中 ...
- 鸿蒙轻内核源码分析:异常钩子模块系统中断异常,如何转储异常信息
摘要:本篇介绍下鸿蒙轻内核中异常钩子模块发生系统中断异常时如何转储异常信息. 本文分享自华为云社区<鸿蒙轻内核M核源码分析系列十七(3) 异常信息ExcInfo>,作者: zhushy. ...
- CI源码分析(一)—config配置文件模块
(一) 使用方式 (a) 系统级配置 语言.字符编码.session.cookie等配置项 文件位置: application/config/config.php 加载方式: 自动加载 调用方式: $ ...
- Nginx源码分析:epoll事件处理模块概述
nginx源码分析 nginx-1.11.1 参考书籍<深入理解nginx模块开发与架构解析> 事件处理模块概述 Nginx的高效请求的处理依赖于事件管理机制,本次默认的场景是Linux操 ...
- tfs_client php,TFS 源码分析 写文件操作 Client端
整个写文件的总体流程这里有介绍 主要分析了写文件时,NameServer端的源码分析 这篇文章介绍写文件时,Client端的源码分析 本文描述的内容涉及TFS写入流程图中的step1, step2, ...
- tornado源码分析系列一
先来看一个简单的示例: #!/usr/bin/env python #coding:utf8import socketdef run():sock = socket.socket(socket.AF_ ...
最新文章
- 如何实现模拟人类视觉注意力的循环神经网络?
- 推荐系统User-Item Embedding图算法
- 抢攻5G网络功能虚拟化,英特尔推专用FPGA加速卡
- 11G Oracle RAC添加新表空间时数据文件误放置到本地文件系统的修正
- 3d激光雷达开发(icp匹配)
- 《Effective C#》Item 14:使用构造函数初始化语句
- Python学习笔记九:文件I/O
- pon移动家庭网关有虚拟服务器吗,电信、移动、联通家庭网关对比分析
- 模拟退火算法及MATLAB代码
- centos 7.6编译安装nginx
- wow插件初级基础知识及安装指南
- 输出方波c语言程序,产生锯齿波以及方波的C程序
- Burp Suite爆破Basic认证密码
- 乐视笔试算法题美团算法笔试题
- 打开excel提示损坏的映像的解决办法
- Matlab人形机器人建模与仿真
- 未越狱设备 安装ipa
- 少儿python培训课
- 【学习笔记】无监督行人重识别
- Java中的模块(Module)入门介绍
热门文章
- 百度地图示例左侧的代码编辑器Ace Editor
- ubuntu等linux下自定义设置程序代理工具proxychains简介
- 如何知道网站的IP,然后利用IP登陆网站?
- 蚂蚁金服CTO鲁肃:支付宝成就了我,我做了很多“拧螺丝”的事儿
- 真香!GitHub刚刚宣布:私有库免费啦!
- Redis : redis事务
- 6.exit _exit _Exit
- 【Python】青少年蓝桥杯_每日一题_1.11_奇偶数
- jQuery-点击按钮实现回到顶部的两种方式
- Java 洛谷 P1089 津津的储蓄计划讲解