譯者說
Tornado 4.3
于2015年11月6日發布,該版本正式支持Python3.5
的async
/await
關鍵字,并且用舊版本CPython編譯Tornado同樣可以使用這兩個關鍵字,這無疑是一種進步。其次,這是最后一個支持Python2.6
和Python3.2
的版本了,在后續的版本了會移除對它們的兼容。現在網絡上還沒有Tornado4.3
的中文文檔,所以為了讓更多的朋友能接觸并學習到它,我開始了這個翻譯項目,希望感興趣的小伙伴可以一起參與翻譯,項目地址是tornado-zh on Github,翻譯好的文檔在Read the Docs上直接可以看到。歡迎Issues or PR。
Tornado web應用的結構
通常一個Tornado web應用包括一個或者多個RequestHandler
子類,一個可以將收到的請求路由到對應handler的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
的第三個參數被傳遞, 它會作為 初始參數傳遞給 RequestHandler.initialize
. 最后 URLSpec
可能有一個名字(name), 這將允許它被 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()
通過名字加載一個Template
并使用給定的參數渲染它. write()
被用于非模板基礎的輸出; 它接受字符串, 字節, 和字典(字典會被編碼成JSON).
在RequestHandler
中的很多方法的設計是為了在子類中復寫和在整個應用中使用. 常用的方法是定義一個 BaseHandler
類, 復寫一些方法例如RequestHandler.write_error
和RequestHandler.get_current_user
然后子類繼承使用你自己的 BaseHandler
而不是RequestHandler
在你所有具體的處理程序中.
處理輸入請求
處理請求的程序(request handler)可以使用 self.request
訪問代表當前請求的對象. 通過tornado.httputil.HTTPServerRequest
的類定義查看完整的屬性列表.
使用HTML表單格式請求的數據會被解析并且可以在一些方法中使用, 例如RequestHandler.get_query_argument
和RequestHandler.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"))
由于HTLM表單編碼不確定一個標簽的參數是單一值還是一個列表,RequestHandler
有明確的方法來允許應用程序表明是否它期望接收一個列表.對于列表, 使用RequestHandler.get_query_arguments
和RequestHandler.get_body_arguments
而不是它們的單數形式.
通過一個表單上傳的文件可以使用 self.request.files
,它遍歷名字(HTML 標簽 <input type="file">
的name)到一個文件列表.每個文件都是一個字典的形式{"filename":..., "content_type":..., "body":...}
. files
對象是當前唯一的如果文件上傳是通過一個表單包裝(i.e. a multipart/form-data
Content-Type); 如果沒用這種格式,原生上傳的數據可以調用 self.request.body
使用.默認上傳的文件是完全緩存在內存中的; 如果你需要處理占用內存太大的文件可以看看 stream_request_body
類裝飾器.
由于HTML表單編碼格式的怪異 (e.g. 在單數和復數參數的含糊不清), Tornado不會試圖統一表單參數和其他輸入類型的參數. 特別是, 我們不解析JSON請求體.應用程序希望使用JSON代替表單編碼可以復寫 RequestHandler.prepare
來解析它們的請求:
def prepare(self):
if self.request.headers["Content-Type"].startswith("application/json"):
self.json_args = json.loads(self.request.body)
else:
self.json_args = None
復寫RequestHandler的方法
除了 get()
/post()
/等, 在 .RequestHandler
中的某些其他方法被設計成了在必要的時候讓子類重寫. 在每個請求中, 會發生下面的調用序列:
- 在每次請求時生成一個新的
RequestHandler
對象 -
RequestHandler.initialize()
被Application
配置中的初始化參數被調用.initialize
通常應該只保存成員變量傳遞的參數; 它不可能產生任何輸出或者調用方法, 例如RequestHandler.send_error
. -
RequestHandler.prepare()
被調用. 這在你所有處理子類共享的基類中是最有用的, 無論是使用哪種HTTP方法,prepare
都會被調用.prepare
可能會產生輸出; 如果它調用RequestHandler.finish
(或者redirect
, 等), 處理會在這里結束. - 其中一種HTTP方法被調用:
get()
,post()
,put()
,等. 如果URL的正則表達式包含捕獲組, 它們會被作為參數傳遞給這個方法. - 當請求結束,
RequestHandler.on_finish()
方法被調用. 對于同步處理程序會在get()
(等)后立即返回; 對于異步處理程序,會在調用RequestHandler.finish()
后返回.
所有這樣設計被用來復寫的方法被記錄在了RequestHandler
的文檔中.其中最常用的一些被復寫的方法包括:
-
RequestHandler.write_error
- 輸出對錯誤頁面使用的HTML. -
RequestHandler.on_connection_close
- 當客戶端斷開時被調用;應用程序可以檢測這種情況,并中斷后續處理. 注意這不能保證一個關閉的連接及時被發現. -
RequestHandler.get_current_user
- 參考user-authentication
-
RequestHandler.get_user_locale
- 返回.Locale
對象給當前
用戶使用 -
RequestHandler.set_default_headers
- 可以被用來設置額外的響應
頭(例如自定義的Server
頭)
錯誤處理
如果一個處理程序拋出一個異常, Tornado會調用RequestHandler.write_error
來生成一個錯誤頁.tornado.web.HTTPError
可以被用來生成一個指定的狀態碼; 所有其他的異常都會返回一個500狀態.
默認的錯誤頁面包含一個debug模式下的調用棧和另外一行錯誤描述(e.g. "500: Internal Server Error"). 為了創建自定義的錯誤頁面, 復寫RequestHandler.write_error
(可能在一個所有處理程序共享的一個基類里面).這個方法可能產生輸出通常通過一些方法, 例如 RequestHandler.write
和RequestHandler.render
. 如果錯誤是由異常引起的, 一個 exc_info
將作為一個關鍵字參數傳遞(注意這個異常不能保證是 sys.exc_info
當前的異常, 所以 write_error
必須使用 e.g. traceback.format_exception
代替traceback.format_exc
).
也可以在常規的處理方法中調用 RequestHandler.set_status
代替write_error
返回一個(自定義)響應來生成一個錯誤頁面. 特殊的例外tornado.web.Finish
在直接返回不方便的情況下能夠在不調用 write_error
前結束處理程序.
對于404錯誤, 使用 default_handler_class
Application setting
. 這個處理程序會復寫RequestHandler.prepare
而不是一個更具體的方法, 例如 get()
所以它可以在任何HTTP方法下工作. 它應該會產生如上所說的錯誤頁面: 要么raise一個 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響應, 更適合e.g. 在SEO友好的方法中把一個頁面重定向到一個權威的URL.
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/\1")),
])
不像 RequestHandler.redirect
, RedirectHandler
默認使用永久重定向.這是因為路由表在運行時不會改變, 而且被認為是永久的.當在處理程序中發現重定向的時候, 可能是其他可能改變的邏輯的結果.用 .RedirectHandler
發送臨時重定向, 需要添加 permanent=False
到.RedirectHandler
的初始化參數.
異步處理
Tornado默認會同步處理: 當 get()
/post()
方法返回, 請求被認為結束并且返回響應. 因為當一個處理程序正在運行的時候其他所有請求都被阻塞,任何需要長時間運行的處理都應該是異步的, 這樣它就可以在非阻塞的方式中調用它的慢操作了. 這個話題更詳細的內容包含在async
中; 這部分是關于在 RequestHandler
子類中的異步技術的細節.
使用 coroutine
裝飾器是做異步最簡單的方式. 這允許你使用 yield
關鍵字執行非阻塞I/O, 并且直到協程返回才發送響應. 查看 coroutines
了解更多細節.
在某些情況下, 協程不如回調為主的風格方便, 在這種情況下tornado.web.asynchronous
裝飾器可以用來代替. 當使用這個裝飾器的時候,響應不會自動發送; 而請求將一直保持開放直到callback調用RequestHandler.finish
. 這需要應用程序確保這個方法被調用或者其他用戶的瀏覽器簡單的掛起.
這里是一個使用Tornado's 內置的 AsyncHTTPClient
調用FriendFeed API的例
子:
class MainHandler(tornado.web.RequestHandler):
@tornado.web.asynchronous
def get(self):
http = tornado.httpclient.AsyncHTTPClient()
http.fetch("http://friendfeed-api.com/v2/feed/bret",
callback=self.on_response)
def on_response(self, response):
if response.error: raise tornado.web.HTTPError(500)
json = tornado.escape.json_decode(response.body)
self.write("Fetched " + str(len(json["entries"])) + " entries "
"from the FriendFeed API")
self.finish()
當 get()
返回, 請求還沒有完成. 當HTTP客戶端最終調用on_response()
, 這個請求仍然是開放的, 響應最終刷到客戶端通過調用 self.finish()
.
為了方便對比, 這里有一個使用協程的相同的例子:
class MainHandler(tornado.web.RequestHandler):
@tornado.gen.coroutine
def get(self):
http = tornado.httpclient.AsyncHTTPClient()
response = yield 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")
更多高級異步的示例, 請看chat example application, 實現了一個使用 長輪詢(long polling)的AJAX聊天室.使用長輪詢的用戶可能想要覆蓋 on_connection_close()
來在客戶端關閉連接之后進行清理(注意看方法的文檔來查看警告).