Tornado 網路應用程式的結構¶
一個 Tornado 網路應用程式通常包含一個或多個 RequestHandler
子類別、一個 Application
物件(負責將傳入的請求路由到處理器),以及一個啟動伺服器的 main()
函式。
一個最小的「Hello World」範例如下:
import asyncio
import tornado
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write("Hello, world")
def make_app():
return tornado.web.Application([
(r"/", MainHandler),
])
async def main():
app = make_app()
app.listen(8888)
shutdown_event = asyncio.Event()
await shutdown_event.wait()
if __name__ == "__main__":
asyncio.run(main())
main
協程¶
從 Tornado 6.2 和 Python 3.10 開始,啟動 Tornado 應用程式的建議模式是建立一個 main
協程,並使用 asyncio.run
來執行。(在較舊的版本中,通常會在一個常規函式中進行初始化,然後使用 IOLoop.current().start()
來啟動事件迴圈。然而,從 Python 3.10 開始,此模式會產生棄用警告,並且會在未來版本的 Python 中失效。)
當 main
函式返回時,程式會結束,因此大多數情況下,對於 Web 伺服器,main
應該永遠執行。等待一個 asyncio.Event
,其 set()
方法永遠不會被呼叫,這是一種方便的方法,可以讓一個非同步函式永遠執行。(如果您希望 main
作為正常關機程序的一部分提前退出,您可以呼叫 shutdown_event.set()
使其退出)。
Application
物件¶
Application
物件負責全域組態,包括將請求對應到處理器的路由表。
路由表是一個 URLSpec
物件(或元組)的列表,每個物件(至少)包含一個正規表示式和一個處理器類別。順序很重要;會使用第一個符合的規則。如果正規表示式包含捕獲群組,這些群組會是路徑參數,並且會傳遞到處理器的 HTTP 方法。如果將字典作為 URLSpec
的第三個元素傳遞,則會提供初始化參數,這些參數會傳遞到 RequestHandler.initialize
。最後,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 網路應用程式的大部分工作都在 RequestHandler
的子類別中完成。處理器子類別的主要進入點是依照處理的 HTTP 方法命名的函式:get()
、post()
等。每個處理器可以定義一個或多個這些方法來處理不同的 HTTP 動作。如上所述,呼叫這些方法時會帶有與符合的路由規則的捕獲群組相對應的引數。
在處理器中,呼叫諸如 RequestHandler.render
或 RequestHandler.write
等方法以產生回應。render()
會依名稱載入 Template
,並使用給定的引數來呈現它。write()
用於非樣板的輸出;它接受字串、位元組和字典(字典會被編碼為 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
中取得,此物件會將名稱(HTML <input type="file">
元素的名稱)對應到檔案列表。每個檔案都是 {"filename":..., "content_type":..., "body":...}
格式的字典。只有在檔案是使用表單包裝器(即 multipart/form-data
Content-Type)上傳時,才會出現 files
物件;如果未使用此格式,則原始上傳資料可在 self.request.body
中取得。預設情況下,上傳的檔案會完全緩衝在記憶體中;如果您需要處理太大而無法舒適地保留在記憶體中的檔案,請參閱 stream_request_body
類別裝飾器。
在 demos 目錄中,file_receiver.py 顯示接收檔案上傳的兩種方法。
由於 HTML 表單編碼的特殊性(例如,單數與複數參數的模糊性),Tornado 不會嘗試將表單參數與其他類型的輸入統一處理。特別是,我們不會解析 JSON 請求主體。希望使用 JSON 而非表單編碼的應用程式可以覆寫 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
覆寫 RequestHandler 方法¶
除了 get()
/post()
/等等之外,RequestHandler
中的某些其他方法被設計為在必要時由子類別覆寫。在每個請求上,會依序發生下列呼叫:
每個請求都會建立一個新的
RequestHandler
物件。initialize()
會使用來自Application
設定的初始化引數進行呼叫。initialize
通常應該只儲存傳遞到成員變數中的引數;它可能不會產生任何輸出或呼叫類似send_error
的方法。prepare()
會被呼叫。這在您的所有處理器子類別共用的基底類別中最有用,因為無論使用哪種 HTTP 方法,都會呼叫prepare
。prepare
可以產生輸出;如果它呼叫finish
(或redirect
等等),則處理會在此處停止。會呼叫其中一個 HTTP 方法:
get()
、post()
、put()
等等。如果 URL 正則表達式包含捕獲群組,它們會作為引數傳遞給此方法。當請求完成時,會呼叫
on_finish()
。這通常會在get()
或另一個 HTTP 方法返回後發生。
所有設計為可覆寫的方法都會在 RequestHandler
文件中註明。一些最常被覆寫的方法包括:
write_error
- 輸出 HTML 以用於錯誤頁面。on_connection_close
- 在客戶端斷開連線時呼叫;應用程式可以選擇偵測到這種情況並停止進一步的處理。請注意,無法保證可以立即偵測到關閉的連線。get_current_user
- 請參閱 使用者身份驗證。get_user_locale
- 返回Locale
物件以用於當前使用者。set_default_headers
- 可用於在回應中設定額外的標頭(例如自訂的Server
標頭)。
錯誤處理¶
如果處理器引發異常,Tornado 將呼叫 RequestHandler.write_error
以產生錯誤頁面。tornado.web.HTTPError
可用於產生指定的狀態碼;所有其他異常都會返回 500 狀態。
預設的錯誤頁面在除錯模式下包含堆疊追蹤,否則只會顯示一行錯誤描述(例如「500: 內部伺服器錯誤」)。若要產生自訂錯誤頁面,請覆寫 RequestHandler.write_error
(可能在您的所有處理器共用的基底類別中)。此方法可以使用諸如 write
和 render
之類的方法正常產生輸出。如果錯誤是由異常引起的,則會將 exc_info
三元組作為關鍵字引數傳遞(請注意,此異常不保證是 sys.exc_info
中的當前異常,因此 write_error
必須使用例如 traceback.format_exception
而不是 traceback.format_exc
)。
也可以從常規處理器方法而不是 write_error
產生錯誤頁面,方法是呼叫 set_status
、寫入回應,然後返回。在簡單返回不方便的情況下,可以引發特殊的異常 tornado.web.Finish
以終止處理器,而無需呼叫 write_error
。
對於 404 錯誤,請使用 default_handler_class
Application 設定
。此處理器應覆寫 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 回應碼,這對於例如以對 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/{0}")),
])
與 RequestHandler.redirect
不同,RedirectHandler
預設使用永久性重新導向。這是因為路由表不會在執行時變更,且被認為是永久性的,而處理程式中的重新導向則可能是其他可能變更的邏輯結果。若要使用 RedirectHandler
發送臨時性重新導向,請將 permanent=False
加入 RedirectHandler
的初始化引數中。
非同步處理程式¶
某些處理程式方法(包括 prepare()
和 HTTP 動詞方法 get()
/post()
/等)可以覆寫為協程,以使處理程式成為非同步的。
例如,以下是一個使用協程的簡單處理程式
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")
如需更進階的非同步範例,請參考 chat 範例應用程式,該應用程式使用 長輪詢實作 AJAX 聊天室。長輪詢的使用者可能需要覆寫 on_connection_close()
以在客戶端關閉連線後進行清理(但請參閱該方法的文檔字串以了解注意事項)。