Tornado入門

Tornado概述

Python的Web框架種類繁多(比Python語言的關鍵字還要多),但在眾多優秀的Web框架中,Tornado框架最適合用來開發需要處理長連接和應對高并發的Web應用。Tornado框架在設計之初就考慮到性能問題,通過對非阻塞I/O和epoll(Linux 2.5.44內核引入的一種多路I/O復用方式,旨在實現高性能網絡服務,在BSD和macOS中是kqueue)的運用,Tornado可以處理大量的并發連接,更輕松的應對C10K(萬級并發)問題,是非常理想的實時通信Web框架。

擴展:基于線程的Web服務器產品(如:Apache)會維護一個線程池來處理用戶請求,當用戶請求到達時就為該請求分配一個線程,如果線程池中沒有空閑線程了,那么可以通過創建新的線程來應付新的請求,但前提是系統尚有空閑的內存空間,顯然這種方式很容易將服務器的空閑內存耗盡(大多數Linux發行版本中,默認的線程棧大小為8M)。想象一下,如果我們要開發一個社交類應用,這類應用中,通常需要顯示實時更新的消息、對象狀態的變化和各種類型的通知,那也就意味著客戶端需要保持請求連接來接收服務器的各種響應,在這種情況下,服務器上的工作線程很容易被耗盡,這也就意味著新的請求很有可能無法得到響應。

Tornado框架源于FriendFeed網站,在FriendFeed網站被Facebook收購之后得以開源,正式發布的日期是2009年9月10日。Tornado能讓你能夠快速開發高速的Web應用,如果你想編寫一個可擴展的社交應用、實時分析引擎,或RESTful API,那么Tornado框架就是很好的選擇。Tornado其實不僅僅是一個Web開發的框架,它還是一個高性能的事件驅動網絡訪問引擎,內置了高性能的HTTP服務器和客戶端(支持同步和異步請求),同時還對WebSocket提供了完美的支持。

了解和學習Tornado最好的資料就是它的官方文檔,在tornadoweb.org上面有很多不錯的例子,你也可以在Github上找到Tornado的源代碼和歷史版本。

5分鐘上手Tornado

  1. 創建并激活虛擬環境。

    mkdir hello-tornado
    cd hello-tornado
    python3 -m venv venv
    source venv/bin/activate
    
  2. 安裝Tornado。

    pip install tornado
    
  3. 編寫Web應用。

    """
    example01.py
    """
    import tornado.ioloop
    import tornado.web
    
    
    class MainHandler(tornado.web.RequestHandler):
    
        def get(self):
            self.write('<h1>Hello, world!</h1>')
    
    
    def main():
        app = tornado.web.Application(handlers=[(r'/', MainHandler), ])
        app.listen(8888)
        tornado.ioloop.IOLoop.current().start()
    
    
    if __name__ == '__main__':
        main()
    
  4. 運行并訪問應用。

    python example01.py
    

    [外鏈圖片轉存失敗(img-NMVQ8CvL-1563000574459)(./res/run-hello-world-app.png)]

在上面的例子中,代碼example01.py通過定義一個繼承自RequestHandler的類(MainHandler)來處理用戶請求,當請求到達時,Tornado會實例化這個類(創建MainHandler對象),并調用與HTTP請求方法(GET、POST等)對應的方法,顯然上面的MainHandler只能處理GET請求,在收到GET請求時,它會將一段HTML的內容寫入到HTTP響應中。main函數的第1行代碼創建了Tornado框架中Application類的實例,它代表了我們的Web應用,而創建該實例最為重要的參數就是handlers,該參數告知Application對象,當收到一個請求時應該通過哪個類的對象來處理這個請求。在上面的例子中,當通過HTTP的GET請求訪問站點根路徑時,就會調用MainHandlerget方法。 main函數的第2行代碼通過Application對象的listen方法指定了監聽HTTP請求的端口。main函數的第3行代碼用于獲取Tornado框架的IOLoop實例并啟動它,該實例代表一個條件觸發的I/O循環,用于持續的接收來自于客戶端的請求。

擴展:在Python 3中,IOLoop實例的本質就是asyncio的事件循環,該事件循環在非Windows系統中就是SelectorEventLoop對象,它基于selectors模塊(高級I/O復用模塊),會使用當前操作系統最高效的I/O復用選擇器,例如在Linux環境下它使用EpollSelector,而在macOS和BSD環境下它使用的是KqueueSelector;在Python 2中,IOLoop直接使用select模塊(低級I/O復用模塊)的epollkqueue函數,如果這兩種方式都不可用,則調用select函數實現多路I/O復用。當然,如果要支持高并發,你的系統最好能夠支持epoll或者kqueue這兩種多路I/O復用方式中的一種。

如果希望通過命令行參數來指定Web應用的監聽端口,可以對上面的代碼稍作修改。

"""
example01.py
"""
import tornado.ioloop
import tornado.web

from tornado.options import define, options, parse_command_line


# 定義默認端口
define('port', default=8000, type=int)


class MainHandler(tornado.web.RequestHandler):

    def get(self):
        self.write('<h1>Hello, world!</h1>')


def main():
    # python example01.py --port=8000
    parse_command_line()
    app = tornado.web.Application(handlers=[(r'/', MainHandler), ])
    app.listen(options.port)
    tornado.ioloop.IOLoop.current().start()


if __name__ == '__main__':
    main()

在啟動Web應用時,如果沒有指定端口,將使用define函數中設置的默認端口8000,如果要指定端口,可以使用下面的方式來啟動Web應用。

python example01.py --port=8000

路由解析

上面我們曾經提到過創建Application實例時需要指定handlers參數,這個參數非常重要,它應該是一個元組的列表,元組中的第一個元素是正則表達式,它用于匹配用戶請求的資源路徑;第二個元素是RequestHandler的子類。在剛才的例子中,我們只在handlers列表中放置了一個元組,事實上我們可以放置多個元組來匹配不同的請求(資源路徑),而且可以使用正則表達式的捕獲組來獲取匹配的內容并將其作為參數傳入到getpost這些方法中。

"""
example02.py
"""
import os
import random

import tornado.ioloop
import tornado.web

from tornado.options import define, options, parse_command_line


# 定義默認端口
define('port', default=8000, type=int)


class SayingHandler(tornado.web.RequestHandler):
    """自定義請求處理器"""

    def get(self):
        sayings = [
            '世上沒有絕望的處境,只有對處境絕望的人',
            '人生的道路在態度的岔口一分為二,從此通向成功或失敗',
            '所謂措手不及,不是說沒有時間準備,而是有時間的時候沒有準備',
            '那些你認為不靠譜的人生里,充滿你沒有勇氣做的事',
            '在自己喜歡的時間里,按照自己喜歡的方式,去做自己喜歡做的事,這便是自由',
            '有些人不屬于自己,但是遇見了也彌足珍貴'
        ]
        # 渲染index.html模板頁
        self.render('index.html', message=random.choice(sayings))


class WeatherHandler(tornado.web.RequestHandler):
    """自定義請求處理器"""

    def get(self, city):
        # Tornado框架會自動處理百分號編碼的問題
        weathers = {
            '北京': {'temperature': '-4~4', 'pollution': '195 中度污染'},
            '成都': {'temperature': '3~9', 'pollution': '53 良'},
            '深圳': {'temperature': '20~25', 'pollution': '25 優'},
            '廣州': {'temperature': '18~23', 'pollution': '56 良'},
            '上海': {'temperature': '6~8', 'pollution': '65 良'}
        }
        if city in weathers:
            self.render('weather.html', city=city, weather=weathers[city])
        else:
            self.render('index.html', message=f'沒有{city}的天氣信息')


class ErrorHandler(tornado.web.RequestHandler):
    """自定義請求處理器"""

    def get(self):
        # 重定向到指定的路徑
        self.redirect('/saying')


def main():
    """主函數"""
    parse_command_line()
    app = tornado.web.Application(
        # handlers是按列表中的順序依次進行匹配的
        handlers=[
            (r'/saying/?', SayingHandler),
            (r'/weather/([^/]{2,})/?', WeatherHandler),
            (r'/.+', ErrorHandler),
        ],
        # 通過template_path參數設置模板頁的路徑
        template_path=os.path.join(os.path.dirname(__file__), 'templates')
    )
    app.listen(options.port)
    tornado.ioloop.IOLoop.current().start()


if __name__ == '__main__':
    main()

模板頁index.html。

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Tornado基礎</title>
</head>
<body>
    <h1>{{message}}</h1>
</body>
</html>

模板頁weather.html。

<!-- weather.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Tornado基礎</title>
</head>
<body>
    <h1>{{city}}</h1>
    <hr>
    <h2>溫度:{{weather['temperature']}}攝氏度</h2>
    <h2>污染指數:{{weather['pollution']}}</h2>
</body>
</html>

Tornado的模板語法與其他的Web框架中使用的模板語法并沒有什么實質性的區別,而且目前的Web應用開發更倡導使用前端渲染的方式來減輕服務器的負擔,所以這里我們并不對模板語法和后端渲染進行深入的講解。

請求處理器

通過上面的代碼可以看出,RequestHandler是處理用戶請求的核心類,通過重寫getpostputdelete等方法可以處理不同類型的HTTP請求,除了這些方法之外,RequestHandler還實現了很多重要的方法,下面是部分方法的列表:

  1. get_argument / get_arguments / get_body_argument / get_body_arguments / get_query_arugment / get_query_arguments:獲取請求參數。
  2. set_status / send_error / set_header / add_header / clear_header / clear:操作狀態碼和響應頭。
  3. write / flush / finish / write_error:和輸出相關的方法。
  4. render / render_string:渲染模板。
  5. redirect:請求重定向。
  6. get_cookie / set_cookie / get_secure_cookie / set_secure_cookie / create_signed_value / clear_cookie / clear_all_cookies:操作Cookie。

我們用上面講到的這些方法來完成下面的需求,訪問頁面時,如果Cookie中沒有讀取到用戶信息則要求用戶填寫個人信息,如果從Cookie中讀取到用戶信息則直接顯示用戶信息。

"""
example03.py
"""
import os
import re

import tornado.ioloop
import tornado.web

from tornado.options import define, options, parse_command_line


# 定義默認端口
define('port', default=8000, type=int)

users = {}


class User(object):
    """用戶"""

    def __init__(self, nickname, gender, birthday):
        self.nickname = nickname
        self.gender = gender
        self.birthday = birthday


class MainHandler(tornado.web.RequestHandler):
    """自定義請求處理器"""

    def get(self):
        # 從Cookie中讀取用戶昵稱
        nickname = self.get_cookie('nickname')
        if nickname in users:
            self.render('userinfo.html', user=users[nickname])
        else:
            self.render('userform.html', hint='請填寫個人信息')


class UserHandler(tornado.web.RequestHandler):
    """自定義請求處理器"""

    def post(self):
        # 從表單參數中讀取用戶昵稱、性別和生日信息
        nickname = self.get_body_argument('nickname').strip()
        gender = self.get_body_argument('gender')
        birthday = self.get_body_argument('birthday')
        # 檢查用戶昵稱是否有效
        if not re.fullmatch(r'\w{6,20}', nickname):
            self.render('userform.html', hint='請輸入有效的昵稱')
        elif nickname in users:
            self.render('userform.html', hint='昵稱已經被使用過')
        else:
            users[nickname] = User(nickname, gender, birthday)
            # 將用戶昵稱寫入Cookie并設置有效期為7天
            self.set_cookie('nickname', nickname, expires_days=7)
            self.render('userinfo.html', user=users[nickname])


def main():
    """主函數"""
    parse_command_line()
    app = tornado.web.Application(
        handlers=[
            (r'/', MainHandler), (r'/register', UserHandler)
        ],
        template_path=os.path.join(os.path.dirname(__file__), 'templates')
    )
    app.listen(options.port)
    tornado.ioloop.IOLoop.current().start()


if __name__ == '__main__':
    main()

模板頁userform.html。

<!-- userform.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Tornado基礎</title>
    <style>
        .em { color: red; }
    </style>
</head>
<body>
    <h1>填寫用戶信息</h1>
    <hr>
    <p class="em">{{hint}}</p>
    <form action="/register" method="post">
        <p>
            <label>昵稱:</label>
            <input type="text" name="nickname">
            (字母數字下劃線,6-20個字符)
        </p>
        <p>
            <label>性別:</label>
            <input type="radio" name="gender" value="男" checked>男
            <input type="radio" name="gender" value="女">女
        </p>
        <p>
            <label>生日:</label>
            <input type="date" name="birthday" value="1990-01-01">
        </p>
        <p>
            <input type="submit" value="確定">
        </p>
    </form>
</body>
</html>

模板頁userinfo.html。

<!-- userinfo.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Tornado基礎</title>
</head>
<body>
    <h1>用戶信息</h1>
    <hr>
    <h2>昵稱:{{user.nickname}}</h2>
    <h2>性別:{{user.gender}}</h2>
    <h2>出生日期:{{user.birthday}}</h2>
</body>
</html>
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容