Tornado 是由 Facebook 開(kāi)源的一個(gè)服務(wù)器“套裝”种呐,適合于做 python 的 web 或者使用其本身提供的可擴(kuò)展的功能,完成了不完整的 wsgi 協(xié)議弃甥,可用于做快速的 web 開(kāi)發(fā)爽室,封裝了 epoll 性能較好。文章主要以分析 tornado 的網(wǎng)絡(luò)部分即異步事件處理與上層的 IOstream 類(lèi)提供的異步IO淆攻,其他的模塊如 web 的 tornado.web 以后慢慢留作分析阔墩。
下面開(kāi)始我們的Tornado之旅,看源代碼之前必定需要有一份源碼了瓶珊,大家可以去官網(wǎng)下載一份啸箫。這里分析的是4.4.2。
視頻分享鏈接:
https://study.163.com/course/introduction/1209485881.htm?share=2&shareId=400000000535031
https://study.163.com/course/introduction/1209407824.htm?share=2&shareId=400000000535031
https://study.163.com/course/introduction/1209401891.htm?share=2&shareId=400000000535031
Tornado 的源碼組織如下
?
tornado網(wǎng)絡(luò)部分最核心的兩個(gè)模塊就是ioloop.py與iostream.py伞芹,我們主要分析的就是這兩個(gè)部分忘苛。
ioloop.py 主要的是將底層的epoll或者說(shuō)是其他的IO多路復(fù)用封裝作異步事件來(lái)處理。
iostream.py主要是對(duì)于下層的異步事件的進(jìn)一步封裝唱较,為其封裝了更上一層的buffer(IO)事件扎唾。
我們先來(lái)看看ioloop(文檔地址:http://www.tornadoweb.org/en/stable/ioloop.html)
We use epoll (Linux) or kqueue (BSD and Mac OS X) if they are available, or else we fall back on select(). If you are implementing a system that needs to handle thousands of simultaneous connections, you should use a system that supports either epoll or kqueue.
Example usage for a simple TCP server:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import errno
import functools
import ioloop
import socket
def connection_ready(sock, fd, events):
????while True:
????????try:
????????????connection, address = sock.accept()
????????except socket.error, e:
????????????if e.args[0] not in (errno.EWOULDBLOCK, errno.EAGAIN):
????????????????raise
????????????return
????????connection.setblocking(0)
????????handle_connection(connection, address)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.setblocking(0)
sock.bind(("", port))
sock.listen(128)
# 創(chuàng)建一個(gè)ioloop 實(shí)例
io_loop = ioloop.IOLoop.instance()
# connection_ready 的第一個(gè)參數(shù)為 sock,即 socket 的返回值
callback = functools.partial(connection_ready, sock)
# 注冊(cè)函數(shù),第一個(gè)參數(shù)是將 sock 轉(zhuǎn)換為標(biāo)準(zhǔn)的描述符南缓,第二個(gè)為回調(diào)函數(shù)稽屏,第三個(gè)是事件類(lèi)型
io_loop.add_handler(sock.fileno(), callback, io_loop.READ)
io_loop.start()
可以看到在注釋前都是使用了傳統(tǒng)的創(chuàng)建服務(wù)器的方式,不用多介紹西乖,注意就是把套接口設(shè)置為非阻塞方式狐榔。
創(chuàng)建ioloop實(shí)例,這里是使用了ioloop.IOLoop中的 instance()靜態(tài)方法获雕,以 @classmethod 方式包裝薄腻。
在后面的add_handler中,程序?yàn)槲覀兊谋O(jiān)聽(tīng)套接口注冊(cè)了一個(gè)回調(diào)函數(shù)和一個(gè)事件類(lèi)型届案。工作方式是這樣庵楷,在注冊(cè)了相應(yīng)的事件類(lèi)型和回調(diào)函數(shù)以后,程序開(kāi)始啟動(dòng),如果在相應(yīng)的套接口上有事件發(fā)生(注冊(cè)的事件類(lèi)型)那么調(diào)用相應(yīng)的回調(diào)函數(shù)尽纽。
當(dāng)監(jiān)聽(tīng)套接口有可讀事件發(fā)生咐蚯,意味著來(lái)了一個(gè)新連接,在回調(diào)函數(shù)中就可以對(duì)這個(gè)套接口accept弄贿,并調(diào)用相應(yīng)的處理函數(shù)春锋,其實(shí)應(yīng)該是處理函數(shù)也設(shè)置為異步的,將相應(yīng)的連接套接口也加入到事件循環(huán)并注冊(cè)相應(yīng)的回調(diào)函數(shù)差凹,只是這里沒(méi)有展示出來(lái)期奔。
在使用非阻塞方式的accept時(shí)候常常返回EAGAIN,EWOULDBLOCK 錯(cuò)誤,這里采取的方式是放棄這個(gè)連接危尿。
設(shè)計(jì)模型:
在深入到模塊進(jìn)行分析之前呐萌,首先來(lái)看看Tornado的設(shè)計(jì)模型。
?
從上面的圖可以看出谊娇,Tornado不僅僅是一個(gè)WEB框架肺孤,它還完整地實(shí)現(xiàn)了HTTP服務(wù)器和客戶(hù)端,在此基礎(chǔ)上提供WEB服務(wù)济欢。它可以分為四層:
最底層的EVENT層處理IO事件渠旁;
TCP層實(shí)現(xiàn)了TCP服務(wù)器,負(fù)責(zé)數(shù)據(jù)傳輸船逮;
HTTP/HTTPS層基于HTTP協(xié)議實(shí)現(xiàn)了HTTP服務(wù)器和客戶(hù)端;
最上層為WEB框架粤铭,包含了處理器挖胃、模板、數(shù)據(jù)庫(kù)連接梆惯、認(rèn)證酱鸭、本地化等等WEB框架需要具備的功能。
理解Tornado的核心框架之后垛吗,就能便于我們后續(xù)的理解凹髓。
Tornado核心文件解讀
為了方便,約定$root指帶tornado的根目錄怯屉∥狄ǎ總的來(lái)說(shuō),要用Tornado完成一個(gè)網(wǎng)站的構(gòu)建锨络,其實(shí)主要需要以下幾個(gè)文件:
$root/tornado/web.py
$root/tornado/httpserver.py
$root/tornado/tcpserver.py
$root/tornado/ioloop.py
$root/tornado/iostream.py
$root/tornado/platfrom/epoll.py
$root/app.py
另外可能還需要一些功能庫(kù)的支持而需要引入的文件就不列舉了赌躺,比如util和httputil之類(lèi)的。來(lái)看看每個(gè)文件的作用羡儿。
app.py 是自己寫(xiě)的礼患,內(nèi)容就如 tornado 的 readme 文檔里給的示例一樣,定義路由規(guī)則和 handler,然后創(chuàng)建 application缅叠,發(fā)起 server 監(jiān)聽(tīng)悄泥,服務(wù)器就算跑起來(lái)了。
緊接著就是 web.py肤粱。其中定義了 Application 和 RequestHandler 類(lèi)弹囚,在 app.py 里直接就用到了。Application 是個(gè)單例狼犯,總攬全局路由余寥,創(chuàng)建服務(wù)器負(fù)責(zé)監(jiān)聽(tīng),并把服務(wù)器傳回來(lái)的請(qǐng)求進(jìn)行轉(zhuǎn)發(fā)(__call__)悯森。RequestHandler 是個(gè)功能很豐富的類(lèi)宋舷,基本上 web 開(kāi)發(fā)需要的它都具備了,比如redirect瓢姻,flush祝蝠,close,header幻碱,cookie绎狭,render(模板),xsrf褥傍,etag等等
從 web 跟蹤到 httpserver.py 和 tcpserver.py儡嘶。這兩個(gè)文件主要是實(shí)現(xiàn) http 協(xié)議,解析 header 和 body恍风, 生成request蹦狂,回調(diào)給 appliaction,一個(gè)經(jīng)典意義上的 http 服務(wù)器(written in python)朋贬。眾所周知凯楔,這是個(gè)很考究性能的一塊(IO),所以它和其它很多塊都連接到了一起锦募,比如 IOLoop摆屯,IOStream,HTTPConnection 等等糠亩。這里 HTTPConnection 是實(shí)現(xiàn)了 http 協(xié)議的部分虐骑,它關(guān)注 Connection 嘛,這是 http 才有的赎线。至于監(jiān)聽(tīng)端口富弦,IO事件,讀寫(xiě)緩沖區(qū)氛驮,建立連接之類(lèi)都是在它的下層--tcp里需要考慮的腕柜,所以,tcpserver 才是和它們打交道的地方,到時(shí)候分析起來(lái)估計(jì)很麻煩
先說(shuō)這個(gè)IOStream盏缤。顧名思義砰蠢,就是負(fù)責(zé)IO的。說(shuō)到IO唉铜,就得提緩沖區(qū)和IO事件台舱。緩沖區(qū)的處理都在它自個(gè)兒類(lèi)里,IO事件的異步處理就要靠 IOLoop 了潭流。
然后是IOLoop竞惋。如果你用過(guò) select/poll/epoll/libevent 的話(huà),對(duì)它的處理模型應(yīng)該相當(dāng)熟悉灰嫉。簡(jiǎn)言之拆宛,就是一個(gè)大大的循環(huán)绒北,循環(huán)里等待事件芦鳍,然后處理事件假抄。這是開(kāi)發(fā)高性能服務(wù)器的常見(jiàn)模型忿族,tornado 的異步能力就是在這個(gè)類(lèi)里得到保證的
最后是 epoll.py。其實(shí)這個(gè)文件也沒(méi)干啥箭启,就是聲明了一下服務(wù)器使用 epoll秉剑。選擇 select/poll/epoll/kqueue 其中的一種作為事件分發(fā)模型澜汤,是在 tornado 里自動(dòng)根據(jù)操作系統(tǒng)的類(lèi)型而做的選擇炎滞,所以這幾種接口是一樣的(當(dāng)然效率不一樣)敢艰,出于簡(jiǎn)化,直接就epoll吧^_^
PS册赛。如果你是一個(gè)細(xì)節(jié)控钠导,可能會(huì)注意到 tornado 里的回調(diào) callback 函數(shù)都不是直接使用的,而是使用 stack_context.wrap 進(jìn)行了封裝击奶。但據(jù)我觀察,封裝前后沒(méi)多大差別(指邏輯流程)责掏,函數(shù)的參數(shù)也不變柜砾。但根據(jù)它代碼里的注釋?zhuān)@個(gè)封裝還是相當(dāng)有用的:
use this whenever saving a callback to be executed later in a different execution context (either in a different thread or asynchronously in the same thread).
所以,我猜换衬,是使用了獨(dú)有的context來(lái)保證在不同環(huán)境也能很好的執(zhí)行痰驱。猜測(cè)而已,我也沒(méi)細(xì)想瞳浦,以后有時(shí)間再看好担映,最有用一個(gè)簡(jiǎn)單的流程來(lái)做結(jié)。d?
本小節(jié)介紹TornadoHTTP服務(wù)器的基本流程叫潦,分別分析httpserver, ioloop, iostream模塊的代碼來(lái)剖析Tornado底層I/O的內(nèi)部實(shí)現(xiàn)蝇完。
httpserver.py中給出了一個(gè)簡(jiǎn)單的http服務(wù)器的demo,代碼如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
from tornado import httpserver
from tornado import ioloop
def handle_request(request):
???message = "You requested %s\n" % request.uri
???request.write("HTTP/1.1 200 OK\r\nContent-Length: %d\r\n\r\n%s" % (
?????????????????len(message), message))
???request.finish()
http_server = httpserver.HTTPServer(handle_request)
http_server.bind(8888)
http_server.start()
ioloop.IOLoop.instance().start()
?該http服務(wù)器主要使用到IOLoop, IOStream, HTTPServer, HTTPConnection幾大模塊,分別在代碼ioloop.py, iostream.py, httpserver.py中實(shí)現(xiàn)短蜕。工作的流程如下圖所示
?
服務(wù)器的工作流程:首先按照socket->bind->listen順序創(chuàng)建listen socket監(jiān)聽(tīng)客戶(hù)端氢架,并將每個(gè)listen socket的fd注冊(cè)到IOLoop的單例實(shí)例中;當(dāng)listen socket可讀時(shí)回調(diào)_handle_events處理客戶(hù)端請(qǐng)求朋魔;在與客戶(hù)端通信的過(guò)程中使用IOStream封裝了讀岖研、寫(xiě)緩沖區(qū),實(shí)現(xiàn)與客戶(hù)端的異步讀寫(xiě)警检。
HTTPServer分析
HTTPServer在httpserver.py中實(shí)現(xiàn)孙援,繼承自TCPServer(netutil.py中實(shí)現(xiàn)),是一個(gè)無(wú)阻塞扇雕、單線(xiàn)程HTTP服務(wù)器拓售。支持HTTP/1.1協(xié)議keep-alive連接,但不支持chunked encoding洼裤。服務(wù)器支持'X-Real-IP'和'X-Scheme'頭以及SSL傳輸邻辉,支持多進(jìn)程為prefork模式實(shí)現(xiàn)。在源代碼的注釋中對(duì)以上描述比較詳細(xì)的說(shuō)明腮鞍,這里就不再細(xì)說(shuō)值骇。
HTTPServer和TCPServer的類(lèi)結(jié)構(gòu):
1
2
3
4
5
6
7
8
9
class TCPServer(object):
????def __init__(self, io_loop=None, ssl_options=None):
????def listen(self, port, address=""):
????def add_sockets(self, sockets):
????def bind(self, port, address=None, family=socket.AF_UNSPEC, backlog=128):
????def start(self, num_processes=1):
????def stop(self):
????def handle_stream(self, stream, address):
????def _handle_connection(self, connection, address):
?文章開(kāi)始部分創(chuàng)建HTTPServer的過(guò)程:首先需要定義處理request的回調(diào)函數(shù),在tornado中通常使用tornado.web.Application封裝移国。然后構(gòu)造HTTPServer實(shí)例吱瘩,注冊(cè)回調(diào)函數(shù)。接下來(lái)監(jiān)聽(tīng)端口迹缀,啟動(dòng)服務(wù)器使碾。最后啟動(dòng)IOLoop。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
def listen(self, port, address=""):
????sockets = bind_sockets(port, address=address)
????self.add_sockets(sockets)
def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=128):
????# 省略sockets創(chuàng)建祝懂,address票摇,flags處理部分代碼
????for res in set(socket.getaddrinfo(address, port, family, socket.SOCK_STREAM,
??????????????????????????????????0, flags)):
????????af, socktype, proto, canonname, sockaddr = res
????????# 創(chuàng)建socket
????????sock = socket.socket(af, socktype, proto)
????????# 設(shè)置socket屬性,代碼省略
????????sock.bind(sockaddr)
????????sock.listen(backlog)
????????sockets.append(sock)
????return sockets
def add_sockets(self, sockets):
????if self.io_loop is None:
????????self.io_loop = IOLoop.instance()
????for sock in sockets:
????????self._sockets[sock.fileno()] = sock
????????add_accept_handler(sock, self._handle_connection,
???????????????????????????io_loop=self.io_loop)
def add_accept_handler(sock, callback, io_loop=None):
????if io_loop is None:
????????io_loop = IOLoop.instance()
????def accept_handler(fd, events):
????????while True:
????????????try:
????????????????connection, address = sock.accept()
????????????except socket.error, e:
????????????????if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
????????????????????return
????????????????raise
????????????# 當(dāng)有連接被accepted時(shí)callback會(huì)被調(diào)用
????????????callback(connection, address)
????io_loop.add_handler(sock.fileno(), accept_handler, IOLoop.READ)
def _handle_connection(self, connection, address):
????# SSL部分省略
????try:
????????stream = IOStream(connection, io_loop=self.io_loop)
????????self.handle_stream(stream, address)
????except Exception:
????????logging.error("Error in connection callback", exc_info=True)
這里分析HTTPServer通過(guò)listen函數(shù)啟動(dòng)監(jiān)聽(tīng)砚蓬,這種方法是單進(jìn)程模式矢门。另外可以通過(guò)先后調(diào)用bind和start(num_processes=1)函數(shù)啟動(dòng)監(jiān)聽(tīng)同時(shí)創(chuàng)建多進(jìn)程服務(wù)器實(shí)例,后文有關(guān)于此的詳細(xì)描述灰蛙。
bind_sockets在啟動(dòng)監(jiān)聽(tīng)端口過(guò)程中調(diào)用祟剔,getaddrinfo返回服務(wù)器的所有網(wǎng)卡信息, 每塊網(wǎng)卡上都要?jiǎng)?chuàng)建監(jiān)聽(tīng)客戶(hù)端的請(qǐng)求并返回創(chuàng)建的sockets。創(chuàng)建socket過(guò)程中綁定地址和端口摩梧,同時(shí)設(shè)置了fcntl.FD_CLOEXEC(創(chuàng)建子進(jìn)程時(shí)關(guān)閉打開(kāi)的socket)和socket.SO_REUSEADDR(保證某一socket關(guān)閉后立即釋放端口物延,實(shí)現(xiàn)端口復(fù)用)標(biāo)志位。sock.listen(backlog=128)默認(rèn)設(shè)定等待被處理的連接最大個(gè)數(shù)為128仅父。
返回的每一個(gè)socket都加入到IOLoop中同時(shí)添加回調(diào)函數(shù)_handle_connection叛薯,IOLoop添加對(duì)相應(yīng)socket的IOLoop.READ事件監(jiān)聽(tīng)浑吟。_handle_connection在接受客戶(hù)端的連接處理結(jié)束之后會(huì)被調(diào)用,調(diào)用時(shí)傳入連接和ioloop對(duì)象初始化IOStream對(duì)象案训,用于對(duì)客戶(hù)端的異步讀寫(xiě)买置;然后調(diào)用handle_stream,傳入創(chuàng)建的IOStream對(duì)象初始化一個(gè)HTTPConnection對(duì)象强霎,HTTPConnection封裝了IOStream的一些操作忿项,用于處理HTTPRequest并返回。至此HTTP Server的創(chuàng)建城舞、啟動(dòng)轩触、注冊(cè)回調(diào)函數(shù)的過(guò)程分析結(jié)束。
HTTPConnection分析
該類(lèi)用于處理http請(qǐng)求家夺。在HTTPConnection初始化時(shí)對(duì)self.request_callback賦值為一個(gè)可調(diào)用的對(duì)象(該對(duì)象用于對(duì)http請(qǐng)求的具體處理和應(yīng)答)脱柱。該類(lèi)首先讀取http請(qǐng)求中header的結(jié)束符b("\r\n\r\n"),然后回調(diào)self._on_headers函數(shù)拉馋。request_callback的相關(guān)實(shí)現(xiàn)在以后的系列中有詳細(xì)介紹榨为。
1
2
3
4
5
6
7
8
9
10
def __init__(self, stream, address, request_callback, no_keep_alive=False,
?????????????????xheaders=False):
????self.request_callback = request_callback
????# some configuration code
????self._header_callback = stack_context.wrap(self._on_headers)
????self.stream.read_until(b("\r\n\r\n"), self._header_callback)
def _on_headers(self, data):
????# some codes
????self.request_callback(self._request)
多進(jìn)程HTTPServer
Tornado的HTTPServer是單進(jìn)程單線(xiàn)程模式,同時(shí)提供了創(chuàng)建多進(jìn)程服務(wù)器的接口煌茴,具體實(shí)現(xiàn)是在主進(jìn)程啟動(dòng)HTTPServer時(shí)通過(guò)process.fork_processes(num_processes)產(chǎn)生新的服務(wù)器子進(jìn)程随闺,所有進(jìn)程之間共享端口。fork_process的方法在process.py中實(shí)現(xiàn)蔓腐,十分簡(jiǎn)潔矩乐。對(duì)fork_process詳細(xì)的分析,可以參考番外篇:Tornado的多進(jìn)程管理分析回论。
FriendFeed使用nginx提供負(fù)載均衡散罕、反向代理服務(wù)并作為靜態(tài)文件服務(wù)器,在后端服務(wù)器上可以部署多個(gè)Tornado實(shí)例傀蓉。一般可以通過(guò)Supervisor控制Tornado app欧漱,然后再通過(guò)nginx對(duì)Tornado的輸出進(jìn)行反向代理。 具體可以參考下這篇文章:Supervisord進(jìn)程管理工具的安裝使用葬燎。
Tornado RequestHandler和Application類(lèi)
前面一小節(jié)提到了需要了解 web.py 這個(gè)文件误甚,這個(gè)文件最關(guān)鍵的地方是定義了 Application 和 RequestHandler 類(lèi)。我們?cè)倏纯?Tornado 的 Hello World萨蚕,我們?cè)倬?jiǎn)一下靶草,下面是最簡(jiǎn)單的實(shí)例化并啟動(dòng)Application的方式:
1
2
3
4
5
6
7
8
import ioloop
import web
application = web.Application([
????(r'/', MainHandler),
])
application.listen(8888)
ioloop.IOLoop.instance().start()
從代碼里可以看到的是:應(yīng)用里定義了 URI 路由和對(duì)應(yīng)的處理類(lèi)蹄胰,并以此構(gòu)建了application對(duì)象岳遥,然后讓這個(gè)對(duì)象監(jiān)聽(tīng)在8888端口,最后由 ioloop 單例進(jìn)入循環(huán)裕寨,不斷分發(fā)事件浩蓉。
這里的URI路由就是r"/"派继,對(duì)應(yīng)處理類(lèi)就是 MainHandler,它們被放在同一個(gè) tuple 里形成了關(guān)聯(lián)捻艳〖菘撸可以看到,application 是接受一個(gè)列表的认轨,因此可以定義多個(gè)全局路由對(duì)應(yīng)不同處理,往列表里 append 就是了嘁字。
如果只是在 tornado 的框架基礎(chǔ)上進(jìn)行開(kāi)發(fā),那就只需要不斷定義不同的處理類(lèi)衷恭,并把對(duì)應(yīng)路由與其關(guān)聯(lián)即可纯续。
tornado.web 里的 RequestHandler 和 Application 類(lèi)
Tornado 使用 web 模塊的 Application 做URI轉(zhuǎn)發(fā),然后通過(guò)RequestHandler處理請(qǐng)求窗看。 Application 提供了一個(gè) listen 方法作為 HTTPServer 中的 listen 的封裝。
初始化 Application 時(shí)烤芦,一般將處理器直接傳入析校,它會(huì)調(diào)用 add_handlers 添加這些處理器,初始化還包括 transforms (分塊遂唧、壓縮等)吊奢、UI模塊盖彭、靜態(tài)文件處理器的初始化。 add_handlers 方法負(fù)責(zé)添加URI和處理器的映射页滚。
Application 實(shí)現(xiàn) URI 轉(zhuǎn)發(fā)時(shí)使用了一個(gè)技巧召边,它實(shí)現(xiàn)了 __call__ 方法,并將 Application 的實(shí)例傳遞給 HTTPServer 裹驰,當(dāng)監(jiān)聽(tīng)到請(qǐng)求時(shí)隧熙,它通過(guò)調(diào)用 Application 實(shí)例觸發(fā) __call__ 。 __call__ 方法中完成具體的URI轉(zhuǎn)發(fā)工作幻林,并調(diào)用已注冊(cè)的處理器的 _execute 方法贞盯,處理請(qǐng)求音念。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def __call__(self, request):
????transforms = [t(request) for t in self.transforms]
????handler = None
????args = []
????kwargs = {}
????handlers = self._get_host_handlers(request) # 取得請(qǐng)求的host的一組處理器
????if not handlers:
????????handler = RedirectHandler(
????????????self, request, url="http://" + self.default_host + "/")
????else:
????????for spec in handlers:
????????????match = spec.regex.match(request.path)? # 匹配請(qǐng)求的URI
????????????if match:
????????????????handler = spec.handler_class(self, request, **spec.kwargs) # 實(shí)例化
????????????????if spec.regex.groups:?? # 取得參數(shù)
????????????????????...
????????????????????if spec.regex.groupindex:
????????????????????????kwargs = dict(
????????????????????????????(str(k), unquote(v))
????????????????????????????for (k, v) in match.groupdict().iteritems())
????????????????????else:
????????????????????????args = [unquote(s) for s in match.groups()]
????????????????break
????????if not handler:???? # 無(wú)匹配
????????????handler = ErrorHandler(self, request, status_code=404)
????...
????handler._execute(transforms, *args, **kwargs)?? # 處理請(qǐng)求
????return handler
?RequestHandler 完成具體的請(qǐng)求,開(kāi)發(fā)者需要繼承它躏敢,并根據(jù)需要闷愤,覆蓋 head 、 get 件余、 post 讥脐、 delete 、 patch 啼器、 put 攘烛、 options 等方法,定義處理對(duì)應(yīng)請(qǐng)求的業(yè)務(wù)邏輯镀首。
RequestHandler 提供了很多鉤子坟漱,包括 initialize 、 prepare 更哄、 on_finish 成翩、 on_connection_close 麻敌、 set_default_headers 等等赢赊。
下面是 _execute 的處理流程:
RequestHandler 中涉及到很多 HTTP 相關(guān)的技術(shù)释移,包括 Header、Status熏纯、Cookie樟澜、Etag、Content-Type萍膛、鏈接參數(shù)蝗罗、重定向、長(zhǎng)連接等等桩匪,還有和用戶(hù)身份相關(guān)的XSRF和CSRF等等傻昙。這方面的知識(shí)可以參考《HTTP權(quán)威指南》妆档。
Tornado默認(rèn)實(shí)現(xiàn)了幾個(gè)常用的處理器:
ErrorHandler :生成指定狀態(tài)碼的錯(cuò)誤響應(yīng)贾惦。
RedirectHandler :重定向請(qǐng)求须板。
StaticFileHandler :處理靜態(tài)文件請(qǐng)求。
FallbackHandler :使可以在Tornado中混合使用其他HTTP服務(wù)器济蝉。
上面提到了 transform 贺嫂,Tornado 使用這種機(jī)制來(lái)對(duì)輸出做分塊和壓縮的轉(zhuǎn)換,默認(rèn)給出了 GZipContentEncoding 和 ChunkedTransferEncoding 踱稍。也可以實(shí)現(xiàn)自定義的轉(zhuǎn)換,只要實(shí)現(xiàn) transform_first_chunk 和 transform_chunk 接口即可楔敌,它們由 RequestHandler 中的 flush 調(diào)用。
總的來(lái)說(shuō)勺卢,Application對(duì)象提供如下幾個(gè)接口:
__init__ 接受路由-處理器列表黑忱,制定路由轉(zhuǎn)發(fā)規(guī)則
listen 建立服務(wù)器并監(jiān)聽(tīng)端口杨何,是對(duì)httpserver的封裝調(diào)用
add_handlers 添加路由轉(zhuǎn)發(fā)規(guī)則(包括主機(jī)名匹配)
add_transform 添加輸出過(guò)濾器。例如gzip埃跷,chunk
__call__ 服務(wù)器連接的網(wǎng)關(guān)處理接口弥雹,一般是被服務(wù)器調(diào)用
最簡(jiǎn)單的應(yīng)該算是 add_transform 了。將一個(gè)類(lèi)添加到列表里就結(jié)束了厕吉。它會(huì)在輸出時(shí)被調(diào)用头朱,比較簡(jiǎn)單,略過(guò)不提烁巫。
然后是 listen磁餐。它接受端口,地址亦歉,其它參數(shù)水由。也很簡(jiǎn)單砂客,用自身和參數(shù)構(gòu)造 http 服務(wù)器,并讓服務(wù)器監(jiān)聽(tīng)在端口-地址上彤恶。其中涉及到底層 socket 的使用和 ioloop 事件綁定,放在以后再說(shuō)术徊≡蹋總之,可以認(rèn)為產(chǎn)生了如下效果:在特定端口-地址上創(chuàng)建并監(jiān)聽(tīng) socket株憾,并注冊(cè)了該 socket 的可讀事件到自身的__call__方法(亦即墙歪,每逢一個(gè)新連接到來(lái)時(shí),__call__就會(huì)被調(diào)用)
接下來(lái)看 __call__ 方法毕源。這是 python 的一個(gè)語(yǔ)法特性霎褐,這個(gè)函數(shù)使得 Application 可以直接被當(dāng)成函數(shù)來(lái)使用。
這里有一個(gè)問(wèn)題省艳,為什么不直接定義一個(gè)函數(shù)例如 call 并在 listen 方法里把 self.call 傳給服務(wù)器做回調(diào)函數(shù),而是使用 self 呢枣购?它們不都是函數(shù)嗎?有什么區(qū)別呢分瘾?
區(qū)別還是有的。首先上岗,如果使用self.call方法肴掷,那么它就是一個(gè)純粹的函數(shù)台夺,那么 application 的內(nèi)部成員就不能用了(比如路由規(guī)則表)颤介。而使用 self(也不是self.__call__)傳遞給服務(wù)器做回調(diào),當(dāng)這個(gè)對(duì)象被當(dāng)作函數(shù)調(diào)用時(shí)赞赖,__call__會(huì)被自動(dòng)調(diào)用滚朵,此時(shí)對(duì)象上下文就被保留下來(lái)了。python 里好像經(jīng)常這么搞前域。辕近。。
好话侄,來(lái)看看__call__的參數(shù):request年堆,HttpRequest對(duì)象,在 httputil 里被定義,在這里被用到的是它的 host 和 path 成員萧芙,用在路由匹配。忽略錯(cuò)誤情況,這個(gè)方法的執(zhí)行流程如下:_get_host_handler(request) 得到該 host 對(duì)應(yīng)的路徑路由列表姨夹,默認(rèn)情況下吼鱼,任何 host 都會(huì)被匹配(原因詳見(jiàn)__init__)玩敏,返回的列表直接就是傳遞給構(gòu)造 application 時(shí)的那個(gè) tuple 列表,當(dāng)然,對(duì)象變了嘴秸,蛋內(nèi)容是一樣的。然后,對(duì)于路徑路由列表中的每一個(gè)對(duì)象帕翻,用 request.path 來(lái)匹配,如果匹配上了粥鞋,就生成 RequestHandler 對(duì)象筹燕,并根據(jù)正則表達(dá)式解析路徑里的參數(shù),可以用數(shù)字做鍵值也可以用字符串做鍵值具篇。具體見(jiàn) python 的 re.match.groups()。然后跳出列表,執(zhí)行_execute() 方法阴挣,這個(gè)方法在 RequestHandler 里被定義骗爆,下次再說(shuō)律胀,簡(jiǎn)言之罪佳,它的作用是 根據(jù) http 方法轉(zhuǎn)到對(duì)應(yīng)方法勋眯,路徑正則表達(dá)式里解析到的參數(shù)也被原樣保留傳進(jìn)去。
一直有個(gè)疑問(wèn),路由規(guī)則是什么時(shí)候建立的呢而涉?為此著瓶,我們先看 add_handlers 方法,它接受兩個(gè)參數(shù)啼县,主機(jī)名正則蟹但,路徑路由列表。同時(shí)谭羔,我們還要注意到华糖,self.handlers 也是一個(gè)列表,是主機(jī)名路由列表瘟裸,它的每個(gè)元素是一個(gè) tuple客叉,包含主機(jī)名和對(duì)應(yīng)的路徑路由列表。如圖:
?
所以话告,add_handlers 的流程就很簡(jiǎn)單了:將路徑路由列表和主機(jī)名合成一個(gè) tuple 添加到 self.handlers 里兼搏,這樣該主機(jī)名就會(huì)在_get_host_handler 里被檢索,就可以根據(jù)主機(jī)名找到對(duì)應(yīng)的路徑路由規(guī)則了沙郭。這里需要注意的一個(gè)問(wèn)題是:由于.*的特殊性(它會(huì)匹配任意字符)佛呻,因此總是需要保證它被放置在列表的最后,所以就有了這段代碼
1
2
if self.handlers and self.handlers[-1][0].pattern == '.*$':
????self.handlers.insert(-1, (re.compile(host_pattern), handlers))
之前還說(shuō)到病线,默認(rèn)情況下吓著,所有的主機(jī)名都會(huì)被匹配,那是因?yàn)樵赺_init__方法里送挑,它調(diào)用了 add_handlers(".*",handlers)绑莺。由于.*匹配所有主機(jī)名,所以構(gòu)造 application 對(duì)象時(shí)傳入的路徑路由規(guī)則列表就是最終默認(rèn)路由列表了惕耕。
最后看一下__init__方法的流程纺裁。它一般接受一個(gè)參數(shù),handlers,亦即最終匹配主機(jī)的路徑路由列表欺缘。先設(shè)定 transform 列表栋豫,再設(shè)定靜態(tài)文件的路由,然后添加主機(jī)(.*)的路由列表谚殊。
好笼才,回顧一下。Application的__init__方法設(shè)定了.*主機(jī)的路徑路由規(guī)則络凿,listen 方法里開(kāi)啟了服務(wù)器并把自身作為回調(diào)骡送。__call__方法在服務(wù)器 accept 到一個(gè)新連接時(shí)被調(diào)用,主要是根據(jù)路由規(guī)則轉(zhuǎn)發(fā)請(qǐng)求到不同的處理器類(lèi)絮记,并在處理器里被分派到對(duì)應(yīng)的具體方法中摔踱,到此完成請(qǐng)求的處理
RequestHandler的分析
從上一節(jié)的流程可以看出,RequestHandler 類(lèi)把 _execute 方法暴露給了 application 對(duì)象怨愤,在這個(gè)方法里完成了請(qǐng)求的具體分發(fā)和處理派敷。因此,我主要看這一方法(當(dāng)然還包括__init__)撰洗,其它方法在開(kāi)發(fā)應(yīng)用時(shí)自然會(huì)用到篮愉,還是比較實(shí)用的,比如header,cookie,get/post參數(shù)的getter/setter方法差导,都是必須的试躏。
首先是__init__。負(fù)責(zé)對(duì)象的初始化设褐,在對(duì)象被構(gòu)造時(shí)一定會(huì)被調(diào)用的颠蕴。
那對(duì)象什么時(shí)候被調(diào)用呢?從上一節(jié)可以看到助析,在.*主機(jī)的路徑路由列表里犀被,當(dāng)路徑正則匹配了當(dāng)前請(qǐng)求的路由時(shí),application 就會(huì)新建一個(gè) RequestHandler 對(duì)象(實(shí)際上是子類(lèi)對(duì)象)外冀,然后調(diào)用 _execute 方法寡键。__init__ 方法接受3個(gè)參數(shù) : application, request, **kwargs,分別是application單例雪隧,當(dāng)前請(qǐng)求request 和 kwargs (暫時(shí)沒(méi)被用上西轩。不過(guò)可以覆蓋initialize方法,里面就有它)膀跌。這個(gè)kwargs 是靜態(tài)存儲(chǔ)在路由列表里的遭商,它最初是在給 application 設(shè)置路由列表時(shí)除了路徑正則,處理器類(lèi)之外的第三個(gè)對(duì)象捅伤,是一個(gè)字典,一般情況是空的巫玻。__init__ 方法也沒(méi)做什么丛忆,就是建立了這個(gè)對(duì)象和 application, request 的關(guān)聯(lián)就完了祠汇。構(gòu)造器就應(yīng)該是這樣,對(duì)吧熄诡?
接下來(lái)會(huì)調(diào)用 _execute 方法可很。
該方法接受三個(gè)參數(shù) transforms(相當(dāng)于 application 的中間件吧,對(duì)流程沒(méi)有影響)凰浮,*args(用數(shù)字做索引時(shí)的正則 group)我抠,**kwargs(用字符串做鍵值時(shí)的正則 group,與__init__的類(lèi)似但卻是動(dòng)態(tài)的)袜茧。該方法先設(shè)定好 transform(因?yàn)椴还苠e(cuò)誤與否都算輸出菜拓,因此中間件都必須到位)。然后笛厦,檢查 http 方法是否被支持纳鼎,然后整合路徑里的參數(shù),檢查 XSRF 攻擊裳凸。然后 prepare()贱鄙。這總是在業(yè)務(wù)邏輯前被執(zhí)行,在業(yè)務(wù)邏輯后還有個(gè) finish()姨谷。業(yè)務(wù)邏輯代碼被定義在子類(lèi)的 get/post/put/delete 方法里逗宁,具體視詳細(xì)情況而定。
還有一個(gè) finish 方法梦湘,它在業(yè)務(wù)邏輯代碼后被執(zhí)行疙剑。緩沖區(qū)里的內(nèi)容要被輸出,連接總得被關(guān)閉践叠,資源總得被釋放言缤,所以這些善后事宜就交給了 finish。與緩沖區(qū)相關(guān)的還有一個(gè)函數(shù) flush禁灼,顧名思義它會(huì)調(diào)用 transforms 對(duì)輸出做預(yù)處理管挟,然后拼接緩沖區(qū)一次性輸出?self.request.write(headers + chunk, callback=callback)。
以上弄捕,就是 handler 的分析僻孝。handler 的讀與寫(xiě),實(shí)際上都是依靠request對(duì)象來(lái)完成的守谓,而 request 到底如何呢穿铆?且看下回分解。
Tornado的核心web框架tornado.web小總結(jié)
Tornado的web框架(tornado.web)在web.py中實(shí)現(xiàn)斋荞,主要包括RequestHandler類(lèi)(本質(zhì)為對(duì)http請(qǐng)求處理的封裝)和Application類(lèi)(是一些列請(qǐng)求處理的集合荞雏,構(gòu)成的一個(gè)web-application,源代碼注釋不翻譯更容易理解:A collection of request handlers that make up a web application)。
RequestHandler分析
RequestHandler提供了一個(gè)針對(duì)http請(qǐng)求處理的基類(lèi)封裝凤优,方法比較多悦陋,主要有以下功能:
提供了GET/HEAD/POST/DELETE/PATCH/PUT/OPTIONS等方法的功能接口,具體開(kāi)發(fā)時(shí)RequestHandler的子類(lèi)重寫(xiě)這些方法以支持不同需求的請(qǐng)求處理筑辨。
提供對(duì)http請(qǐng)求的處理方法俺驶,包括對(duì)headers,頁(yè)面元素棍辕,cookie的處理暮现。
提供對(duì)請(qǐng)求響應(yīng)的一些列功能,包括redirect楚昭,write(將數(shù)據(jù)寫(xiě)入輸出緩沖區(qū))栖袋,渲染模板(render, reander_string)等。
其他的一些輔助功能哪替,如結(jié)束請(qǐng)求/響應(yīng)栋荸,刷新輸出緩沖區(qū),對(duì)用戶(hù)授權(quán)相關(guān)處理等凭舶。
Application分析
源代碼中的注釋寫(xiě)的非常好:
A collection of request handlers that make up a web application. Instances of this class are?callable?and can be passed directly to HTTPServer to serve the application.
該類(lèi)初始化的第一個(gè)參數(shù)接受一個(gè)(regexp, request_class)形式的列表晌块,指定了針對(duì)不同URL請(qǐng)求所采取的處理方法,包括對(duì)靜態(tài)文件請(qǐng)求的處理(web.StaticFileHandler)帅霜。Application類(lèi)中實(shí)現(xiàn)?__call__?函數(shù)匆背,這樣該類(lèi)就成為可調(diào)用的對(duì)象,由HTTPServer來(lái)進(jìn)行調(diào)用身冀。比如下邊是httpserver.py中HTTPConection類(lèi)的代碼钝尸,該處request_callback即為Application對(duì)象。
1
2
3
def _on_headers(self, data):
????# some codes...
????self.request_callback(self._request)
?__call__函數(shù)會(huì)遍歷Application的handlers列表搂根,匹配到相應(yīng)的URL后通過(guò)handler._execute進(jìn)行相應(yīng)處理珍促;如果沒(méi)有匹配的URL,則會(huì)調(diào)用ErrorHandler剩愧。
在Application初始時(shí)有一個(gè)debug參數(shù)猪叙,當(dāng)debug=True時(shí),運(yùn)行程序時(shí)當(dāng)有代碼仁卷、模塊發(fā)生修改穴翩,程序會(huì)自動(dòng)重新加載,即實(shí)現(xiàn)了auto-reload功能锦积。該功能在autoreload.py文件中實(shí)現(xiàn)芒帕,是否需要reload的檢查在每次接收到http請(qǐng)求時(shí)進(jìn)行,基本原理是檢查每一個(gè)sys.modules以及_watched_files所包含的模塊在程序中所保存的最近修改時(shí)間和文件系統(tǒng)中的最近修改時(shí)間是否一致丰介,如果不一致背蟆,則整個(gè)程序重新加載鉴分。
1
2
3
4
5
6
def _reload_on_update(modify_times):
????for module in sys.modules.values():
????????# module test and some path handles
????????_check_file(modify_times, path)
????for path in _watched_files:
????????_check_file(modify_times, path)
Tornado的autoreload模塊提供了一個(gè)對(duì)外的main接口,可以通過(guò)下邊的方法實(shí)現(xiàn)運(yùn)行test.py程序運(yùn)行的auto-reload淆储。但是測(cè)試了一下冠场,功能有限家浇,相比于django的autorelaod模塊(具有較好的封裝和較完善的功能)還是有一定的差距本砰。最主要的原因是Tornado中的實(shí)現(xiàn)耦合了一些ioloop的功能,因而autoreload不是一個(gè)可獨(dú)立的模塊钢悲。
1
2
3
4
5
6
# tornado
python -m tornado.autoreload test.py [args...]
# django
from django.utils import autoreload
autoreload.main(your-main-func)
asynchronous方法
該方法通常被用為請(qǐng)求處理函數(shù)的decorator点额,以實(shí)現(xiàn)異步操作,被@asynchronous修飾后的請(qǐng)求處理為長(zhǎng)連接莺琳,在調(diào)用self.finish之前會(huì)一直處于連接等待狀態(tài)还棱。
總結(jié)
在前面小節(jié)Tornado HTTP服務(wù)器的基本流程中,給出了一張tornado httpserver的工作流程圖惭等,調(diào)用Application發(fā)生在HTTPConnection大方框的handle_request橢圓中珍手。那篇文章里使用的是一個(gè)簡(jiǎn)單的請(qǐng)求處理函數(shù)handle_request,無(wú)論是handle_request還是application辞做,其本質(zhì)是一個(gè)函數(shù)(可調(diào)用的對(duì)象)琳要,當(dāng)服務(wù)器接收連接并讀取http請(qǐng)求header之后進(jìn)行調(diào)用,進(jìn)行請(qǐng)求處理和應(yīng)答秤茅。
1
2
http_server = httpserver.HTTPServer(handle_request)
http_server = httpserver.HTTPServer(application)