Tornado源碼分析 --- 靜態文件處理模塊

每一個web框架都會有對靜態文件的處理支持,下面對於Tornado的靜態文件的處理模塊的源碼進行分析,以增強本身對靜態文件處理的理解。html

先從Tornado的主要模塊 web.py 入手,能夠看到在Application類的 __init__() 方法中對靜態文件的處理部分:python

 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_pathios

參數介紹:nginx

  • static_url_prefix:靜態文件的URL前綴,能夠對靜態文件的訪問路徑進行設置,默認爲 "/static/"
  • static_handler_class:處理靜態文件的類,能夠自定義處理靜態文件的動做,默認的爲 tornado.web.StaticFileHandler
  • static_handler_args:處理靜態文件的參數,若是設置了,應該有一個字典被傳入到static_handler_class類的 initialize 方法中

 

默認的靜態文件處理模塊:class StaticFileHandler(RequestHandler)

介紹和用法:git

  • 處理來自某個目錄的靜態文件內容的模塊,若是在「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「下尋找。github

  • 該靜態文件處理模塊須要一個 」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音頻或視頻)。

  子類擴展注意項:web

    • 這個類被設計爲能夠經過子類去進行擴展,可是因爲該靜態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:   請求指定的頁面信息,並返回實體主體

相關文章
相關標籤/搜索