網上都說nginx和lighthttpd是高性能web服務器,而tornado也是著名的高抗負載應用,它們間有什麼相似處呢?上節提到的ioloop對象是如何循環的呢?往下看。
首先關於TCP服務器的開發上節已經提過,很明顯那個三段式的示例是個效率很低的(因爲只有一個連接被端開新連接才能被接受)。要想開發高性能的服務器,就得在這accept上下功夫。
首先,新連接的到來一般是經典的三次握手,只有當服務器收到一個SYN時才說明有一個新連接(還沒建立),這時監聽fd是可讀的可以調用accept,此前服務器可以乾點別的,這就是SELECT/POLL/EPOLL的思路。而只有三次握手成功後,accept纔會返回,此時監聽fd是讀完成狀態,似乎服務器在此之前可以轉身去幹別的,等到讀完成再調用accept就不會有延遲了,這就是AIO的思路,不過在*nix平臺上好像支持不是很廣。。。再有,accept得到的新fd,不一定是可讀的(客戶端請求還沒到達),所以可以等新fd可讀時在read()(可能會有一點延遲),也可以用AIO等讀完後再read就不會延遲了。同樣類似,對於write,close也有類似的事件。
總的思路就是,在我們關心的fd上註冊關心的多個事件,事件發生了就啓動回調,沒發生就看點別的。這是單線程的,多線程的複雜一點,但差不多。nginx和lightttpd以及tornado都是類似的方式,只不過是多進程和多線程或單線程的區別而已。爲簡便,我們只分析tornado單線程的情況。
關於ioloop.py的代碼,主要有兩個要點。一個是configurable機制,一個就是epoll循環。先看epoll循環吧。IOLoop 類的start是循環所在,但它必須被子類覆蓋實現,因此它的start在PollIOLoop裏。略過循環外部的多線程上下文環境的保存與恢復,單看循環:
06 |
with self ._callback_lock: |
07 |
callbacks = self ._callbacks |
09 |
for callback in callbacks: |
10 |
self ._run_callback(callback) |
15 |
if self ._timeouts[ 0 ].callback is None : |
17 |
heapq.heappop( self ._timeouts) |
18 |
elif self ._timeouts[ 0 ].deadline
< = now: |
19 |
timeout = heapq.heappop( self ._timeouts) |
20 |
self ._run_callback(timeout.callback) |
22 |
seconds = self ._timeouts[ 0 ].deadline - now |
23 |
poll_timeout = min (seconds,
poll_timeout) |
34 |
if self ._blocking_signal_threshold is not None : |
37 |
signal.setitimer(signal.ITIMER_REAL, 0 , 0 ) |
40 |
event_pairs = self ._impl.poll(poll_timeout) |
41 |
except Exception
as e: |
47 |
if ( getattr (e, 'errno' , None ) = = errno.EINTR or |
48 |
( isinstance ( getattr (e, 'args' , None ), tuple ) and |
49 |
len (e.args) = = 2 and e.args[ 0 ] = = errno.EINTR)): |
54 |
if self ._blocking_signal_threshold is not None : |
55 |
signal.setitimer(signal.ITIMER_REAL, |
56 |
self ._blocking_signal_threshold, 0 ) |
62 |
self ._events.update(event_pairs) |
64 |
fd,
events = self ._events.popitem() |
66 |
self ._handlers[fd](fd,
events) |
67 |
except (OSError,
IOError) as e: |
68 |
if e.args[ 0 ] = = errno.EPIPE: |
72 |
app_log.error( "Exception
in I/O handler for fd %s" , |
75 |
app_log.error( "Exception
in I/O handler for fd %s" , |
首先是設定超時時間。然後在互斥鎖下取出上次循環遺留下的回調列表(在add_callback添加對象),把這次列表置空,然後依次執行列表裏的回調。這裏的_run_callback就沒什麼好分析的了。緊接着是檢查上次循環遺留的超時列表,如果列表裏的項目有回調而且過了截止時間,那肯定超時了,就執行對應的超時回調。然後檢查是否又有了事件回調(因爲很多回調函數裏可能會再添加回調),如果是,則不在poll循環裏等待,如註釋所述。接下來最關鍵的一句是event_pairs = self._impl.poll(poll_timeout),這句裏的_impl是epoll,在platform/epoll.py裏定義,總之就是一個等待函數,當有事件(超時也算)發生就返回。然後把事件集保存下來,對於每個事件,self._handlers[fd](fd,
events)根據fd找到回調,並把fd和事件做參數回傳。如果fd是監聽的fd,那麼這個回調handler就是accept_handler函數,詳見上節代碼。如果是新fd可讀,一般就是_on_headers 或者 _on_requet_body了,詳見前幾節。我好像沒看到可寫時的回調?以上,就是循環的流程了。可能還是看的糊里糊塗的,因爲很多對象怎麼來的都不清楚,configurable也還沒有看。看完下面的分析,應該就可以了。
Configurable類在util.py裏被定義。類裏有一段註釋,已經很明確的說明了它的設計意圖和用法。它是可配置接口的父類,可配置接口對外提供一致的接口標識,但它的子類實現可以在運行時進行configure。一般在跨平臺時由於子類實現有多種選擇,這時候就可以使用可配置接口,例如select和epoll。首先注意
Configurable 的兩個函數: configurable_base 和 configurable_default, 兩函數都需要被子類(即可配置接口類)覆蓋重寫。其中,base函數一般返回接口類自身,default返回接口的默認子類實現,除非接口指定了__impl_class。IOLoop及其子類實現都沒有初始化函數也沒有構造函數,其構造函數繼承於Configurable,如下:
01 |
def __new__( cls , * * kwargs): |
02 |
base = cls .configurable_base() |
05 |
impl = cls .configured_class() |
06 |
if base.__impl_kwargs: |
07 |
args.update(base.__impl_kwargs) |
11 |
instance = super (Configurable, cls ).__new__(impl) |
15 |
instance.initialize( * * args) |
當子類對象被構造時,子類__new__被調用,因此參數裏的cls指的是Configurabel的子類(可配置接口類,如IOLoop)。先是得到base,查看IOLoop的代碼發現它返回的是自身類。由於base和cls是一樣的,所以調用configured_class()得到接口的子類實現,其實就是調用base(現在是IOLoop)的configurable_default,總之就是返回了一個子類實現(epoll/kqueue/select之一),順便把__impl_kwargs合併到args裏。接着把kwargs併到args裏。然後調用Configurable的父類(Object)的__new__方法,生成了一個impl的對象,緊接着把args當參數調用該對象的initialize(繼承自PollIOloop,其initialize下段進行分析),返回該對象。
所以,當構造IOLoop對象時,實際得到的是EPollIOLoop或其它相似子類。另外,Configurable 還提供configure方法來給接口指定實現子類和參數。可以看的出來,Configurable類主要提供構造方法,相當於對象工廠根據配置來生產對象,同時開放configure接口以供配置。而子類按照約定調整配置即可得到不同對象,代碼得到了複用。
解決了構造,來看看IOLoop的instance方法。先檢查類是否有成員_instance,一開始肯定沒有,於是就構造了一個IOLoop對象(即EPollIOLoop對象)。以後如果再調用instance,得到的則是已有的對象,這樣就確保了ioloop在全局是單例。再看epoll循環時注意到self._impl,Configurable 和 IOLoop 裏都沒有, 這是在哪兒定義的呢? 爲什麼IOLoop的start跑到PollIOLoop裏,應該是EPollIOLoop纔對啊。 對,應該看出來了,EPollIOLoop
就是PollIOLoop的子類,所以方法被繼承了是很常見的哈。
從上一段的構造流程裏可以看到,EPollIOLoop對象的initialize方法被調用了,看其代碼發現它調用了其父類(PollIOLoop)的它方法, 並指定了impl=select.epoll(), 然後在父類的方法裏就把它保存了下來,所以self._impl.poll就等效於select.epoll().poll().PollIOLoop裏還有一些註冊,修改,刪除監聽事件的方法,其實就是對self._impl的封裝調用。就如上節的 add_accept_handler 就是調用ioloop的add_handler方法把監聽fd和accept_handler方法進行關聯。
IOLoop基本是個事件循環,因此它總是被其它模塊所調用。而且爲了足夠通用,基本上對回調沒多大限制,一個可執行對象即可。事件分發就到此結束了,和IO事件密切相關的另一個部分是IOStream,看看它是如何讀寫的。