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
-
創建并激活虛擬環境。
mkdir hello-tornado cd hello-tornado python3 -m venv venv source venv/bin/activate
-
安裝Tornado。
pip install tornado
-
編寫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()
-
運行并訪問應用。
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請求訪問站點根路徑時,就會調用MainHandler
的get
方法。 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復用模塊)的epoll
或kqueue
函數,如果這兩種方式都不可用,則調用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
列表中放置了一個元組,事實上我們可以放置多個元組來匹配不同的請求(資源路徑),而且可以使用正則表達式的捕獲組來獲取匹配的內容并將其作為參數傳入到get
、post
這些方法中。
"""
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
是處理用戶請求的核心類,通過重寫get
、post
、put
、delete
等方法可以處理不同類型的HTTP請求,除了這些方法之外,RequestHandler
還實現了很多重要的方法,下面是部分方法的列表:
-
get_argument
/get_arguments
/get_body_argument
/get_body_arguments
/get_query_arugment
/get_query_arguments
:獲取請求參數。 -
set_status
/send_error
/set_header
/add_header
/clear_header
/clear
:操作狀態碼和響應頭。 -
write
/flush
/finish
/write_error
:和輸出相關的方法。 -
render
/render_string
:渲染模板。 -
redirect
:請求重定向。 -
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>