Tornado是一個Python Web框架和異步網絡庫,最初是在FriendFeed上開發的。經過使用非阻塞網絡I/O,Tornado能夠擴展到數萬個開放鏈接,使其成爲長輪詢,WebSockets和其餘須要與每一個用戶創建長鏈接的應用程序的理想選擇。javascript
Tornado大體可分爲四個主要部分:css
RequestHandler
,它是子類,用於建立Web應用程序和各類支持類)。HTTPServer
和AsyncHTTPClient
)的客戶端和服務器端實現。tornado.gen
),它容許以比連接回調更直接的方式編寫異步代碼。這相似於Python 3.5(async def
)中引入的原生協程功能。建議使用原生協程代替tornado.gen
模塊。Tornado Web框架和HTTP服務器一塊兒提供了WSGI的全棧替代方案。雖然能夠在WSGI容器(WSGIAdapter
)中使用Tornado Web框架,或者使用Tornado HTTP服務器做爲其餘WSGI框架(WSGIContainer
)的容器,可是這些組合中的每個都有侷限性,而且要充分利用Tornado,你將會須要一塊兒使用Tornado的Web框架和HTTP服務器。html
實時Web功能須要每一個用戶保持大部分時間爲空閒狀態的長鏈接。在傳統的同步Web服務器中,這意味着將一個線程投入到每一個用戶,這可能很是昂貴。java
爲了最小化併發鏈接的成本,Tornado使用單線程事件循環。這意味着全部應用程序代碼都應該是異步和非阻塞的,由於一次只能有一個操做處於活動狀態。node
術語異步和非阻塞是密切相關的,而且一般能夠互換使用,但它們並不徹底相同。python
函數在返回以前等待某事發生時會阻塞。一個函數可能因爲多種緣由而阻塞:網絡I/O,磁盤I/O,互斥體等。事實上,每一個函數在運行和使用CPU時都會至少有一點阻塞(對於一個極端的例子來講明爲何CPU阻塞必須像其餘類型的阻塞同樣嚴肅,考慮密碼散列函數,如bcrypt,它設計使用數百毫秒的CPU時間,遠遠超過典型的網絡或磁盤訪問)。nginx
函數能夠在某些方面阻塞,在其餘方面非阻塞。在Tornado的上下文中,咱們一般談論在網絡I/O的上下文中阻塞,儘管要最小化全部類型的阻塞。git
一般會在觸發一些後續操做以前致使某些後臺工做發生(這是相對於正常的異步函數來講,正常的異步函數會在return以前作完它們要作的全部事情)。有不少種不一樣種類的異步接口: github
不管使用哪一種類型的接口,定義的異步函數與其調用者的交互方式都不一樣,沒有一種自由的方式可使同步函數以對其調用者透明的方式實現異步web
(像gevent這樣的系統使用輕量級線程來提供與異步系統至關的性能,但它們實際上並無實現異步)。
Tornado中的異步操做一般返回佔位符對象(Futures
),但一些低級別組件除外,好比IOLoop
中就使用回調。Futures
一般會經過await
或yield
關鍵字返回結果。
下面是一個同步函數的例子:
from tornado.httpclient import HTTPClient def synchronous_fetch(url): http_client = HTTPClient() response = http_client.fetch(url) return response.body
下面使用原生協程重寫,使之變爲一個實現相同功能的異步函數:
from tornado.httpclient import AsyncHTTPClient async def asynchronous_fetch(url): http_client = AsyncHTTPClient() response = await http_client.fetch(url) return response.body
或者爲了與舊版本的Python兼容,使用tornado.gen模塊:
from tornado.httpclient import AsyncHTTPClient from tornado import gen @gen.coroutine def async_fetch_gen(url): http_client = AsyncHTTPClient() response = yield http_client.fetch(url) raise gen.Return(response.body)
協程是否是看上去有點神奇,但他們內部作的是這樣的:
from tornado.concurrent import Future def async_fetch_manual(url): http_client = AsyncHTTPClient() my_future = Future() fetch_future = http_client.fetch(url) def on_fetch(f): my_future.set_result(f.result().body) fetch_future.add_done_callback(on_fetch) return my_future
請注意,協程在獲取完成以前返回其Future。 這是協程異步的緣由。
你能夠經過傳遞迴調對象來執行協程所能作的任何事情,但協程能夠像寫同步代碼同樣實現異步功能,這樣能夠簡化咱們的代碼。這對於錯誤處理尤其重要,由於try/except
塊的工做方式與協程中的預期相同,
而回調很難實現。
Coroutines是在Tornado中編寫異步代碼的推薦方法。Coroutines使用Pythonawait
或yield
關鍵字來掛起和恢復執行而不是一系列回調(在gevent這樣的框架中看到的協做輕量級線程有時也被稱爲協程,但在Tornado中全部協程都使用顯式上下文切換並被稱爲異步函數)。
協程幾乎和同步代碼同樣簡單,並且沒有線程那樣的昂貴開銷。它們還經過減小可能發生的上下文切換來簡化併發。
例子:
async def fetch_coroutine(url): http_client = AsyncHTTPClient() response = await http_client.fetch(url) return response.body
Python 3.5引入了async和await關鍵字(使用這些關鍵字的函數也稱爲「native coroutines」)。 爲了與舊版本的Python兼容,您可使用tornado.gen.coroutine裝飾器來使用「decorated」或「yield-based」的協程。
儘量使用原生協程。 僅在須要與舊版本的Python兼容時才使用裝飾器協程。Tornado文檔中的示例一般使用原生形式。
兩種形式之間的轉換一般很簡單:
# Decorated: # Native: # Normal function declaration # with decorator # "async def" keywords @gen.coroutine def a(): async def a(): # "yield" all async funcs # "await" all async funcs b = yield c() b = await c() # "return" and "yield" # cannot be mixed in # Python 2, so raise a # special exception. # Return normally raise gen.Return(b) return b
其它兩種形式的協程區別:
async for
和async
語句,這使得某些模式更加簡單。yield
或await
它們,不然原生協程根本不會運行。裝飾器協程一旦被調用就能夠「在後臺」開始運行。請注意,對於這兩種協程,使用await
或yield
很重要,這樣任何異常才能正常拋出。executor.submi
的結果。對於原生協程,請改用IOLoop.run_in_executor
。tornado.gen.convert_yielded
。本節介紹裝飾器協程的操做。原生協程在概念上是類似的,但因爲與Python運行時的額外集成而稍微複雜一些。
包含yield
的函數是生成器。全部生成器都是異步的,在調用時,它們返回一個生成器對象而不是運行到完成。 @gen.coroutine
裝飾器經過yield
表達式與生成器通訊,並經過返回Future
與協程的調用者通訊。
這是協程裝飾器內循環的簡化版本:
# Simplified inner loop of tornado.gen.Runner def run(self): # send(x) makes the current yield return x. # It returns when the next yield is reached future = self.gen.send(self.next) def callback(f): self.next = f.result() self.run() future.add_done_callback(callback)
裝飾器從生成器接收Future
,等待(不阻塞)該Future
完成,而後「展開」Future
並將結果做爲yield
表達式的結果發送回生成器。 大多數異步代碼從不直接接觸Future
類,除非當即將異步函數返回的Future
傳遞給yield
表達式。
協程不會以正常方式拋出異常:它們拋出的任何異常都將被困在等待對象中,直到它被放棄爲止。 這意味着以正確的方式調用協同程序很重要,不然您可能會發現未被注意到的錯誤:
async def divide(x, y): return x / y def bad_call(): # This should raise a ZeroDivisionError, but it won't because # the coroutine is called incorrectly. divide(1, 0)
在幾乎全部狀況下,任何調用協程的函數都必須是協程自己,並在調用中使用await
或yield
關鍵字。 當重寫超類中定義的方法時,請查閱文檔以查看是否容許協程(文檔應該說方法「多是協程」或「可能返回Future
」):
async def good_call(): # await will unwrap the object returned by divide() and raise # the exception. await divide(1, 0)
有時你可能想要「Fire and forget」一個協程而不等待它的結果。在這種狀況下,建議使用IOLoop.spawn_callback
,這使得IOLoop
負責調用。 若是失敗,IOLoop
將記錄堆棧路徑:
# The IOLoop will catch the exception and print a stack trace in # the logs. Note that this doesn't look like a normal call, since # we pass the function object to be called by the IOLoop. IOLoop.current().spawn_callback(divide, 1, 0)
對於使用@gen.coroutine
的函數,建議以這種方式使用IOLoop.spawn_callback
,可是使用async def
的函數須要它(不然協程運行程序將沒法啓動)。
最後,在程序的頂層,若是IOLoop
還沒有運行,您能夠啓動IOLoop
,運行協程,而後使用IOLoop.run_sync
方法中止IOLoop。 這一般用於啓動面向批處理( batch-oriented)程序的main
函數:
# run_sync() doesn't take arguments, so we must wrap the # call in a lambda. IOLoop.current().run_sync(lambda: divide(1, 0))
從協程中調用一個阻塞函數的最簡單的方法就是使用ThreadPoolExecutor
,返回一個其餘協程兼容的Futures
對象:
async def call_blocking(): await IOLoop.current().run_in_executor(None, blocking_func, args)
multi
函數接受其值爲Futures
的列表和dicts,而且並行等待全部這些Futures
:
from tornado.gen import multi async def parallel_fetch(url1, url2): resp1, resp2 = await multi([http_client.fetch(url1), http_client.fetch(url2)]) async def parallel_fetch_many(urls): responses = await multi ([http_client.fetch(url) for url in urls]) # responses is a list of HTTPResponses in the same order async def parallel_fetch_dict(urls): responses = await multi({url: http_client.fetch(url) for url in urls}) # responses is a dict {url: HTTPResponse}
在裝飾器協程中,能夠直接yield
列表或字典:
@gen.coroutine def parallel_fetch_decorated(url1, url2): resp1, resp2 = yield [http_client.fetch(url1), http_client.fetch(url2)]
有時保存一個Future對象比當即yield它會更有用,以便你能夠在等待以前開始開始另外一個操做:
from tornado.gen import convert_yielded async def get(self): # convert_yielded() starts the native coroutine in the background. # This is equivalent to asyncio.ensure_future() (both work in Tornado). fetch_future = convert_yielded(self.fetch_next_chunk()) while True: chunk = yield fetch_future if chunk is None: break self.write(chunk) fetch_future = convert_yielded(self.fetch_next_chunk()) yield self.flush()
這對於裝飾的協同程序來講更容易一些,由於它們在被調用時當即啓動:
@gen.coroutine def get(self): fetch_future = self.fetch_next_chunk() while True: chunk = yield fetch_future if chunk is None: break self.write(chunk) fetch_future = self.fetch_next_chunk() yield self.flush()
在原生協程中,可使用async for
。在舊版本的Python中,循環對於協程來講很棘手,由於沒法在for
循環或while
循環的每次迭代中yield
並捕獲yield
的結果。相反,您須要將循環條件與訪問結果分開,如本例中的Motor
:
import motor db = motor.MotorClient().test @gen.coroutine def loop_example(collection): cursor = db.collection.find() while (yield cursor.fetch_next): doc = cursor.next_object()
PeriodicCallback
一般不與協同程序一塊兒使用。相反,一個協程能夠包含一個while True:
:循環並使用tornado.gen.sleep
:
async def minute_loop(): while True: await do_something() await gen.sleep(60) # Coroutines that loop forever are generally started with # spawn_callback(). IOLoop.current().spawn_callback(minute_loop)
有時可能須要更復雜的循環。 例如,前一個循環每60 + N
秒運行一次,其中N
是do_something()
的運行時間。 要徹底每60秒運行一次,請使用上面的交叉存取:
async def minute_loop2(): while True: nxt = gen.sleep(60) # Start the clock. await do_something() # Run while the clock is ticking. await nxt # Wait for the timer to run out.
Tornado的tornado.queues
模塊爲協程實現異步生產者/消費者模式,相似於Python標準庫的隊列模塊爲線程實現的模式。
一個yieldQueue.get
的協程直到隊列中有元素以前都會暫停。若是隊列設置了最大容量,一個yieldQueue.put
的協程在隊列有空間以前都會暫停。
一個Queue
維護一個從零開始的未完成任務的計數。put
增長計數; task_done
減小計數。
在此處的web-spider示例中,隊列開始僅包含base_url。當一個worker獲取一個頁面時,它會解析連接並將新的連接放入隊列中,而後調用task_done
來減小一次計數器。 最終,一個worker獲取一個以前URL已經被訪問過的頁面,而且隊列中也沒有剩餘的工做。 所以,該worker對task_done
的調用將計數器減小爲零。 正在等待join
的主協程將取消暫停而後結束。
#!/usr/bin/env python3 import time from datetime import timedelta from html.parser import HTMLParser from urllib.parse import urljoin, urldefrag from tornado import gen, httpclient, ioloop, queues base_url = 'http://www.tornadoweb.org/en/stable/' concurrency = 10 async def get_links_from_url(url): """Download the page at `url` and parse it for links. Returned links have had the fragment after `#` removed, and have been made absolute so, e.g. the URL 'gen.html#tornado.gen.coroutine' becomes 'http://www.tornadoweb.org/en/stable/gen.html'. """ response = await httpclient.AsyncHTTPClient().fetch(url) print('fetched %s' % url) html = response.body.decode(errors='ignore') return [urljoin(url, remove_fragment(new_url)) for new_url in get_links(html)] def remove_fragment(url): pure_url, frag = urldefrag(url) return pure_url def get_links(html): class URLSeeker(HTMLParser): def __init__(self): HTMLParser.__init__(self) self.urls = [] def handle_starttag(self, tag, attrs): href = dict(attrs).get('href') if href and tag == 'a': self.urls.append(href) url_seeker = URLSeeker() url_seeker.feed(html) return url_seeker.urls async def main(): q = queues.Queue() start = time.time() fetching, fetched = set(), set() async def fetch_url(current_url): if current_url in fetching: return print('fetching %s' % current_url) fetching.add(current_url) urls = await get_links_from_url(current_url) fetched.add(current_url) for new_url in urls: # Only follow links beneath the base URL if new_url.startswith(base_url): await q.put(new_url) async def worker(): async for url in q: if url is None: return try: await fetch_url(url) except Exception as e: print('Exception: %s %s' % (e, url)) finally: q.task_done() await q.put(base_url) # Start workers, then wait for the work queue to be empty. workers = gen.multi([worker() for _ in range(concurrency)]) await q.join(timeout=timedelta(seconds=300)) assert fetching == fetched print('Done in %d seconds, fetched %s URLs.' % ( time.time() - start, len(fetched))) # Signal all the workers to exit. for _ in range(concurrency): await q.put(None) await workers if __name__ == '__main__': io_loop = ioloop.IOLoop.current() io_loop.run_sync(main)
Tornado Web應用程序一般由一個或多個RequestHandler
子類,一個將請求路由處處理程序(handlers)的Application
對象和一個用於啓動服務器的main()
函數組成。
最小的「hello world」示例以下所示:
import tornado.ioloop import tornado.web class MainHandler(tornado.web.RequestHandler): def get(self): self.write("Hello, world") def make_app(): return tornado.web.Application([ (r"/", MainHandler), ]) if __name__ == "__main__": app = make_app() app.listen(8888) tornado.ioloop.IOLoop.current().start()
Application
對象Application對象負責全局配置,包括將請求映射處處理程序的路由表。
路由表是URLSpec
對象(或元組)的列表,每一個對象包含(至少)正則表達式和處理程序類。 順序很重要: 最終生效的是第一個匹配規則。若是正則表達式包含匹配分組,
這些分組會做爲路徑參數傳遞給處理器的HTTP方法。若是一個字典做爲URLSpec
的第三方元素被傳遞進來,它支持initialization參數,這個參數會傳遞給RequestHandler.initializa
方法。
最後,URLSpec
可能還會有一個名字,這將使它可以被RequestHandler.reverse_url
方法使用。
例如,在此片斷中,根URL /
映射到MainHandler
,表單/story/
後跟數字的URL映射到StoryHandler
。 該數字(做爲字符串)傳遞給StoryHandler.get
。
class MainHandler(RequestHandler): def get(self): self.write('<a href="%s">link to story 1</a>' % self.reverse_url("story", "1")) class StoryHandler(RequestHandler): def initialize(self, db): self.db = db def get(self, story_id): self.write("this is story %s" % story_id) app = Application([ url(r"/", MainHandler), url(r"/story/([0-9]+)", StoryHandler, dict(db=db), name="story") ])
Application
構造器接受許多能用來指定應用行爲的關鍵字參數,而且容許可選特性;完整列表請參看Application.settings
。
RequestHandler
Tornado Web應用程序的大部分工做都是在RequestHandler
的子類中完成的。處理器子類的主要入口是處理對應的HTTP方法:get()
,post()
等等。每一個處理程序能夠定義一個或多個這些方法來處理不一樣的HTTP操做。如上所述,這些方法將會加上路徑中匹配到的參數來被調用。
在處理程序中,調用RequestHandler.render
或RequestHandler.write
等方法來生成響應。 render()
按名稱加載模板,並使用給定的參數呈現它。 write()
用於非基於模板的輸出;它接受strings,bytes和字典(dicts將被編碼爲JSON)。
RequestHandler
中的許多方法都設計爲在子類中重寫,並能夠在整個應用程序中使用。一般定義一個BaseHandler
類來重寫諸如write_error
和get_current_user
之類的方法,而後把你的BaseHandler代替RequestHandler
做爲全部處理器的基類。
請求處理程序可使用self.request
訪問表示當前請求的對象。有關完整的屬性列表,請參閱HTTPServerRequest
的類定義。
HTML表單提交的請求數據將會爲你分析好,而且能夠在一些方法像get_query_argument
和get_body_argument中
可用。
class MyFormHandler(tornado.web.RequestHandler): def get(self): self.write('<html><body><form action="/myform" method="POST">' '<input type="text" name="message">' '<input type="submit" value="Submit">' '</form></body></html>') def post(self): self.set_header("Content-Type", "text/plain") self.write("You wrote " + self.get_body_argument("message"))
由於HTML表單編碼對於一個參數是單值仍是列表是模糊不清的,RequestHandler
對方法進行了區分,使得應用可以代表是否但願得到列表,對列表來講,使用get_query_arguments
和get_body_arguments
代替單值的方法。
經過表單上傳文件能夠經過self.request.files
來得到,映射名字(input的name屬性)到文件列表。每一個文件都是{"filename":…, "content_type":…, "body":…}
格式的字典。files
對象文件僅在使用表單包裝器上傳文件時才存在(例如multipart/form-data Content-Type
);若是沒有使用這種格式,原生的上傳數據會存放在self.request.boby
中。默認狀況下上傳的文件是所有緩存在內存中的;若是你須要處理一些很大的文件,不方便放在內存中,查看stream_request_body
裝飾器。
在demos文件夾中(tornado源碼中有一個demos文件夾,存放了幾個小的例子),file_reciver.py展現了這兩種方法的來接收上傳文件。
因爲HTML表單編碼的問題(單值和多值的模糊性),Tornado並不打算用其餘類型的輸入來統一表單參數。尤爲咱們不會去分析JSON請求體。但願使用JSON來代替form-encoding的應用可能會重寫prepare
方法來解析它們的請求:
def prepare(self): if self.request.headers.get("Content-Type", "").startswith("application/json"): self.json_args = json.loads(self.request.body) else: self.json_args = None
除了get()
和post()
方法,在RequestHandler
還有一些其餘方法在必要時也能夠被子類重寫。在每一個請求中,都會發生如下一系列的調用:
RequestHandler
對象。Application
配置中得到初始參數的話initialize()
函數會被調用,initialize
函數應該只保存傳遞給成員變量的參數;它可能不會產生任何輸出或調用相似send_error
同樣的方法。prepare
方法。這在一個基類中是最有用的,基類由你的處理器子類所共享,由於不論使用哪一種HTTP方法prepare
函數都會被調用。prepare
可能會產生輸出;若是它調用了finish
(或者redirect
方法等),進程就會在此結束。get()
,post()
,put()
等等。若是URL正則表達式包含了捕捉分組參數(capturing groups),這些參數也會傳遞到此方法中。on_finish
會被調用,對於大多數處理程序,這一步在get()
(或其餘方法)return後就會當即執行;對於使用tornado.web.asynchronous
裝飾器的處理程序,它發生在調用finish()
以後。正如在RequestHandler
文檔中提到的同樣,全部方法都是能夠重寫的。 一些最常被重寫的方法包括:
write_error
- 輸出html的錯誤頁面。on_connection_close
當客戶端斷開鏈接的時候調用;應用可能會選擇檢測這種狀況而後中止更深層的處理,注意,不能保證一個已關閉的鏈接也能被及時檢測到。 請注意,沒法保證能夠當即檢測到已關閉的鏈接。get_current_user
- 請參閱用戶身份驗證(User authentication)。get_user_locale
- 爲當前用戶返回一個locale
對象。set_default_headers
- 可用於在response上設置其餘的響應頭(例如自定義Server
header)若是處理器拋出一個異常,tornado會調用RequestHandler.write_error
來生成一個錯誤頁面。tornado.web.HTTPError
能夠被用來產生一個特定的狀態碼;其餘全部異常都返回500狀態碼。
默認的錯誤頁面包括一個堆棧路徑(debug模式下)另外還有一個錯誤的線上描述(move brand_model.txt to project)。爲了生成一個本身的錯誤頁面,重寫RequestHandler.write_error
(多是在一個由你的全部處理器所共享的基類中)這個方法能夠用過write
和render
等方法產生正常的輸出。若是錯誤是由一個異常致使的,一個exc_info
會做爲一個關鍵字參數被傳遞進來(注意,此異常不保證是sys.exc_info
中當前的異常,所以write_error
必須使用traceback.format_exception
來代替traceback.format_exc
)。
經過調用set_status
,寫一個響應或return等方法從常規的處理器方法(而不是write_error
)中產生一個錯誤頁面也是可能的。tornado.web.Finish
這個特殊的異常可能會被拋出以終止處理器,在簡單地返回不方便的狀況下並不調用write_error
方法。
對於404錯誤來講,使用default_handler_class``````Application setting
。這個處理器用改重寫prepare
方法,而不是更詳細的如get()
方法,以便在任何HTTP方法中都能使用。它應該產生一個如上所述的錯誤頁面:或者經過拋出一個HTTPError(404)
並重寫爲write_error
,或者調用self.set_status(404)
並在prepare()
中直接產生響應。
在Tornado中有兩種方式能夠重定一個請求:RequestHandler.redirect
和RedirectHandler
。
您能夠在RequestHandler
方法中使用self.redirect()
將用戶重定向到其餘位置。還有一個可選參數permanent
,可用於代表該重定向爲永久重定向。 permanent
的默認值爲False
,它生成302 Found
HTTP響應代碼,適用於成功POST請求後重定向用戶等事項。 若是permanent
是true
,301 Moved Permanently
HTTP響應碼會被使用,在下面這中狀況下是有用的:重定向到一個權威的URL來採用一種SEO友好的方式獲取頁面。
RedirectHandler
可讓你在你的Application
路由表中直接配置重定向。例如,配置一個靜態重定向:
app = tornado.web.Application([ url(r"/app", tornado.web.RedirectHandler, dict(url="http://itunes.apple.com/my-app-id")), ])
RedirectHandler
還支持正則表達式替換。如下規則將以/pictures/
開頭的全部請求重定向到前綴/photos/
:
app = tornado.web.Application([ url(r"/photos/(.*)", MyPhotoHandler), url(r"/pictures/(.*)", tornado.web.RedirectHandler, dict(url=r"/photos/{0}")), ])
不像RequestHandler.redirect
,RedirectHandler
默認使用永久重定向。這是由於路由表在運行過程當中不會改變而且是永久的,儘管在處理器中的重定向極可能是其餘可能回改變的邏輯所致使的。若是想使用RedirectHandler
發起一個臨時重定向,只須要把permanent=False
參數加到RedirectHandler
的初始化參數中。
某些處理程序方法(包括prepare()
和HTTP請求方法get()
、post()
等)能夠做爲協程重寫,以實現異步化。
Tornado還支持使用tornado.web.asynchronous
裝飾器那樣基於回調的異步處理程序樣式,但這種樣式已棄用,將在Tornado 6.0中刪除。新的應用程序應該使用協程代替舊的寫法。
下面是一個簡單的使用協程的handler示例:
class MainHandler(tornado.web.RequestHandler): async def get(self): http = tornado.httpclient.AsyncHTTPClient() response = await http.fetch("http://friendfeed-api.com/v2/feed/bret") json = tornado.escape.json_decode(response.body) self.write("Fetched " + str(len(json["entries"])) + " entries "
"from the FriendFeed API")
有關更高級的異步示例,請查看聊天示例應用程序,該應用程序使用長輪詢實現AJAX聊天室。 長輪詢的用戶可能但願在客戶端關閉鏈接後重寫on_connection_close
()以進行清理操做(但要注意的是,請參見該方法的docstring)。
Tornado包含一種簡單,快速,靈活的模板語言。 本節介紹該語言以及國際化等相關問題。
Tornado能夠任意使用其餘的Python模板語言,哪怕它們並無被集成到RequestHandler.render
中。 只需將模板渲染爲字符串並將其傳遞給RequestHandler.write
便可。
默認狀況下,Tornado在與引用它們的.py文件的目錄中查找模板文件。要將模板文件放在不一樣的目錄中,請使用template_path
應用程序設置(若是不一樣的處理程序具備不一樣的模板路徑,則修改RequestHandler.get_template_path
)便可。
要從非文件系統位置加載模板,請子類化tornado.template.BaseLoader
,並傳遞一個使用template_loader
應用程序設置的實例。
默認狀況下,編譯的模板會被緩存;要關閉此緩存和從新加載模板,以便始終能夠看到對底層文件的更改,請使用應用程序設置compiled_template_cache = False
或debug = True
。
Tornado模板只是使用Python控制語句和內嵌表達式標記的HTML(或任何其餘基於文本的格式)。
<html> <head> <title>{{ title }}</title> </head> <body> <ul> {% for item in items %} <li>{{ escape(item) }}</li> {% end %} </ul> </body> </html>
若是你將這個模板保存爲「template.html」並將它放入python文件所在的目錄中,你可使用下面的代碼渲染這個模板:
class MainHandler(tornado.web.RequestHandler): def get(self): items = ["Item 1", "Item 2", "Item 3"] self.render("template.html", title="My title", items=items)
Tornado模板支持控制語句和表達式。 控制語句由{%
和%}
包圍,例如{% if len(items) > 2 %}
。 表達式由{{
和}}
包圍,例如{{items [0]}}
。
控制語句或多或少地映射到Python語句。咱們支持if
,for
,while
和try
,全部這些都以{% end% }
結束。咱們還使用extends
和block
語句支持模板繼承(template inheritance),這些語句在tornado.template
的文檔中有詳細描述。
表達式能夠是包括函數調用的任何Python表達式。模板代碼在包含如下對象和函數的命名空間中執行(請注意,此列表僅適用於使用RequestHandler.render
和render_string
渲染的模板。若是你直接在RequestHandler
以外使用tornado.template
模塊,下面的許多項都將沒法使用)。
escape
: tornado.escape.xhtml_escapexhtml_escape
: tornado.escape.xhtml_escapeurl_escape
: tornado.escape.url_escapejson_encode
: tornado.escape.json_encodesqueeze
: tornado.escape.squeezelinkify
: tornado.escape.linkifydatetime
: the Python datetime modulehandler
: the current RequestHandler objectrequest
: handler.requestcurrent_user
: handler.current_userlocale
: handler.locale_
: handler.locale.translatestatic_url
: handler.static_urlxsrf_form_html
: handler.xsrf_form_htmlreverse_url
: Application.reverse_urlui_methods
和 ui_modules``````Application
的設置項render
或render_string
的關鍵詞在構建實際應用程序時,你將可能會使用到Tornado模板的全部功能,尤爲是模板繼承。 閱讀tornado.template
部分中有關這些功能的全部內容(包括UIDodules
在內的某些在tornado.web
模塊中實現的功能)
實際上Tornado模板在後臺將直接轉換爲Python語言。你在模板中包含的表達式將逐字複製到表示模板的Python函數中。 咱們不會試圖阻止模板語言中的任何內容;咱們建立模板時就爲了提供比其餘相對嚴格的模板系統中所缺乏的靈活性。所以,若是在模板表達式中編寫不受控制的內容,則在Python中執行模板時將會出現不可預知的錯誤。
默認狀況下,使用tornado.escape.xhtml_escape
函數對全部模板輸出進行轉義。 能夠經過將autoescape = None
傳遞給Application
或tornado.template.Loader
構造函數,能夠進行全局的轉義開關設置,也可使用{% autoescape None %}
指令的模板文件或在單個表達式中使用{% raw ...%}
替換{{...}}
達到關閉轉義的目的。另外,在全部位置中均可以使用轉義函數的名稱代替None
。
請注意,雖然Tornado的自動轉義有助於避免XSS漏洞,但不能保證在全部的狀況下都有用。出如今某些位置的表達式(例如Javascript或CSS)可能須要額外的轉義。此外,必須注意始終在可能包含不可信內容的HTML屬性中使用雙引號和xhtml_escape,或者必須爲屬性使用單獨的轉義函數(可在http://wonko.com/post/html-escaping中查看示例)。
當前用戶的區域設置(不管它們是否登陸)始終在請求處理程序中以self.locale
的形式提供,在模板中始終以locale
的形式提供。
語言環境的名稱(例如en_US
)儲存在locale.name
中,您可使用Locale.translate
方法翻譯字符串。模板還有_()
這樣可用於字符串轉換的全局函數調用。 translate函數有兩種形式:
_("Translate this string")
它根據當前語言環境直接翻譯字符串,而且:
_("A person liked this", "%(num)d people liked this", len(people)) % {"num": len(people)}
它根據第三個參數的值翻譯一個能夠是單數或複數的字符串。在上面的示例中,若是len(people)
爲1,則將返回第一個字符串的翻譯,不然將返回第二個字符串的翻譯。
下面是一個恰當的國際化模板:
<html> <head> <title>FriendFeed - {{ _("Sign in") }}</title> </head> <body> <form action="{{ request.path }}" method="post"> <div>{{ _("Username") }} <input type="text" name="username"/></div> <div>{{ _("Password") }} <input type="password" name="password"/></div> <div><input type="submit" value="{{ _("Sign in") }}"/></div> {% module xsrf_form_html() %} </form> </body> </html>
默認狀況下,咱們使用用戶瀏覽器發送的Accept-Language
標頭檢測用戶的語言環境。若是咱們找不到合適的Accept-Language
值,咱們選擇en_US
。
若是你容許用戶將其區域設置設置爲首選項,則能夠經過重寫RequestHandler.get_user_locale
來替換此默認區域設置選擇:
class BaseHandler(tornado.web.RequestHandler): def get_current_user(self): user_id = self.get_secure_cookie("user") if not user_id: return None return self.backend.get_user_by_id(user_id) def get_user_locale(self): if "locale" not in self.current_user.prefs: # Use the Accept-Language header return None return self.current_user.prefs["locale"]
若是get_user_locale
返回None
,咱們將返回Accept-Language
標頭。
tornado.locale
模塊支持以兩種格式加載翻譯:gettext
及其相關工具使用的.mo
格式,還有簡單的.csv
格式。 應用程序一般會在啓動時調用tornado.locale.load_translations
或tornado.locale.load_gettext_translations
; 有關支持的格式的詳細信息,請參閱這些方法。
您可使用tornado.locale.get_supported_locales()
獲取應用程序中所支持的語言環境列表。 將在支持的語言環境列表中選擇最接近的匹配項做爲用戶的語言環境。例如,若是用戶的語言環境是es_GT
,而且支持es
語言環境,那麼在該請求中,self.locale
的值就是es
。若是找不到接近的匹配項,咱們會切換回en_US
。
Tornado支持UI模塊(UI modules),這將使你的應用能夠更輕鬆地支持標準化、可複用的UI組件。 UI模塊就像用渲染頁面組件的特殊函數調用同樣,它們能夠與本身的CSS和JavaScript打包在一塊兒。
例如,若是你想要本身寫一個博客,而且但願在博客主頁和每一個博客條目頁面上都顯示博客條目,則能夠建立一個Entry
模塊以在全部頁面上渲染它們。首先,爲UI模塊建立一個Python模塊,例如uimodules.py
:
class Entry(tornado.web.UIModule): def render(self, entry, show_comments=False): return self.render_string( "module-entry.html", entry=entry, show_comments=show_comments)
在應用程序中經過ui_modules
設置告訴Tornado使用uimodules.py
:
from . import uimodules class HomeHandler(tornado.web.RequestHandler): def get(self): entries = self.db.query("SELECT * FROM entries ORDER BY date DESC") self.render("home.html", entries=entries) class EntryHandler(tornado.web.RequestHandler): def get(self, entry_id): entry = self.db.get("SELECT * FROM entries WHERE id = %s", entry_id) if not entry: raise tornado.web.HTTPError(404) self.render("entry.html", entry=entry) settings = { "ui_modules": uimodules, } application = tornado.web.Application([ (r"/", HomeHandler), (r"/entry/([0-9]+)", EntryHandler), ], **settings)
在模板中,你可使用{% module %}
語句調用模塊。例如,你能夠從home.html
調用Entry
模塊:
{% for entry in entries %} {% module Entry(entry) %} {% end %}
以及從entry.html
中調用:
{% module Entry(entry, show_comments=True) %}
模塊能夠經過重寫embedded_css
,embedded_javascript
,javascript_files
或css_files
方法來包含自定義CSS和JavaScript函數:
class Entry(tornado.web.UIModule): def embedded_css(self): return ".entry { margin-bottom: 1em; }" def render(self, entry, show_comments=False): return self.render_string( "module-entry.html", show_comments=show_comments)
不管在頁面上使用模塊多少次,CSS和JavaScript模塊都將包含在內。 CSS始終包含在頁面的<head>
中,而且JavaScript老是包含在頁面末尾的</body>
標記以前。
當不須要額外的Python代碼時,模板文件自己能夠用做模塊。例如,能夠重寫前面的示例以將如下內容放在module-entry.html
中:
{{ set_resources(embedded_css=".entry { margin-bottom: 1em; }") }} <!-- more template html... -->
將使用下面命令調用重寫後的模板模塊:
{% module Template("module-entry.html", show_comments=True) %}
set_resources
函數僅在經過{% module Template(...) %}
調用的模板中可用。與{% include ... %}
指令不一樣,模板模塊與其包含模板具備不一樣的命名空間——它們只能
看到全局模板命名空間和它們本身的關鍵字參數。
你可使用set_cookie
方法在用戶的瀏覽器中設置cookie:
class MainHandler(tornado.web.RequestHandler): def get(self): if not self.get_cookie("mycookie"): self.set_cookie("mycookie", "myvalue") self.write("Your cookie was not set yet!") else: self.write("Your cookie was set!")
Cookie並不安全,客戶端能夠輕鬆修改。若是你須要設置Cookie,例如,識別當前登陸的用戶,則須要對cookie簽名以防止僞造。
Tornado支持使用set_secure_cookie
和get_secure_cookie
方法簽名的cookie。 要使用這些方法,您須要在建立應用程序時指定名爲cookie_secret
的密鑰。
您能夠將設置做爲關鍵字參數傳遞給應用程序:
application = tornado.web.Application([ (r"/", MainHandler), ], cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__")
除了時間戳和HMAC簽名以外,簽名cookie還包含cookie的編碼值。若是cookie是舊的或簽名不匹配,get_secure_cookie
將返回None,就像沒有設置cookie同樣。
以上示例的安全版本:
class MainHandler(tornado.web.RequestHandler): def get(self): if not self.get_secure_cookie("mycookie"): self.set_secure_cookie("mycookie", "myvalue") self.write("Your cookie was not set yet!") else: self.write("Your cookie was set!")
Tornado的安全cookie確保完整性但並未對cookie密。也就是說,cookie雖然不能被修改,但客戶端能夠看到裏面的內容。 cookie_secret是一個對稱密鑰,必須保密——任何得到此密鑰值的人均可以生成本身的簽名cookie。
默認狀況下,Tornado的安全cookie將在30天后過時。要更改此設置,請使用set_secure_cookie
的expires_days
關鍵字參數和get_secure_cookie
的max_age_days
參數。 這兩個值是分開傳遞的,這樣設計的緣由是,好比你能夠對於大多數用途,設置有一個有效期爲30天的cookie,但對於某些敏感操做(例如更改賬單信息),在讀取cookie時使用較小的max_age_days
。
Tornado還支持多個簽名密鑰以啓用簽名密鑰輪換。cookie_secret
必須是一個以一個整數類型的版本號做爲鍵值,並以相應的密鑰做爲值的字典。須要注意的是,只能使用應用中經過key_version
設置的那個版本的密鑰進行簽名,但可使用字典中全部其餘密鑰進行cookie簽名的驗證。 要實現cookie更新,能夠經過get_secure_cookie_key_version
查詢當前的簽名密鑰版本。
當前通過身份驗證的用戶能夠在每一個請求處理程序中的self.current_user
獲取到,在每一個模板中則從current_user
中獲取。 默認狀況下,current_user
爲None
。
要在應用程序中實現用戶身份驗證,你須要重寫請求處理程序中的get_current_user()
方法,以根據cookie的值肯定當前用戶。這是一個只須要驗證cookie中保存的用戶暱稱就能夠登陸應用的示例:
class BaseHandler(tornado.web.RequestHandler): def get_current_user(self): return self.get_secure_cookie("user") class MainHandler(BaseHandler): def get(self): if not self.current_user: self.redirect("/login") return name = tornado.escape.xhtml_escape(self.current_user) self.write("Hello, " + name) class LoginHandler(BaseHandler): def get(self): self.write('<html><body><form action="/login" method="post">' 'Name: <input type="text" name="name">' '<input type="submit" value="Sign in">' '</form></body></html>') def post(self): self.set_secure_cookie("user", self.get_argument("name")) self.redirect("/") application = tornado.web.Application([ (r"/", MainHandler), (r"/login", LoginHandler), ], cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__")
你能夠要求用戶使用Python裝飾器tornado.web.authenticated
登陸。 若是請求轉到使用此裝飾器的方法,而且用戶未登陸,則會將其重定向到login_url(另外一個應用程序設置)。
上面的例子能夠這樣重寫:
class MainHandler(BaseHandler): @tornado.web.authenticated def get(self): name = tornado.escape.xhtml_escape(self.current_user) self.write("Hello, " + name) settings = { "cookie_secret": "__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__", "login_url": "/login", } application = tornado.web.Application([ (r"/", MainHandler), (r"/login", LoginHandler), ], **settings)
若是使用authenticated
裝飾器裝飾的post()
方法,而且用戶未登陸,則服務器將發送403響應。 @authenticated
裝飾器只是簡寫,若是不是self.current_user:self.redirect()
,可能不適合非基於瀏覽器的登陸方案。
查看Tornado Blog示例應用程序,獲取使用身份驗證的完整示例(並將用戶數據存儲在MySQL數據庫中)。
tornado.auth
模塊爲網絡上許多最受歡迎的網站實施身份驗證和受權協議,包括Google / Gmail,Facebook,Twitter和FriendFeed。該模塊包括經過這些站點記錄用戶的方法,以及在適用的狀況下受權訪問服務的方法,以便你下載用戶的地址簿或表明他們發佈Twitter消息。
如下是使用Google進行身份驗證的示例處理程序,將Google憑據保存在Cookie中以供往後訪問:
class GoogleOAuth2LoginHandler(tornado.web.RequestHandler, tornado.auth.GoogleOAuth2Mixin): async def get(self): if self.get_argument('code', False): user = await self.get_authenticated_user( redirect_uri='http://your.site.com/auth/google', code=self.get_argument('code')) # Save the user with e.g. set_secure_cookie else: await self.authorize_redirect( redirect_uri='http://your.site.com/auth/google', client_id=self.settings['google_oauth']['key'], scope=['profile', 'email'], response_type='code', extra_params={'approval_prompt': 'auto'})
有關更多詳細信息,請參閱tornado.auth
模塊文檔。
跨站點請求僞造(XSRF)是我的Web應用程序的常見問題。有關XSRF如何工做的更多信息,請參閱Wikipedia文章。
廣泛接受的防止XSRF的解決方案是爲每一個用戶提供不可預測的值,並將該值做爲附加參數包含在網站上的每一個表單提交中。 若是cookie和表單提交中的值不匹配,則該請求多是僞造的。
Tornado內置XSRF保護。 要在你的站點中使用的話須要設置xsrf_cookies
:
settings = { "cookie_secret": "__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__", "login_url": "/login", "xsrf_cookies": True, } application = tornado.web.Application([ (r"/", MainHandler), (r"/login", LoginHandler), ], **settings)
若是設置了xsrf_cookies
,則Tornado Web應用程序將爲全部用戶設置_xsrf cookie
,並拒絕全部不包含正確_xsrf
值的POST
,PUT
和DELETE
請求。
若是啓用此設置,則經過POST提交的全部表單都要包含對應字段。你可使用全部模板中提供的特殊UIModulexsrf_form_html()
來執行此操做:
<form action="/new_message" method="post"> {% module xsrf_form_html() %} <input type="text" name="message"/> <input type="submit" value="Post"/> </form>
若是您提交AJAX POST
請求,則還須要修改JavaScript以在每一個請求中包含_xsrf
值。 這是咱們在FriendFeed中用於AJAX POST
請求的jQuery函數,它自動將_xsrf
值添加到全部請求:
function getCookie(name) { var r = document.cookie.match("\\b" + name + "=([^;]*)\\b"); return r ? r[1] : undefined; } jQuery.postJSON = function(url, args, callback) { args._xsrf = getCookie("_xsrf"); $.ajax({url: url, data: $.param(args), dataType: "text", type: "POST", success: function(response) { callback(eval("(" + response + ")")); }}); };
對於PUT
和DELETE
請求(以及不使用表單編碼參數的POST
請求),XSRF令牌也能夠經過名爲X-XSRFToken
的HTTP頭傳遞。 XSRF cookie一般在使用xsrf_form_html
時設置,但在不使用任何常規表單的純Javascript應用程序中,你可能須要手動獲取self.xsrf_token
(只需讀取屬性就足以將cookie設置爲函數反作用)。
若是須要基於每一個處理程序自定義XSRF行爲,則能夠重寫RequestHandler.check_xsrf_cookie()
。 例如,若是你的API的身份驗證不使用cookie,你可能但願經過使用check_xsrf_cookie()
不執行任何操做來禁用XSRF保護。可是,若是同時支持cookie和非基於cookie的身份驗證,則必須在使用cookie對當前請求進行身份驗證時使用XSRF保護。
DNS從新綁定是一種能夠繞過同源策略並容許外部站點訪問專用網絡上的資源的攻擊。 此攻擊包含一個TTL值特別小的DNS名稱,該名稱在返回由攻擊者控制的IP地址和受害者控制的IP地址之間交替(一般是可猜想的私有IP地址,例如127.0.0.1或192.168.1.1)
使用TLS的應用程序不容易受到此攻擊(由於瀏覽器將顯示警告並阻止自動訪問,由於被DNS被修改後訪問的站點與真實目標站點的證書不匹配)。
沒法使用TLS並依賴網絡級訪問控制的應用程序(例如,假設127.0.0.1上的服務器只能由本地計算機訪問)應經過驗證Host HTTP標頭來防止DNS從新綁定。這意味着將限制主機名模式傳遞給HostMatches路由器或Application.add_handlers
的第一個參數:
# BAD: uses a default host pattern of r'.*' app = Application([('/foo', FooHandler)]) # GOOD: only matches localhost or its ip address. app = Application() app.add_handlers(r'(localhost|127\.0\.0\.1)', [('/foo', FooHandler)]) # GOOD: same as previous example using tornado.routing. app = Application([ (HostMatches(r'(localhost|127\.0\.0\.1)'), [('/foo', FooHandler)]), ])
此外,Application
和DefaultHostMatches
路由器的default_host
參數不得在可能易受DNS從新綁定攻擊的應用程序中使用,由於它與通配符主機模式具備相似的效果。
因爲Tornado提供本身的HTTPServer,所以運行和部署它與其餘Python Web框架略有不一樣。 你能夠編寫一個啓動服務器的main()
函數,而不是配置WSGI容器來查找應用程序:
def main(): app = make_app() app.listen(8888) IOLoop.current().start() if __name__ == '__main__': main()
配置操做系統或進程管理器來運行此程序以啓動服務器。請注意,可能須要增長每一個進程的打開文件數(以免「打開太多文件」-Error)。要提升此限制(例如將其設置爲50000),
你可使用資源控制命令,修改/etc/security/limits.conf
或在supervisord配置中設置minfds
。
因爲Python GIL(Global Interpreter Lock 全局解釋器鎖),須要時能夠運行多個Python進程以充分利用多核機器性能。一般,每一個CPU最好運行一個進程。
Tornado包含一個內置的多進程模式,能夠同時啓動多個進程。 這須要對標準主要功能稍做改動:
def main(): app = make_app() server = tornado.httpserver.HTTPServer(app) server.bind(8888) server.start(0) # forks one process per cpu IOLoop.current().start()
儘管有一些限制,但這是啓動多個進程並讓它們共享同一端口的最簡單方法。首先,每一個子進程都有本身的IOLoop,所以在fork以前沒有任何東西觸及(甚至間接)全局IOLoop實例是很重要的。 其次,在此模型中很難進行不停機更新。最後,因爲全部進程共享同一個端口,所以很難對它們進行獨立的監控。
對於更復雜的部署,建議單獨啓動進程,並讓每一個進程偵聽不一樣的端口。supervisord的「進程組」功能是實現此功能的一種好方法。當每一個進程使用不一樣的端口時,一般須要外部負載均衡器(如HAProxy或nginx)向外部訪問者提供統一的地址。
在像nginx這樣的負載均衡器後面運行時,建議將xheaders = True
傳遞給HTTPServer
構造函數。這將告訴Tornado使用像X-Real-IP
這樣的標頭來獲取用戶的IP地址,而不是將全部流量識別爲負載均衡服務器的IP地址。
這是一個準系統nginx配置文件,其結構相似於咱們在FriendFeed上使用的配置文件。它假定nginx和Tornado服務器在同一臺機器上運行,而且四個Tornado服務器在端口8000 - 8003上運行:
user nginx; worker_processes 1; error_log /var/log/nginx/error.log; pid /var/run/nginx.pid; events { worker_connections 1024; use epoll; } http { # Enumerate all the Tornado servers here upstream frontends { server 127.0.0.1:8000; server 127.0.0.1:8001; server 127.0.0.1:8002; server 127.0.0.1:8003; } include /etc/nginx/mime.types; default_type application/octet-stream; access_log /var/log/nginx/access.log; keepalive_timeout 65; proxy_read_timeout 200; sendfile on; tcp_nopush on; tcp_nodelay on; gzip on; gzip_min_length 1000; gzip_proxied any; gzip_types text/plain text/html text/css text/xml application/x-javascript application/xml application/atom+xml text/javascript; # Only retry if there was a communication error, not a timeout # on the Tornado server (to avoid propagating "queries of death" # to all frontends) proxy_next_upstream error; server { listen 80; # Allow file uploads client_max_body_size 50M; location ^~ /static/ { root /var/www; if ($query_string) { expires max; } } location = /favicon.ico { rewrite (.*) /static/favicon.ico; } location = /robots.txt { rewrite (.*) /static/robots.txt; } location / { proxy_pass_header Server; proxy_set_header Host $http_host; proxy_redirect off; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Scheme $scheme; proxy_pass http://frontends; } } }
你能夠經過在應用程序中指定static_path
設置來提供Tornado中的靜態文件:
settings = { "static_path": os.path.join(os.path.dirname(__file__), "static"), "cookie_secret": "__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__", "login_url": "/login", "xsrf_cookies": True, } application = tornado.web.Application([ (r"/", MainHandler), (r"/login", LoginHandler), (r"/(apple-touch-icon\.png)", tornado.web.StaticFileHandler, dict(path=settings['static_path'])), ], **settings)
進行上面的設置後,全部以/static/
開頭的請求都將自動從靜態目錄中查找對應的文件,好比訪問http://localhost:8888/static/foo.png
,返回的就是指定靜態目錄中的foo.png。咱們還會自動從靜態目錄中提供/robots.txt
和/favicon.ico
(即便它們不以/static/
前綴開頭)。
在上面的設置中,咱們已經明確地使用StaticFileHandler
將Tornado配置爲從根路徑便可訪問到apple-touch-icon.png
,儘管實際上該文件存放在靜態文件目錄中。 (爲了告訴StaticFileHandler
所請求的文件名,正則表達式中的匹配組是必需的;請記住,匹配組將做爲方法參數傳遞給處理程序。)你能夠執行相同的操做來訪問站點根目錄的sitemap.xml
。 固然,您也能夠經過在HTML中使用相應的<link />
標記來避免僞造從根目錄訪問apple-touch-icon.png
。
爲了提升性能,瀏覽器一般會主動地緩存靜態資源,所以瀏覽器不會發送沒必要要的If-Modified-Since
或Etag
請求,由於這些可能阻止頁面的渲染。TornadoTornado支持這種開箱即用的靜態內容版本控(static content versioning)。。
要使用此功能,請在模板中使用static_url
方法,而不是直接在HTML中鍵入靜態文件的URL:
<html> <head> <title>FriendFeed - {{ _("Home") }}</title> </head> <body> <div><img src="{{ static_url("images/logo.png") }}"/></div> </body> </html>
static_url()
函數將該相對路徑轉換爲相似於/static/images/logo.png?v=aae54
的URI。v
參數是logo.png中內容的哈希值,它的存在使得Tornado服務器向用戶的瀏覽器發送緩存頭,這將使瀏覽器無限期地緩存內容。
v
參數是由文件內容決定的,若是更新文件並從新啓動服務器,它將開始發送新的v
值,用戶的瀏覽器將自動獲取新文件。 若是文件的內容沒有改變,瀏覽器將繼續使用本地緩存的副本,而無需檢查服務器上的更新,從而顯着提升了渲染性能。
在生產環境中,你可能但願使用像nginx這樣靜態文件性能更好的服務器提供靜態文件。你幾乎能夠配置任何Web服務器以識別static_url()
使用的版本標記,並設置相應的緩存頭。
如下是咱們在FriendFeed中使用的相對應的nginx配置:
location /static/ { root /var/friendfeed/static; if ($query_string) { expires max; } }
若是將debug = True
傳遞給Application
構造函數,則應用程序將以調試/開發模式運行。 在此模式下,將啓用在開發時爲方便起見的若干功能(每一個功能也可做爲單獨的標誌使用;若是二者都指定,則單個標誌優先):
autoreload = True
:應用程序將監視其源文件的更改,並在發生任何更改時從新加載。 這減小了在開發過程當中手動重啓服務器的須要。可是,某些故障(例如導入時的語法錯誤)仍然會以調試模式目前沒法恢復的方式使服務器宕機compiled_template_cache = False
:不緩存模板。static_hash_cache = False
:靜態文件哈希值(由static_url
函數使用)不會被緩存serve_traceback = True
:當未捕獲RequestHandler
中的異常時,將生成包含堆棧路徑的錯誤頁面。 自動重載模式與HTTPServer的多進程模式不兼容。若是使用自動重載模式,則不得向HTTPServer.start
提供除1之外的參數(或調用tornado.process.fork_processes
)。
調試模式的自動重載功能可做爲tornado.autoreload
中的獨立模塊使用。 這二者能夠結合使用,能夠提升程序魯棒性,可以更容易發現語法錯誤:在應用程序中設置autoreload = True
以檢測運行時的更改,並使用python -m tornado.autoreload myserver.py
啓動服務器以捕獲任何語法錯誤或其餘啓動時的錯誤。
從新加載會丟失全部Python解釋器的命令行參數(例如-u),由於它使用sys.executable和sys.argv從新執行Python。 此外,修改這些變量將致使從新加載出錯。
在某些平臺(包括Windows和10.6以前的Mac OSX)上,該過程沒法「就地」更新,所以當檢測到代碼更改時,舊服務器退出並啓動新服務器。 衆所周知,這會混淆一些IDE。
Tornado一般並不使用WSGI容器,而是獨立運行。 可是,在某些環境(例如Google App Engine)中,只容許WSGI,而且應用程序沒法運行本身的服務器。 在這種狀況下,Tornado支持有限的操做模式,該模式不支持異步操做,但容許在僅WSGI環境中使用Tornado功能的子集。 WSGI模式中不容許的功能包括協程,@ asynchronous
裝飾器,AsyncHTTPClient
,auth
模塊和WebSockets
。
你可使用tornado.wsgi.WSGIAdapter
將Tornado應用程序轉換爲WSGI應用程序。 在此示例中,配置WSGI容器以查找應用程序對象:
import tornado.web import tornado.wsgi class MainHandler(tornado.web.RequestHandler): def get(self): self.write("Hello, world") tornado_app = tornado.web.Application([ (r"/", MainHandler), ]) application = tornado.wsgi.WSGIAdapter(tornado_app)