zoukankan      html  css  js  c++  java
  • 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:   请求指定的页面信息,并返回实体主体

        

        

      

  • 相关阅读:
    Representation Data in OpenCascade BRep
    Render OpenCascade Geometry Surfaces in OpenSceneGraph
    Render OpenCascade Geometry Curves in OpenSceneGraph
    OpenCascade Shape Representation in OpenSceneGraph
    Geometry Surface of OpenCascade BRep
    Geometry Curve of OpenCascade BRep
    Tyvj2017清北冬令营入学测试
    Spfa算法模板
    洛谷1016 旅行家的预算
    洛谷1290 欧几里得的游戏
  • 原文地址:https://www.cnblogs.com/ShaunChen/p/6636122.html
Copyright © 2011-2022 走看看