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.renderRequestHandler.write 等方法以產生回應。render() 會依名稱載入 Template,並使用給定的引數來呈現它。write() 用於非樣板的輸出;它接受字串、位元組和字典(字典會被編碼為 JSON)。

RequestHandler 中的許多方法都設計為在子類別中被覆寫並在整個應用程式中使用。通常會定義一個 BaseHandler 類別,它會覆寫諸如 write_errorget_current_user 等方法,然後針對您所有特定的處理器子類化您自己的 BaseHandler,而不是 RequestHandler

處理請求輸入

請求處理器可以使用 self.request 存取代表目前請求的物件。請參閱 HTTPServerRequest 的類別定義,以取得完整的屬性清單。

HTML 表單使用的格式的請求資料會為您解析,並在諸如 get_query_argumentget_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_argumentsget_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 中的某些其他方法被設計為在必要時由子類別覆寫。在每個請求上,會依序發生下列呼叫:

  1. 每個請求都會建立一個新的 RequestHandler 物件。

  2. initialize() 會使用來自 Application 設定的初始化引數進行呼叫。initialize 通常應該只儲存傳遞到成員變數中的引數;它可能不會產生任何輸出或呼叫類似 send_error 的方法。

  3. prepare() 會被呼叫。這在您的所有處理器子類別共用的基底類別中最有用,因為無論使用哪種 HTTP 方法,都會呼叫 prepareprepare 可以產生輸出;如果它呼叫 finish(或 redirect 等等),則處理會在此處停止。

  4. 會呼叫其中一個 HTTP 方法:get()post()put() 等等。如果 URL 正則表達式包含捕獲群組,它們會作為引數傳遞給此方法。

  5. 當請求完成時,會呼叫 on_finish()。這通常會在 get() 或另一個 HTTP 方法返回後發生。

所有設計為可覆寫的方法都會在 RequestHandler 文件中註明。一些最常被覆寫的方法包括:

錯誤處理

如果處理器引發異常,Tornado 將呼叫 RequestHandler.write_error 以產生錯誤頁面。tornado.web.HTTPError 可用於產生指定的狀態碼;所有其他異常都會返回 500 狀態。

預設的錯誤頁面在除錯模式下包含堆疊追蹤,否則只會顯示一行錯誤描述(例如「500: 內部伺服器錯誤」)。若要產生自訂錯誤頁面,請覆寫 RequestHandler.write_error(可能在您的所有處理器共用的基底類別中)。此方法可以使用諸如 writerender 之類的方法正常產生輸出。如果錯誤是由異常引起的,則會將 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 請求後重新導向使用者之類的操作。如果 permanentTrue,則會使用 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() 以在客戶端關閉連線後進行清理(但請參閱該方法的文檔字串以了解注意事項)。