BT原始碼學習心得(五):統一網路服務介面--RawServer
author:wolfenstein
以後的部分都需要網路服務(種子檔案的產生在本地就可以完成,但是通過這些種子檔案下載實際的內容和提供跟蹤器服務都需要網路),在BT的程式設計中,為網路服務提供了統一的介面,這樣程式中的其它部分需要開啟一個網路服務時,只需要向這個介面進行註冊,並提供相應的處理對象(handler)即可,當網路事件發生時,將會自動這個處理對象中的相關函數進行處理。
這個統一網路服務介面定義在BitTorrent/RawServer.py中,由它去實際調用和網路插口(socket)有關的庫,另外,RawServer還提供add_task功能,可以允許一些任務被延後執行。
RawServer在初始化的時候,可以從外部傳入一個doneflag參數,這是一個Event的資料類型,可以從其它地方觸發它,這樣可以隨時中斷RawServer中的主迴圈(listen_forever中的)。另外還進行一些內部變數的設定。最後,它給自己增加了一個任務,scan_for_timeouts,這個任務會定時得檢查逾時的網路連接,並關閉它們。
我們可以看到add_task的所做的工作就是將要延時執行的任務計算出它的實際執行時間,並把它添加到一個排好序的列表中(funcs),且保持這個列表仍然處於有序狀態,這個列表以實際執行時間為順序。
當其它模組要提供網路服務時,它首先調用RawServer的create_serversocket函數,這個函數會返回一個socket對象,並且這個socket返回時,已經處於listen狀態了。當然,這個時候如果真有外部的網路連接進來,還是不會有什麼動作的,因為相應的處理對象還沒有註冊進來。
接下來應該調用start_listening函數,這個函數的作用是把得到的網路插口和它對應的處理對象添加到一個字典中,該字典以網路插口的描述符(FD)為主鍵。值得注意的是,這個函數名稱中雖然有listen字樣,但是socket.listen函數卻不是在這裡調用,而是在create_serversocket就已經被調用了。傳遞進來的處理對象的類型沒有限制,唯一的要求是它必須包含有external_connection_made函數,這樣當外部網路連接到來時,這個函數就會被調用。處理對象通常還應提供data_came_in函數來處理網路資料,以及connection_flushed函數來處理資料已經正式發出(相對於還在緩衝區的情況)時的處理,後面兩個函數也可以不提供,因為在external_connection_made函數裡,可以把新串連的網路資料處理對象重新置放到一個包含有data_came_in函數和connection_flushed函數的對象。
start_listening函數處理完後,該網路插口就已經存在於serversockets字典中了。
而當其它模組要串連到外部網路時,應該調用start_connection函數,這個函數將把網路插口添加到另一個字典single_sockets中,當然,使用了SingleSocket對象對其進行了一定程度的封裝。從後面的分析可以看到,這個SingleSocket對象的主要功能是對輸出的資料進行了一定的緩衝,並在不會阻塞的情況下把這些資料實際得寫到socket中。
start_connection需要傳入的處理對象是必須包含data_came_in而可以不包含external_connection_made的對象。
在start_listening和start_connection中都用到了poll對象,這是系統提供的一個提供輪詢機制的模組,使用檔案描述符作為參數,可以得到相應的事件(即該檔案描述符對應的插口有資料流入或者留出等),而在這兩個函數中,都調用了poll的註冊函數,方便後面的poll輪詢操作。
需要注意的是,在上面的這些函數被執行後,網路連接還是不會被處理,因為雖然開啟了相應的網路插口,也註冊了相應的處理對象,但是整個的輪詢機制還沒有建立起來。直到listen_forever函數被調用後,這個機制才真正得建立起來。這個函數的主體就是一個無限的while迴圈,只有doneflag這個事件可以被用來中止這個迴圈。它首先做的事情是從添加的任務funcs尋找最近要執行的任務的時間,並與目前時間相減,計算出period,然後用poll輪詢這麼長的時間,這樣做就可以保證輪詢結束後不會耽誤外部任務過久。輪詢到的結果返回在events裡,這是一個列表,它的元素是以檔案描述符和事件所組成的二元組。接下來就是根據時間的情況,把需要馬上執行的外部任務都執行了,_make_wrapped_call的主要作用就是執行外部任務,只是給它們增加一些意外處理的保護代碼。執行完這些外部任務後,調用_close_dead關閉不活躍的網路連接,接下來就是使用_handle_events來處理前面的poll搜集到的網路事件了。
_handle_events的主體是一個for迴圈,檢查每一個sock和它對應的event。首先看它是在serversockets字典中還是在single_sockets字典中,如果是前者,那麼這是一個偵聽中的插口,再檢查網路事件,如果不是出錯事件的話,那麼就說明是有外部串連到達,熟悉socket編程的人都應該知道,這時正確的處理方式是建立一個新的socket,然後讓偵聽中的插口去accept它,以後資料的讀寫應該在新的socket中進行。接下來的處理也是這樣,新的socket被用SingleSocket封裝起來了,並且也被放到single_sockets字典中,因為它和用start_connection建立的socket一樣,都是有可能有資料流入的,而偵聽的插口只需要處理網路連接。接下來,前面註冊的處理對象中的external_connection_made函數被調用了,允許進行一些其它的相關操作,我們注意到,這裡處理對象被原封不動得傳入到新的SingleSocket中,當然實際上在external_connection_made函數中可以把SingleSocket的處理對象重新導向到其它對象中。
接下來的else語句說明sock在single_sockets字典中,只有一種情況例外,就是os.pipe。這種情況下不用處理這個事件,直接continue處理下一個事件即可。然後檢查事件,如果是出錯則關閉該插口,否則就說明是有資料流動,而資料流動無非是流入和流出兩種情況,如果是流入的話,就把資料讀到一個緩衝區裡,然後調用處理對象中提供的data_came_in進行處理,而data_came_in得到的參數直接就是緩衝區中的資料,它不需要再
處理socket以及考慮可能會形成的阻塞等問題了。另外由於SingleSocket中對寫操作也進行了封裝,即如果網路有阻塞的可能,資料也會先寫入緩衝區,這樣data_came_in中就可以隨便調用s.write了。最後如果是資料流出,則調用s.try_write,這個函數實現得也很安全。
最後檢查是否資料都已經真的發出去了(flushed),如果是,則調用處理對象中提供的connection_flushed函數進行收尾工作。
以後我們可以看到,在BT的實現中,建立了各種各樣的對象,而且這些對象之間有各種各樣比較複雜的關係,但是所有的網路服務,都是通過RawServer來進行的,再具體一些,那就是RawServer這個對象只會被建立一個,而所有要求網路服務的模組都會把網路服務的處理對象註冊到這個RawServer中,方便統一管理。
最後說一下,今天用google搜尋發現原來去年就已經有人分析過BT的原始碼,不僅感歎自己孤陋寡聞,不過發現現在的版本(4.0.3)和當時的版本已經有了一些差別,而且我也可以以我的閱讀原始碼的思路繼續前進,提供給大家一個不同的視角,因而決定把我的學習心得繼續寫完,希望大家能夠支援。