天天看點

BitTorrent源碼分析(4.0.3)

  BT的源代碼是使用python寫的,這是一種動态類型的語言,所有的對象不需要定義其類型,任何對象可以作為參數傳入某個函數中,唯一的要求是當調用該 對象的某個方法時,它必須存在。另外這種語言提供了大量的子產品,這些子產品中很多都能在不同的平台實作其功能,大大得友善了編寫跨平台程式。

    在BT的代碼中,主要功能都有指令行模式和圖形界面模式兩種執行方式,但最後它們執行的核心功能的代碼都是相同的,差別在于執行這些核心功能時,傳遞給它們的參數是從指令行和配置檔案處擷取還是從圖形界面中擷取。

    在我開始學習時,看的是4.0.3版本的代碼。主要有兩個主要的執行子產品btdownloadgui和btmaketorrentgui,前者是用戶端, 後者是制造種子檔案的工具(從4.0.0版本開始,btmaketorrentgui代替了btcompletedir)。另外,還有個tracker模 塊也很重要。學習的時候如果喜歡直接切入正題,就可以不看和gui相關的部分,直接看實作核心功能的子產品。

    提一下圖形界面,BT的圖形界面子產品用的是gtk,它的詳細資料可以在這裡找到:http://www.pygtk.org/

    使用gtk編寫圖形界面的好處是它的跨平台性很好,可以在不同的作業系統上生成風格相近的圖形界面。另外在BT中貌似還用了另一個圖形界面子產品庫 (btdownloadcurses),我大概看了一下說明,好像這個curses隻能用于某些平台,想了下我主要的學習目的是BT,于是在GUI方面就 集中精力攻gtk了,這個curses庫就沒有去看它。

我學習BT的過程大概如下:

    看python語言教程熟悉python語言。

    試着看btdownloadgui,發現看着頭很大,另外發現很多子產品在python網站上的子產品參考手冊上沒有。遂發現了gtk的網站,熟悉了一下使用 gtk編寫GUI程式的基本方法後,繼續試圖看btdownloadgui的圖形部分,有些明白,但是還是感覺到有些吃力。

    開始嘗試轉移一下目标,先看btmaketorrentgui,研究一下種子檔案是怎麼生成的,如果心裡對種子檔案的結構有了解再研究下載下傳部分的代碼應該能輕松些。這部分比較成功得完成了,學習到了BT的種子檔案的結構,還對gtk的GUI程式編寫也比較熟悉了。

    接下來看的是tracker部分的代碼,看的時候基本上都看完了,知道了一個tracker是如何得與用戶端通信。但是對于一些具體的資料結構可能還會存在一些模糊的地方。

    最後回過頭來看btdownloadgui的代碼,發現終于可以順利得看下去了。然後将所有看到的結果總結起來,學習到了BT的通信協定。

    今後的部分将把以上說的學習過程具體展開。

0. 程式運作參數的擷取

    把這部分單獨列舉出來,是因為我覺得BT的程式在處理配置參數方面的這部分代碼很有參考價值。

    程式的配置參數首先來源于BitTorrent/defaultargs.py。這個子產品中包含了一些參數的預設值,由于它們是直接編譯進BT的子產品中, 是以即使其它的配置檔案都丢失後,程式還是有一些預設值可以作為參數。defaultargs.py中定義了一個函數get_defaults可以讓各個 可執行子產品得到它們對應的預設值。例如btdownloadgui和btmaketorrentgui得到的預設值是不同的。而且 defaultargs.py中為可執行子產品生成的預設屬性集合是一個以三元組為元素的清單。每一個三元組代表一條屬性(也就是一條程式的配置參數),每 個三元組包含了屬性的名稱,屬性的值和該屬性的說明。值得注意的是,在後面我們可以看到,屬性的說明直接作為幫助資訊輸出給使用者。

    在擷取完預設值後,各個可執行子產品通常都調用BitTorrent/configfile.py中的函數 parse_configuration_and_args來進一步從可能存在的配置檔案和指令行參數中對程式運作的一些參數進行調整。這個函數中首先從 配置檔案中擷取資訊,值得一提的是它擷取配置檔案的方法。BT将在使用者的"主目錄"下建立一個.bittorrent的目錄,并且從這裡讀取配置檔案(如 果這個檔案不存在就建立一個,下次就存在了)。其中"主目錄"的擷取方法在BitTorrent/__init__.py中,它的實作方法比較巧妙,利用 了python的os和os.path庫進行操作,可以針對不同的作業系統使用合适的方法得到這個"主目錄"。在windows下,這個目錄通常是文檔和 設定目錄下的使用者名目錄下的"Application Data",而在linux下這個目錄就是~。

    在parse_configuration_and_args中,還要調用BitTorrent/parseargs.py子產品中的parseargs對 指令行中輸入的參數進行處理。根據其中的注釋,可以看出程式将會把以下格式的參數作為一種控制方面的操作(options)而改變原來的配置,其它的參數 都隻是簡單得堆在args數組中供可執行子產品自己去處理。這些格式有--aaa 1型,即以兩個-号開頭的參數,将和他後面的參數一起,構成一個配置項(aaa=1),-a1型,即以一個-号開頭的參數,直接取一個字母作為配置項的屬 性名,這個參數後面的子字元串作為配置項的屬性值(a=1),-a 1型,即以一個-号開頭的參數,但是後面隻有一個字母,就将下一個參數作為屬性值(是以這裡也是a=1)。

    經過這些處理後,程式的其它部分就可以很友善得使用配置好的參數了,例如:

    if self.config['xxx']

         .......

    或者

    aaa = self.config['bbb']

    等等。

----------------------------------------------------

1.1 種子檔案的編碼方式

    BT的作者使用了一種比較簡單易懂的編碼方式來對設計種子檔案。這種編碼方式能夠很簡單得對python中的各種資料類型,如字元串,整數,清單,字典等進行編碼。而且對于類型的嵌套,如一個清單中的元素又是一個清單等情況能夠進行很好得處理。

    BitTorrent/bencode.py子產品負責進行編碼解碼的工作。函數bencode能夠對python的複雜資料類型進行編碼。這個函數的意思難道是BT encode?它通過恰當得遞歸調用自己來完成任務。

    首先他判斷要編碼的資料的類型,然後根據這個類型調用相應的編碼函數encode_func[type(x)],定義了以下類型的編碼函數:

    encode_int,負責對IntType和LongType類型進行編碼。編碼為一個字母"i"加上這個數值的字元串表示再加上一個字母"e"。就是說整數19851122将會被編碼成"i19851122e"存在檔案中。

    encode_string,負責對StringType類型進行編碼。編碼方式為字元串的長度加上一個冒号":"再加上字元串本身。例如helloworld将被編碼成"10:helloworld"存放在檔案中。

    encode_list,負責對ListType和TupleType類型進行編碼。編碼方式為一個字母"l",然後遞歸得調用相應的編碼函數将清單或者元組的所有元素進行bencode,最後編上一個"e"結束。

    encode_dict,負責對DictType類型進行編碼。編碼方式為一個字母"d",然後遞歸得對每個元素進行處理。在DictType中,每個元 素都由一個key和value對組成。首先以長度加":"加實際值的方式編碼key,因為key通常都是簡單值,是以可以這樣編碼。然後對value進行 bencode,最後加上一個"e"結束。

    通過分析以上的編碼函數我們可以看出,複雜的對象被以此種編碼方式進行編碼後将能夠無歧義地被還原出來。而BT的種子檔案就是這樣一種複雜的對象(字典類 型)。知道編碼方式後,下次介紹種子檔案時,隻需要解釋這個字典類型包含的每個元素的情況即可,儲存成檔案和從檔案中讀取的這個過程就不需要再解釋了。

1.2 種子檔案的生成

   在知道種子檔案采取的編碼方式後,我們現在可以來看一個種子檔案具體是如何生成的了。在BT中,生成種子檔案的可執行子產品是 btmaketorrent.py(指令行模式)或者btmaketorrentgui.py(圖形界面模式),通過分析,可以知道它們最終都将調用函數 make_meta_files進行種子檔案的生成,差別僅僅在于提供給這個函數的參數從何而來。指令行模式下的程式很簡單,即直接從指令行下擷取參 數,GUI部分的程式以後再和下載下傳用戶端的圖形界面程式一起分析,現在我們先直接切入正題。

    BitTorrent/makemetafile.py子產品中提供函數make_meta_files。它的參數意義如下:

    URL:Tracker的URL位址,在BT的協定設計中,還是需要有個伺服器作為tracker來協調各個用戶端的下載下傳的,tracker部分的程式以後會介紹,現在隻需要知道這個URL将要作為一條資訊寫入到種子檔案中即可。

    file:種子檔案的來源檔案或目錄清單(即準備要在BT上共享的資源),注意,這裡的清單意思是該清單中的每一項都為其生成一個種子檔案,而此清單中的每一項可以是一個檔案或者是一個目錄。

    flag:一個Event對象,可以用來檢查是否使用者要求中止程式。程式設計得比較合理,可以在很細的粒度下檢查這個Event是否被觸發,如果是則中止執行。

    progressfunc:一個回調函數,程式會在恰當的地方調用它,以表示現在的工作進度,在指令行模式下,這個回調函數被指向在控制台上顯示進度資訊的函數,在GUI模式下,這個回調函數則會影響一個圖形界面的進度條。

    filefunc:也是一個回調函數,程式會在恰當的地方調用它,以表示現在在處理哪個檔案。

    piece_len_pow2:分塊的大小,BT中把要共享的資源分成固定大小的塊,以便處理。這個參數就是用2的指數表示的塊的大小,例如當該參數為19的情況下,則表示共享的資源将被分成512k大小的塊為機關進行處理。

    target:目标檔案位址,即種子檔案的位址。這個參數可以不指定(None),則種子檔案将與公享資源處于同一目錄。

    comment:說明。一段可以附加在種子檔案内的資訊。

    filesystem_encoding:檔案系統編碼資訊。

    make_meta_files的主要工作是進行一系列的檢查。例如在開始的時候就檢查files的長度(元素的個數)和target,當files的長 度大于1且target不是None的時候就會報錯,因為如果要生成多個種子檔案的話,是不能指定target的(這樣target隻确定了一個種子檔案 的儲存位置)。接下來檢查檔案系統的編碼問題。然後把files中所有以.torrent結尾的項目全部刨掉,剩下的作為參數傳遞給 make_meta_file進行處理,注意,這個函數一次生成一個種子檔案。

    下面來看make_meta_file,它一開始計算出塊的大小,以2的指數為基礎。接下來找到種子檔案的儲存位址,如果有target,以target 為準,否則如果要對一個目錄生成種子檔案,則生成以那個目錄名為名稱,字尾".torrent"的檔案。否則生成以源檔案為名稱,後 綴".torrent"的檔案。

    下面調用函數makeinfo來生成一個"info"。這個info是什麼東西呢?繼續看。makeinfo首先檢查傳給它的path,看看是單個檔案還 是一個目錄。如果是一個目錄的話,則調用subfiles把這個目錄下的所有檔案全部列出來,這個subfiles設計得比較巧妙,使用堆棧的方法避免了 遞歸調用。從subfiles得到結果後,首先對它們進行排序。然後使用變量fs儲存這些檔案的清單資訊,fs是一個list結構,每個元素包含了檔案名 稱和它的大小組成的二進制組。接下來就是記錄檔案的内容了,下面的這個算法看上去有點暈,其實它的意義是很明确的,每次從要共享的資源裡讀取長度為 piece_length(就是前面那個以2的指數為基礎計算出來的塊的大小)的資料,然後計算它的sha消息摘要值。如何做到這一點呢?就是根據那個排 好序的檔案清單,讀出piece_length的長度的内容,如果這個檔案長度不夠,則再讀下一個檔案,知道長度夠了或者讀完所有檔案為止。生成一個消息 摘要後把它加入到pieces數組中,再讀下一塊,直到全部處理完。為一個檔案生成info的方法類似,隻是更簡單,直接從這個檔案中一塊一塊得處理即 可。最後這個makeinfo傳回的info是一個字典,它的資料如下:

    pieces:每一塊的消息摘要值的連接配接。

    piece length:每一塊的長度。

    files:檔案的清單資訊,這裡由于檔案順序和生成消息摘要的順序是相同的,以後BT的用戶端根據種子檔案的描述,就可以很清晰得确定原始的檔案名和它們的大小,再配以消息摘要值,就可以檢查下載下傳内容是否正确了。

    name:種子檔案的内部名稱,種子檔案可以被随便改名,但是為了識别它友善,内部還是起了這麼一個名稱的,通常用要共享的資源來命名它。

    我們注意到flag.isSet多處被檢查,其中粒度最小的地方是在讀取了一塊之後。它傳回後将一路傳回到make_meta_files結束,這樣使用者随時可以中斷程式的執行。

    在makeinfo傳回info這個字典類型的資料後,再調用check_info這個函數對其内容進行檢查,這個函數定義在BitTorrent/btformats.py子產品中,後面在用戶端進行下載下傳的時候還需要檢查它。

    最後我們看到的是一個類型為字典的data,其中的元素包含了announce,一個字元串,creation date,一個整型資料,info,又是一個字典,如果有comment的話,那麼還包含了字元串類型的comment。

    最後把這個類型為字典的data儲存到磁盤上,工作就算完成了。怎麼對這種比較複雜的資料類型進行編碼以友善儲存呢?就是上次提到的bencode。

    是以我們可以看到,一個種子檔案就是一個類型為字典的data編碼後的情形。

----------------------------------------------------

2. 統一網絡服務接口--RawServer

    以後的部分都需要網絡服務(種子檔案的生成在本地就可以完成,但是通過這些種子檔案下載下傳實際的内容和提供跟蹤器服務都需要網絡),在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)和當時的版本已經有了一些差别,而且我也可以以我的閱讀源代碼的思路繼續前進,提供給大家一個不同的視角,因而決定把我的學習心得繼續寫完,希望大家能夠支援。

----------------------------------------------------

3.1 跟蹤伺服器(Tracker)的代碼分析(初始化)

    Tracker在BT中是一個很重要的部分。這個名詞我注意到以前的文章中都是直接引用,沒有翻譯過來,想了一下,決定把它翻譯成跟蹤伺服器。

    在BT下載下傳中,種子檔案表明了要下載下傳的檔案的資訊和對它進行檢查的消息摘要碼,但是每個對等客戶(peer,以後我把peer全部翻譯成對等客戶,以差別 client)要擷取其它對等客戶的資訊時,還是要和跟蹤伺服器聯系的。跟蹤伺服器上面不儲存任何和種子所代表的内容有關的檔案,它隻記錄所有下載下傳該種子 的機器的IP位址,端口等資訊,并在客戶向它請求是傳回一些這樣的資訊清單,具體的實際内容,由對等客戶之間完成互動。

    跟蹤伺服器的代碼實作在BitTorrent/track.py中,在bttrack.py中隻是很簡單得一行:

    track(argv[1:])

    這樣就把參數傳到track.py的track函數。track函數本身也比較簡單,處理參數和相關的配置檔案,建立一個RawServer,然後用 create_serversocket建立伺服器套接字,然後開始服務。關于在BT中使用網絡服務上次已經有很詳細地介紹,這裡不再重複。隻是針對 tracker函數的具體情況,分析一下運作到listen_forever後的情況,首先,建立了Tracker對象,打開了在某個端口 (config['port'])偵聽的網絡服務,這個函數的處理對象是一個HTTPHandler。是以我們要分析程式的流程隻需要先分析 Tracker的初始化函數,看看它建立後都做了些什麼,然後再看HTTPHandler實際分析它的網絡協定。

    在Tracker對象的初始化函數中,首先還是對各種變量的初始化。然後要從一個狀态檔案中進行一些狀态恢複,也就是恢複state變量。這個變量中的值 很重要,我們可以需要從一些地方來得知它的結構,狀态檔案的讀取和儲存出得不到它的資訊,因為這兩處的實作方式就是bencode和bdecode,隻能 保證無論state的結構是什麼都能合适得被儲存和恢複,由此又看出bencode編碼設計的巧妙。但是有一個函數對我們分析state的内部結構很有幫 助,那就是statefiletemplate,這個函數檢查state中的值是否合法,是以我們可以從這裡得到state的一些結構資訊。

    首先,state必須是一個字典類型的變量。然後檢查每一項的值。如果發現一項關鍵字是'peers',那麼它的值必須也是一個字典,這個字典是一個以種子檔案的資訊部分的消息摘要值為關鍵字的字典,由于sha摘要算法比較好得滿足了摘要算法的要求,即不同的種子檔案它們生成相同摘要的機率極小。而且由于 這是由種子檔案的内容生成的摘要值,是以即使把種子檔案改名,還是可以識别出來是哪個種子檔案。是以'peers'的值可以看成是為每一個種子檔案記錄的 資訊,那麼為每個種子檔案記錄的是什麼資訊呢?這個資訊又是一個字典,這次以每個對等客戶的ID為關鍵字,每個對等客戶在連接配接到跟蹤伺服器的時候都會為自 己生成一個ID,這個ID怎麼生成的以後看用戶端的代碼可以知道,現在我們知道的是,它的長度必須為20。這個字典的值,嗯,又是個字典,不過這個字典的 意義就明顯多啦,包括了IP是多少,端口是多少,還剩多少沒有下載下傳完。是以state的内容可以看成是這樣的:{'peers':{},...},其中 peers的結構是這樣的:{hash1:{ID1: {'ip':xxx.xxx.xxx.xxx,'port':xxxx,left:XXXX}, ID2: {'ip':yyy.yyy.yyy.yyy,'port':yyyy,left:YYYY},...},hash2:{...},...}。以上是 state中'peers'這一項。'completed'這一項就相對結構簡單了,它記錄的是每個種子檔案的下載下傳完成情況,它的結構是個字典,以每個種 子的資訊部分的消息摘要值為關鍵字,而對應的值就是一個整數,表示該種子檔案已經有多少人完成了下載下傳。接下來是'allowed'項,這項記錄了該跟蹤服 務器所關注的所有的種子的資訊,仍然以資訊部分的消息摘要值為關鍵字,内容就是該種子檔案的實際資訊,從後面的分析(對 BitTorrent/parsedir.py的分析)可以知道是哪些資訊,另外由于之前對種子檔案的内部結構我們已經比較清楚,是以也可以猜出部分。 state中還有'allowed_dir_files'項,這一項也是記錄檔案資訊的字典,但它是以每個檔案的檔案名為關鍵字(而不是消息摘要值),每個檔案的項目是一個清單,結構如下:[(檔案修改時間,檔案大小),消息摘要值],就是說,這個以檔案名為關鍵字的字典它的每一個值都是一個清單,這個列 表有兩個元素,第一個元素是一個二進制組,内容是檔案修改時間和檔案大小,第二個元素是消息摘要值。最後,我們注意到statefiletemplate在 處理'allowed'項和'allowed_dir_files'項時還有一些額外的檢查代碼,即所有在'allowed'項裡面出現的元素,它的消息 摘要值都必須在'allowed_dir_files'項中出現,且'allowed_dir_files'中所有的項中的值的消息摘要部分必須在 'allowed'中出現,另外'allowed_dir_files'中不得出現重複的消息摘要值('allowed'項本身就以消息摘要值為關鍵字, 而字典的關鍵字已經保證不會重複)。

    是以現在我們知道了state中的注意部分的結構。下面我們注意這兩句:

    self.downloads    = self.state.setdefault('peers', {})

    self.completed    = self.state.setdefault('completed', {})

    這樣就把state中的'peers'和'completed'的值傳到了downloads和completed中,更重要的是,以後在跟蹤伺服器的運 行過程中,如果'peers'和'completed'的值發生改變(那簡直是一定的),state中的相應值也會發生變化,這樣,儲存dfile時,就 可以及時更新state的值了。以後我們分析跟蹤伺服器運作過程的時候少不了和它們打交道,現在我們可以先記住, downloads儲存了所有的下載下傳的客 戶端的資訊,completed儲存所有的種子的下載下傳完成情況的統計資訊。

    下面的這個for循環根據配置檔案處理NAT的問題,以及計算種子的個數。completed隻是記錄所有下載下傳完成的客戶的數目,而隻有已經下載下傳完成 (left=0),但是還在downloads中出現(即下載下傳完畢但是沒有關閉用戶端)的用戶端才算是一個種子。這裡我們可以很容易得看 出,seedcount是一個以資訊摘要為關鍵字,整型為值的統計種子數的一個字典。

    下面是一個計算的變量,times表示了每個種子(以資訊摘要為關鍵字)中每個客戶(以客戶ID為關鍵字)的上次的有活動的時間。接下來增加了兩個任務,每隔一段時間儲存一下dfile,并且檢查下載下傳的用戶端是否已經有很長時間沒有反應的。

    接下來準備一個日志檔案,并試圖把标準輸出重定向到這個日志檔案中。

    最後要去尋找該跟蹤伺服器所關注的所有的種子,即parsedir,這個函數可以自己去看,相信在知道了種子檔案的編碼格式和前面的狀态中的項的要求後, 不難分析。總得說來,這個函數做了以下事情,即尋找某個目錄下所有的.torrent檔案,把這些檔案中的資訊讀取進來,并且排除錯誤,重複等等不合要求 的,然後進行加工,輸出符合要求的結果,儲存在allowed和allowed_dir_files中,進而影響state。

    現在tracker對象已經建立起來,它已經有它要進行跟蹤的所有種子的資訊,并且準備好了維護所有連接配接進來的客戶的清單,是以它可以正式開始提供跟蹤服務了。下一次我們就可以看看tracker動起來的效果。

3.2 跟蹤伺服器(Tracker)的代碼分析(HTTP協定處理對象)

    上次我們分析了Tracker類初始化的過程,現在開始具體看跟蹤伺服器是如何提供服務的。

    首先分析Tracker處理對象是HTTPHandler,它定義在BitTorrent/HTTPHandler.py中,這個對象的初始化函數很簡 單,隻是把Tracker.get函數指派到自己的一個内部變量備用。當有外部網絡連接配接到達時,根據前面對RawServer的分析,我們知 道,HTTPHandler.external_connection_made函數會被調用,它維護了自己内部的一個字典connections,以傳 進來的參數connection(它的類型是SingleSocket)為關鍵字,值為一個建立立的HTTPConnection,建立立的 HTTPConnection也主要是進行一些值的初始化,另外注意這句:

    self.next_func = self.read_type

    這個變量被指向自己的一個函數,後面我們還會看到,它還會發生變化,以靈活處理資料的不同部分。

    現在可以分析用戶端和跟蹤伺服器的網絡通訊協定了。當有資料到達時,HTTPHandler.data_came_in會被調用,從它的代碼中我們可以一 眼看出,起主要作用的其實是該網絡連接配接對應的HTTPConnection的data_came_in函數,它首先檢查donereading标志和 next_func函數,即如果已經完成讀的操作或者沒有next_func來處理下一步,都直接傳回,然後将data(網絡中讀到的資料)添加到自己内 部的buf中,下面是一個while循環,可以看出,它的做法是每次從網絡資料中尋找/n值,以該值做為兩個不同的處理單元,然後将這個回車前面的部分賦 值到val,後面的部分指派到buf(就相當于buf在這個回車前面的部分砍掉,剩下的留待下一次處理),然後将這個val交由next_func處理, 處理的結果傳回給next_func,意思就是在next_func裡處理完這部分值後,它很清楚下面一部分該由哪個函數處理,然後将next_func 重新定向到它就行了,最後進行一些檢檢視看還要不要繼續處理。這個函數我們可以看出,設計得比較巧妙,能夠自動得把一個協定的不同的部分分到不同的函數進 行處理,而且即使網絡阻塞了,隻來了一部分資料,下次又來一部分資料,隻要它和buf一整合,next_func永遠指向處理下一部分資料的函數。

    從HTTPConnection的初始化過程我們知道,第一部分的資料處理的函數read_type,首先去除空格,然後把它們按照空格符分開,如果有三 個詞,那麼認定它的格式為command path garbage,否則,認為是command path。然後檢查command必須是GET或者HEAD,現在也已經可以猜出來path應該是一個URL路徑,至此,我們可以看出,用戶端和跟蹤服務 器的通信協定其實就是HTTP協定。接下來就是read_header來讀取HTTP的頭部。它首先看有沒有資料,如果有的話,很簡單,隻是維護一個字典 headers,且尋找到':',':'之前的就是關鍵字,之後的就是值,然後next_func還是read_header,就是說,剩下的資料都是一 行一行的頭部資訊。全部讀完後,檢查headers裡面有沒有accept-encoding項,這項指定傳回的資料的編碼方式,隻有兩種,普通模式 ('identity')和壓縮模式('gzip'),然後調用getfunc,其實就是Tracker.get來正式處理使用者的HTTP請求,而且已經 把請求轉化成比較友善的參數,即path(使用者的請求URL)和headers資訊。處理完畢後,如果傳回的結果不是None的話,則調用answer把 處理結果傳回給使用者。

    我們先看answer,看到它的參數,我們就知道,它把傳回的結果轉化成HTTP協定的要求。傳給它的參數是一個元組,包含回應代碼,回應字元串,頭部數 據,正文資料四部分。它首先看是否要壓縮,如果是的話,就進行壓縮,但是壓縮後它把壓縮後的資料和之前的資料進行長度比較,如果壓縮後資料反而更長,那麼 就不壓縮了。接下來是進行日志的記錄,諸如某年某月某日某時某分某人在這裡請求了某物,傳回了某些資料等等。前面我們注意到在Tracker初始化的時候 已經把标準輸出重定向到日志檔案中了,是以這裡的print其實就是往日志檔案中寫。然後用一個StringIO來處理字元串的操作,可以不斷得往裡面 write,我們看到,程式按照标準的HTTP應答格式("HTTP 1.0 XXX ResponseStringBlablabla../n")的格式,全部處理完後,一次性地往connection裡write,把它傳送到網絡 裡,RawServer裡面已經幫我們處理好了網絡阻塞之類的問題,然後檢查,如果資料全部寫出去了,那麼就關閉這個連接配接。HTTP協定也确實是這樣的, 一個請求,一個回應,就完成了。

    現在我們可以看到,在BT中用戶端和跟蹤伺服器之間的通信協定就是HTTP協定,而且HTTPHandler和HTTPConnection已經把 HTTP的很細節的部分全部都處理好了,這就意味着Tracker.get已經得到了一個連接配接對象,一個使用者請求的位址,以及一個字典類型的HTTP請求 頭部資料,并且這個函數隻需要專心得完成處理,并把處理的結果以包含HTTP回應代碼(200,404,500等),回應字元串(如Not Found,這樣和前面的代碼合起來就是HTTP 1.0 404 Not Found),HTTP回應頭部資料和正文資料的四元組傳回即可。

    下一次,我們就可以很仔細得看Tracker到底是如何得處理使用者請求了。

3.3 跟蹤伺服器(Tracker)的代碼分析(使用者請求的實際處理)

    通過上一次的分析,我們已經知道了Tracker采用http協定和用戶端通信,這一次我們就可以直接分析Tracker.get函數的代碼,看看跟蹤伺服器是如何處理使用者的請求的。

    首先是檢查IP,一個是通過網絡連接配接直接得到的IP(這個有可能是對方的http代理伺服器的IP),另一個是從請求的頭部資料中解析出來的IP,通過分 析函數get_forwarded_ip和_get_forwarded_ip我們可以發現,它的原理是從頭部資料中看有沒有這些關鍵 字:http_x_forwarded_for,http_client_ip,http_via,http_from,如果有的話,就說明目前的 http連接配接中的網絡的另一頭是一個代理伺服器,而不是實際的用戶端,有些http代理伺服器會送出這些http請求的頭部資料告訴伺服器用戶端的真實 IP位址。還有就是要檢查這些IP位址是否合法,是否為本地位址 (10.*.*.*, 127.*.*.*, 169.254.*.*, 172.16.*.*-172.31.*.*, 192.168.*.*)等,然後确認 真實的IP值。接下來準備好paramslist準備處理請求參數,這是一個字典,但是它的每個元素的值是一個清單,這種情況在http請求的參數清單中 很常見。

    接下來用urlparse把使用者的http請求分成标準的幾部分,例如,類似如下的請求:

    http://xxx.xxx.xxx/path;parameters?query1=a&query2=b&query3=c#fragment

    将被分割成這樣的6塊:http,xxx.xxx.xxx,path,parameters,query1=a&query2=b&query3=c,fragment

    接下來把query中的值按照'&'以及'='進行處理,把它們填入到paramslist中,确認出請求的參數。下面就是根據各種情況傳回給使用者适當的結果了。

    首先,如果使用者請求路徑是空的或者是'index.html'(對應于http://xxx.xxx.xxx/或者http://xxx.xxx.xxx/index.html), 那麼就傳回給使用者一個資訊頁面,使用get_infopage函數完成,當然,在實際運作中,可以把配置檔案設定為不允許顯示資訊頁面,這樣 get_infopage就會傳回HTTP404代碼,否則,生成一個頁面檔案,它包含了一些基本資訊,以及該跟蹤伺服器目前關注的種子檔案的清單以及它 們的一些統計情況。接下來的情況是使用者請求路徑'scrape'或者'file',也是傳回相應的資訊給使用者。其中,get_scrape傳回的各個種子 檔案的統計資訊,例如某個種子有多少下載下傳完了,多少人還沒下載下傳完成等。而'file'則是直接擷取某個種子檔案(即允許使用者直接從跟蹤伺服器下載下傳種子文 件)。注意在以上的請求過程中,表示某個具體的種子檔案的關鍵字是它的資訊部分的消息摘要值(infohash)。

    在處理完另一種情況,使用者要求傳回圖示檔案後,接下來就是跟蹤伺服器的主要功能,announce了。是以如果路徑不是announce的話,那就傳回 HTTP404。下面先擷取infohash,即用戶端請求的是哪個種子檔案的情況,然後進行check_allowed檢查,排除有些種子不能在此跟蹤 伺服器上下載下傳的情況。這樣可以及時得對跟蹤伺服器做一些授權方面的操作,例如如果發現有人釋出了有違反國家法律法規的内容的種子,跟蹤伺服器的維護人員隻 需要及時得把這個種子的infohash添加到這個清單中,就再沒有人可以下載下傳這個種子了。

    在排除了各種意外的情況後,就可以用add_data把使用者的資訊添加進去了。一個跟蹤伺服器在擷取了一個用戶端要求下載下傳某個種子檔案的請求後,它把這個 客戶的資訊記錄起來,這樣他就知道有哪些客戶對某個具體的種子檔案感興趣。然後再根據這些清單,選取一些客戶的資訊傳回給客戶,而要下載下傳這個種子檔案中的 實際共享資源?自己去找其它客戶(客戶稱呼其它客戶時應該叫對等客戶)解決吧,跟蹤伺服器上一個位元組都沒有。

    add_data首先從downloads這個字典中找到關鍵字為infohash的項,指派到peers中,如果downloads中沒有這項,則建立 這項(setdefault,詳見python庫參考手冊之内建類型字典的詳細說明),然後檢查傳進來的參數是否合法,如peerid必須是20個字 節,event(如果有)隻能是'started','completed','stopped','snooped'中的一項等。peers是一個以 peerid為關鍵字的字典,是以先看看原來有沒有這個peer,用peerid把它取出來。然後準備好rsize,這個是傳回的項(即傳回多少個對等客 戶的資訊給客戶)的數目,它由客戶的要求以及伺服器的配置參數共同決定。然後如果使用者的請求中有event=stopped項,則删除該客戶的資訊,否 則,如果peer為空(peers中沒有這個peerid的peer),則設定好一個新的peer的資訊,然後添加進去,如果peer已經存在了,那麼就 更新一下它的資訊。最後傳回rsize完成任務。其中有一個NAT處理的類,定義在BitTorrent/NatCheck.py中,它通過往使用者聲稱的 IP和端口進行一次連接配接,看看有沒有反應,以确認NAT情況,并且記錄下來。最後還要把這些資料放到becache中,這個地方存放經過一定格式處理後的 客戶資料,可以加快傳回客戶資訊的函數的處理過程。becache中所有的客戶資訊都按照下載下傳完成(做種)和未完成分開了,這個可以友善伺服器傳回一定的 做種peers和非做種peers給客戶。

    在add_data把客戶的資料更新好後,接下來就是用peerlist函數傳回一定量的客戶資訊了。它根據使用者的要求(rsize),以及所有的 peers中種子數占的比例,決定傳回給客戶的資訊中,有多少是做種的peers,即傳回給客戶的做種peers占所有傳回給客戶的peers的比例,應 該接近于所有在下載下傳這個種子的做種peers和全部peers的比例。然後把兩部分的資訊複制到一個cache中,再用shuffle把它們打亂,最後把 這些資訊傳回給客戶。是以我們可以看到,跟蹤伺服器傳回使用者資訊的準則是,在保持種子所占的比例接近全局的種子比例的情況下,随機選取客戶資訊傳回給客戶。

    今天把跟蹤伺服器的代碼全部分析完了,下一次就可以開始用戶端源代碼分析了。 

----------------------------------------------------

4 用戶端源代碼分析(圖形界面淺析)

    用戶端将從btdownloadgui.py開始進行分析,這樣可以順便把Python中的GUI程式設計也看一下。Python中的GUI程式設計也有很多内容,是以不可能深入得分析,僅僅以BT的源代碼為例看一下。

    btdownloadgui.py中使用gtk作為其圖形界面的開發庫。這個庫中提供了很豐富的類,可以來建立圖形界面中所需要的各種widget,而在 主要的視窗類DownloadInfoFrame的初始化過程中,程式的主要任務就是建立視窗中要用到的各種各樣的widget,并且把它們加入窗體。然 後用connect函數把某個widget上可能會發生的事件與某個處理函數連接配接起來,這樣,一個GUI界面就建立起來了。具體的過程可以參考gtk的每 個類的說明文檔,并不困難。

    這裡再簡單介紹一下btdownloadgui.py中的其它的類。它們通常都是GUI界面中的各種子視窗,需要在某個按鈕或者菜單被選中後彈出。我們也可以看到作者在GUI界面方面的設計的一些巧妙之處。

    Validator:在一個基本的Entry上繼承的類,但是可以對使用者輸入的值進行判斷,隻有出現在有效字元清單中的輸入有效。另外在這個類的基礎上還 繼承了IPValidator,PortValidator,PercentValidator,MinutesValidator對各種輸入資料進行校驗。

    RateSliderBox:這是一個表示速率的滑動塊,可以根據滑鼠的調整顯示出對應的速度以及相應的網絡連接配接類型。内部完成了滑塊位置和對應速率的轉換。

    StopStartButton:停止/開始按鈕,可以通過點選這個按鈕來臨時停止或者恢複所有的種子的下載下傳。

    VersionWindow(顯示版本資訊),AboutWindow(顯示關于資訊),LogWindow(顯示日 志),SettingsWindow(進行一些參數設定),FileListWindow(檔案清單視窗),PeerListWindow(對等客戶清單 視窗),TorrentInfoWindow(種子檔案資訊視窗)都是一個子視窗,它們在相應的功能被調用時會彈出來,其中LogWindow使用到了 LogBuffer中的内容,即在任何需要記錄日志的地方調用LogBuffer的log_text函數,然後LogWindow可以把它們都顯示出來。

    TorrentBox是一個種子檔案下載下傳任務在圖形界面上的表示的基類。裡面定義了名稱辨別,狀态圖示,進度條等等GUI元素,所有的下載下傳任務都以這個類 為基類。KnownTorrentBox則是已經不在下載下傳任務清單的圖形界面表示,可以把它拖到下載下傳隊列中,這樣就可以繼續下載下傳這些任務,這有兩種情況, 把已經完成的任務拖到下載下傳隊列中就表示要繼續做種,而把已經失敗的任務拖到下載下傳隊列中就表示要恢複下載下傳。DroppableTorrentBox是一個增 加了托拽對象處理的類。以DroppableTorrentBox為基類的類有 QueuedTorrentBox,PausedTorrentBox,RunningTorrentBox分别對應了還在隊列中的下載下傳任務,暫停的任務 和正在運作的任務。KnownTorrentBox的區域中的項目可以托拽到下載下傳隊列中,另外QueuedTorrentBox和 PausedTorrentBox,RunningTorrentBox中的項目可以自由托拽,滿足使用者對下載下傳任務的隊列管理要求。這裡還要提一下,那就 是RunningTorrentBox和PausedTorrentBox不能同時出現,它由開始提到的StopStartButton管理,即暫停/運 行所有的任務,而QueuedTorrentBox是那種由于同時下載下傳的任務的數量的限制而暫時得不到下載下傳的任務,即使StopStartButton的 狀态是運作所有任務,它也不會被運作,隻有當某個下載下傳任務結束或者中止後,系統從隊列中選取一個進入運作隊列。

    以上是每一個具體的運作項目對應的widget,下面來看它們的容器,HSeparatedBox是一個容器的基類,定義在 BitTorrent/GUI.py中,這個子產品中還定義了其它一些為GUI顯示友善的函數和對象。HSeparatedBox中可以添加若幹子視窗,并 且可以對它們進行重新排列,另外它可以用分割線分割添加進來的子視窗。DroppableBox是HSeparatedBox的子類,它增加的功能是處理 托拽對象離開,KnownBox則又是DroppableBox的子類,它将被填充進若幹個KnownTorrentBox,對應的還有 RunningBox和QueuedBox,填充入相應的下載下傳任務的widget(注意到PausedTorrentBox和 RunningTorrentBox不能同時出現,它們都被放在容器RunningBox),而RunningAndQueueBox是一個同時塞進了 RunningTorrentBox和QueuedBox的容器。

    系統中每個TorrentBox儲存某個具體種子的資訊僅僅是為了顯示,當需要的時候,這些widget都是随時被删除,或者建立,或者進行重新排列,這些種子的随着運作的時間一些統計資訊發生改變,也會通過這些widget顯示出來。

    BitTorrent/TorrentQueue.py子產品完成了從GUI界面到實際的種子下載下傳任務的子產品中的銜接。它實作了Feedback的接口,這 樣當某個種子的下載下傳任務發生變化(如完成,出錯等)後,可以及時得通知它,然後顯示在GUI界面上。通過分析其它的BT的下載下傳程式(即非圖形界面的下載下傳程 序,如btdownloadheadless.py等),也可以看到有一個繼承了Feedback接口的類,但是它的實作方式通常是以文字的形式表現出種 子的變化。另外,TorrentQueue子產品還可以恢複上次的下載下傳情況(讀取一些狀态檔案),通過所有的下載下傳程式的分析(圖形界面的或者文本的)我們可 以認為,實際執行下載下傳任務的對象是BitTorrent/download.py中的Multitorrent對象為。而TorrentQueue的代碼 仍然算在GUI部分中,通過分析它的代碼,我們已經知道它已經讀取了相應的種子檔案中的資訊,并且進行了相關的處理。

    由于我們主要是要分析BT的用戶端的實際功能代碼,是以GUI部分隻能比較簡略得說一下,其實這部分可以對着gtk的參考手冊看相關的圖形界面程式的源代 碼(btdownloadgui.py,btmaketorrentgui.py),還是比較好分析的。下一次開始就可以分析 BitTorrent/download.py中的Multitorrent,直奔主題了。

----------------------------------------------------

5.1 用戶端源代碼分析(相關對象一覽)

    BitTorrent/download.py中的Multitorrent對象能夠開始實際的下載下傳任務。要開始下載下傳,需要建立一個 Multitorrent對象,然後反複得調用start_torrent方法開始一個新的下載下傳,調用這個方法時必須已經準備好相應的下載下傳任務的資訊作為 參數,包括已經處理好的元資訊(經過BitTorrent/ConvertedMetainfo.py子產品進行處理),配置資訊,一個實作了 FeedBack接口的類(這樣種子在下載下傳的時候狀态發生變化可以及時反映出來,至于是反應在文字資訊上還是在圖形界面上那就看這個FeedBack接口 的實作),以及儲存種子檔案内容的本地目錄。這個函數會傳回一個_SingleTorrent對象,代表一個單一的種子檔案下載下傳任務,這個對象前面的一條 短線代表它是私有對象,不能單獨建立,隻能通過start_torrent來進行建立。它除了使用FeedBack接口來反應狀态變化以外,還可以允許界 面子產品主動地調用_SingleTorrent.get_status來擷取關于該種子檔案下載下傳狀況的一些統計資訊。當然不要忘記調用 multitorrent.rawserver.listen_forever()開始這一切的排程,在建立multitorrent類時,它會在内部創 建一個rawserver。

    前面幾次都是直接上來就通過流程來分析程式,但是這次不一樣,因為用戶端的程式結構比較複雜,而且各種對象之間互相關聯,必須先對這些對象的功能有一個大緻的了解才好繼續分析,是以這一次将簡要得介紹一下用戶端的下載下傳程式中牽涉到的主要對象。

    Multitorrent:下載下傳任務管理的主對象,定義于BitTorrent/download.py中,内部維護了一個RawServer,且可以建立_SingleTorrent(與其定義于同一子產品中。)它内部還維護了其它對象。

    SingleportListener:管理網絡連接配接,是Multitorrent中的RawServer的網絡連接配接處理對象,定義于BitTorrent/Encoder.py中。

    FilePool:管理檔案池,定義于BitTorrent/Storage.py中,它可以保證同一時刻打開硬碟上的檔案數量在一個限定的值以内。

    RateLimiter:速度限制類。定義于BitTorrent/RateLimiter.py中,它可以控制全部種子檔案下載下傳時上傳的速度。

    Storage和StorageWrapper:儲存管理類。定義于BitTorrent/Storage.py和StorageWrapper.py 中,它們的作用是對程式的其它部分屏蔽掉種子檔案中第幾塊對應于實際硬碟上的哪個檔案的偏移量多少。即它對程式的其它部分提供諸如以下的這些服務,确定現 在本地有第幾塊,沒有第幾塊;應其它部分要求讀出第幾塊(其它程式就不用管第幾塊實際上是硬碟上的那個檔案),然後它們好發送到網絡上;其它部分從網絡上 得到一塊新的資料,叫它存儲到硬碟上。Storage和StorageWrapper都和_SingleTorrent一一對應。

    Choker:阻塞管理類。定義于BitTorrent/Choker.py中,它的作用是确定上傳的阻塞政策,即目前的連接配接中,阻塞哪些連接配接。與_SingleTorrent一一對應。

    Measure:速度測量器。定義于BitTorrent/CurrentRateMeasure.py中,它的作用是計算速率。在_SingleTorrent中定義了若幹Measure對象來計算各種速率(如上傳,下載下傳等)。

    RateMeasure:也是速度測量器。定義于BitTorrent/RateMeasure.py中,和Measure不一樣的地方在于它可以在初始 化的時候傳入一個表示還剩多少位元組的參數進去,因而它多了一個功能,那就是根據目前的速率,估算出預計剩餘時間。_SingleTorrent中定義了一 個RateMeasure。

    PiecePicker:塊選取器。定義于BitTorrent/PiecePicker.py中,進行“下一塊下載下傳哪塊”這件事情的決策工作,與_SingleTorrent一一對應。

    Downloader:下載下傳工作管理器。定義于BitTorrent/Downloader.py中,管理該種子任務中的所有下載下傳工作。因為一個種子檔案的下載下傳過程中要和很多個對等客戶打交道,是以需要建立若幹個連接配接。與_SingleTorrent一一對應。

    Encoder:連接配接管理器。定義于BitTorrent/Encoder.py中,管理該種子檔案任務中的所有連接配接(不管是主動連接配接到其它對等客戶上或者是其它對等客戶連接配接到本地),與_SingleTorrent一一對應。

    Connection:連接配接。定義于BitTorrent/Connecter.py中,一個該對象對應于一個連接配接。是以一個_SingleTorrent中包含了若幹個Connection對象(由Encoder負責統一管理)。

    SingleDownload:單一下載下傳。定義于BitTorrent/Downloader.py中,對應一個連接配接中的下載下傳。它與Connection 一一對應,且由Downloader對象産生(Downloader.make_download),每次新的連接配接建立時,Encoder都會把這個連接配接 儲存起來,并且産生一個SingleDownload對象。

    Upload:單一上傳。定義于BitTorrent/Downloader.py中,對應于一個連接配接中的上傳。和SingleDownload一樣,它與Connection一一對應,每次新連接配接建立時,由Encoder産生。

    Bitfield:位圖對象。定義于BitTorrent/bitfield.py中,用來表示一個比特數組。它典型用途是表示目前的種子檔案的下載下傳過程 中,本地有第幾塊,沒有第幾塊。出現在兩個地方,StorageWrapper,儲存本地的塊擁有情況資訊,以及SingleDownload中,儲存别 人的塊擁有情況資訊(以友善決定以後是不是要從他那裡下載下傳)。

    Rerequester:跟蹤請求發生器。定義于BitTorrent/Rerequester.py中,作用就是和跟蹤伺服器打交道,來擷取對等客戶的資訊。與_SingleTorrent一一對應。

    DownloaderFeedback:下載下傳任務狀态資訊搜集器。定義于BitTorrent/DownloaderFeedback.py中,它提供了 搜集下載下傳任務的狀态資訊的接口,可以完成狀态資訊的搜集以顯示給使用者。圖形界面程式或者其它的界面程式在調用_SingleTorrent的搜集資訊函數 時,最終還是要和該對象打交道(可以參閱_SingleTorrent.get_status函數的實作)。與_SingleTorrent一一對應。

5.2  用戶端源代碼分析(存儲管理)

    這一次分析BT的存儲管理。我們知道,BT把要共享的資源化分成統一大小的塊,并且在種子檔案中記錄每一塊的消息摘要值,以便在下載下傳時确定某一塊是否已經 正确下載下傳。而且在前面的種子檔案的制作過程中我們已經看到,除非是最後一塊,其它的塊大小都是相同的,是以很有可能出現在一個檔案的開始多少個位元組屬于某 一塊,然後從中間偏移多少位元組又屬于某一塊,或者在檔案比較小的情況下某一塊包含了若幹檔案等。而BT的存儲管理部分就對程式的其它部分屏蔽了這些差別, 即對其它部分而言,隻需要按照塊來進行存取。

    首先了看FilePool類,它在Multitorrent中定義,就是說,全局隻有一個。是以它可以保證多個種子檔案在下載下傳時硬碟上的檔案被打開的數量 限制在一定數量。内部維護了如下變量:handlebuffer為所有已經打開的檔案的清單,allfiles是一個字典,記錄所有的檔案的擁有者情況, 即哪個檔案是屬于哪個種子檔案。它的關鍵字是各個檔案的檔案名,值則是對應的_SingleTorrent對象。handles則是記錄檔案名與對應句柄 關系的字典,whandles還說明了哪些檔案是可寫的,注意,在whandles中,并沒有儲存對應句柄,即如果有一個檔案出現在handles中,可 以通過handles直接擷取其句柄,避免多次打開或者關閉檔案,而如果它出現在whandles中,那麼說明它還是可寫的。

    Storage是在每個_SingleTorrent中被定義。建立Storage需要一個檔案和它們對應的大小的清單,檔案的大小方面的資訊可以從種子 檔案的元資訊裡得到,另外,必須要把種子檔案的元資訊的檔案清單中的每一個項目都加上在硬碟中實際儲存的目錄,以便可以直接對應到某個具體的檔案。 Storage在建立的時候建立檔案名和全局的位元組之間的映射關系,即清單ranges,該清單的每一個清單項是一個三元組,起始偏移,結束偏移,檔案 名。它的意思是種子檔案的内容中,從第幾個位元組到第幾個位元組是屬于哪個檔案。另外,假設種子檔案中對所有資訊的内容進行了塊的劃分,設這個塊長為 piecelen,那麼每一塊還有一個位元組偏移,如從第0個位元組到第piecelen(不含)個位元組是屬于第一塊,接下來屬于第二塊等。是以這個 Storage類就要解決BT的下載下傳按塊進行和硬碟中按檔案進行存儲之間的沖突。這裡再提一下,之是以把piece翻譯成塊,是因為後面的BT的下載下傳過程 中,還要把每一塊再切分成若幹的slice,而我習慣于把slice翻譯成片。

    在Storage中,有兩個私有函數_intervals和_get_file_handle,它們給read和write提供了兩項重要的功能,而 read和write是Storage對外提供的重要接口。_intervals的任務是提供一個全局的偏移量和長度,傳回一張表,說明要對這些資料進行 通路應該分别通路哪些檔案的第幾個位元組開始的多少位元組。這樣,在read和write裡面就可以用for ... in _intervals(xx,xx)了。而_get_file_handle則是擷取一個檔案的實際句柄,以便對其進行讀寫,在Storage中擷取檔案 的句柄要用一個函數來處理的原因是必須考慮到檔案打開數量的要求限制,因而在_get_file_handle中要和FilePool打交道,在打開一個 檔案句柄的同時,要對FilePool内部的一些變量進行維護。

    Storage還有一項功能就是讀寫一個“快速恢複”狀态檔案。它隻負責讀寫一部分,這個檔案中還有一部分資料由StorageWrapper類來進行讀 寫。由Storage類負責讀寫的部分是檔案頭,包括'BitTorrent resume state file, version 1'字樣,和總的資料量,以及各個檔案的大小和更改時間等。

    StorageWrapper類則是在_SingleTorrent._start_download中定義,提供的接口要更加進階。例如,它提供按照某 一塊來進行通路,然後在内部通過把塊号乘以一塊的大小來得到實際的位元組偏移量,然後讓Storage來進行讀寫。另外,它維護了一張本地擁有哪些塊的比特 數組have,可以友善決策。還有兩個表示塊的存儲狀況的數組,places和rplaces,它們的意思是資料的第幾塊存儲在硬碟上的第幾塊以及硬碟上 的第幾塊存儲的實際上是資料的第幾塊。這個數組基于兩部分資料的抽象:第一部分的資料抽象是把種子檔案中所表示的内容(即共享的資源)看成是一塊連續的數 據,這塊資料有若幹塊。第二部分的資料抽象是把存儲在硬碟上的檔案,看成是一塊連續的資料,即連續的存儲空間,也分成若幹塊。當下載下傳任務全部結束後,應該 有對于0到self.numpieces-1,有places[i]=rplaces[i]=i。而在下載下傳過程中,由于采取了一定的政策,不一定是先下第 0塊,再下第1塊等,是以在這個過程中有可能places[i]和rplaces[i]中的值不等于i。

    我們先來看一下比特數組,即Bitfield,它定義在BitTorrent/bitfield.py中。它用最節約的空間完成了比特數組的存儲,即比特 數除于8的位元組數。另外,它實作了__setitem__和__getitem__,這樣就可以直接對have[i]進行讀寫操作來完成值的操作。注意到 在__setitem__的實作中有assert val,這就意味着隻能把數組中的某一項指派成1。這項功能比較适用于表示塊擁有的狀況,即某一塊隻能從沒有到有,不能“得而複失”。

    StorageWrapper還有一項很重要的功能就是對每一塊資料再次細分成若幹個slice,而一個slice就是兩個對等客戶之間通過網絡進行資料 交換的最小機關。在此基礎上,它要負責生成請求,inactive_requests儲存的就是所有可能的請求。在看這部分代碼時,注意1和l的差別,在 初始化時,inactive_requests[i]的值是1,表示某一塊還沒有(因而可以為此生成網絡請求),當得知某一塊已經擷取 時,inactive_requests[i]的值變為None,具體得知某一塊已經擷取時要進行的操作為markgot,它的意義是第piece塊在硬 盤上的存儲空間中的第pos塊被發現。另外,初始化的時候調用_check_partial和_make_partial檢查某個具體的塊,看看有哪些 slice還需要下載下傳。把這些請求放到inactive_requests中,以後當程式其它部分決定要開始下某一塊時,StorageWrapper為 其生成相應的網絡請求的參數(第幾塊,偏移多少,請求多少長度的資料),new_request即完成這項工作,另外piece_came_in和 get_piece提供資料的讀寫操作,調用它們的時候都要指定index(第幾塊),begin(塊内偏移),length(長度,在 piece_came_in中是piece,即資料本身,可以直接得到它的長度)。

    最後,關于存儲管理這部分,還有一點需要提到,那就是早期的某個BT版本是在下載下傳剛剛開始的時候就申請好相應的硬碟空間。而現在則是随着下載下傳的進行檔案逐 漸增大。但是下載下傳的順序很可能不是先下第0塊,再下第1塊這樣,是以在檔案中存儲的順序也不一樣,這樣當新的下載下傳資料到來存儲到硬碟上時,很可能就要對起 進行調整,盡可能讓它們“對号入座”。_move_piece函數就能進行資料的移動,而參考piece_came_in開頭部分對 _move_piece調用的代碼就可以了解BT在下載下傳過程中逐漸使塊的順序“對号入座”的這個過程。

5.3 用戶端源代碼分析(從開始到連接配接建立階段)

    這一次開始恢複按照過程進行描述,即從Multitorrent.start_torrent函數的執行開始。

    通過前面的分析,我們知道當Multitorrent.start_torrent被調用時,一個新的種子下載下傳任務就開始了。這個函數本身很簡單,就是創 建(并傳回)一個新的_SingleTorrent對象,然後讓其start_download方法開始排程。start_download這個函數一開始看上去有點奇怪,其實這是作者設計的一個小技巧。python中的yield關鍵字可以使一個函數傳回一個值,但是它的内部執行狀态仍然儲存,這樣下次 調這個函數的時候,這個函數就繼續從那裡往下執行。可以用諸如it = self._start_download(*args, **kwargs) 這樣的形式來确定一個疊代器,注意在執行這一句的時候,_start_download并未得到執行。要使包含有yield的關鍵字的 函數得到執行,隻需要反複調用it.next(),這将傳回每次yield出來的值,當函數執行到結尾時,将會抛出一個StopIteration異常, 通過捕捉這個異常就可以知道函數以及執行完畢。在start_download中幹了以下事情,把一個函數指派到_contfunc,并且執行了它一次。 這個函數的實際内容就是開始執行_start_download,為什麼要這樣費一下周折呢,這樣做的目的就是為了在合适的時候分出一個線程。到目前為 止,程式還是隻有一個主線程在運作。繼續往下看_start_download函數,根據元資訊的檔案清單和儲存到硬碟上的位址,整理出一個實際的檔案列 表,可以直接對它們進行存儲。然後建立一個新的Storage對象,它需要的檔案名和大小的元組清單可以通過zip函數得到,這個函數的功能是從第一個參 數(清單類型)中擷取第一個元素,然後和第二個參數的第一個元素組成一個元組,再将第一個參數和第二個參數的第二個元素組成一個元組等,最後變成了一個列 表。然後進行“快速恢複”的檔案檢查。接下來注意到函數hashcheck,通過建立一個新的線程,然後讓它開始運作,接下來yield None,注意,從這一句開始,其實就已經傳回了。hashcheck函數将在新的線程開始執行,我們來看看hashcheck函數中都幹了什麼,主要就 是建立了一個StorageWrapper類,它初始化時就已經對硬碟上有的内容确定下來了。然後,它執行了_contfunc()!沒錯,執行它的效果 就是從yield None後面的部分繼續執行下去了,但是,這時已經是在另一個線程中。接下來建立一個Choker,以及一些速度測量器。然後要建立一個 PiecePicker,初始化完成後,還要告訴PiecePicker哪些塊已經擁有了(PiecePicker.complete)以及哪些塊已經下 了一部分(PiecePicker.requested)。下面建立一個Downloader對象,但是對于Upload,隻是定義一個函數 make_upload,它能夠随時生成一個Upload對象。然後建立一個Encoder對象,注意它把Downloader和make_upload 做為參數,從結構上來說,它們就被綁定到一起了。接下來要用add_torrent把一個種子檔案(以infohash為關鍵字)和它的Encoder綁 定到一起,這樣,當收到其它的對等客戶的連接配接的時候就能夠知道對方要下載下傳的是哪個種子檔案了。最後建立Rerequester和 DownloaderFeedback這兩個對象。

    最後執行Rerequester.begin,啟動它,讓它開始和跟蹤伺服器互動,然後就可以調用feedback接口的started函數,意思就是說,我們已經開始了,至于是用圖形界面還是文字界面向使用者表示這一事實那就是feedback接口的事情了。

    Rerequester。它的作用即為向跟蹤伺服器要對等客戶的資訊,前面通過對跟蹤伺服器的代碼分析已經對用戶端和跟蹤伺服器之 間的通信協定有了一個基本的了解。我們稱和跟蹤伺服器進行一個http請求并擷取它的回應資料的過程稱為一次釋出 (announce),Rerequester的begin函數能夠保證自己每隔60秒_check一次。我們來看_check一次要做什麼:首先要保證 兩次釋出的時間間隔不能過短,另外要根據自己的peerid制作 url(_makeurl:http://xxx.xxxtracker.xxx:xxxx/announce?info_hash=xxxx& peer_id=xxxx&port=xxxx&key=xxxx),根據不同的情況調用_announce進行一次釋出。給 _announce的參數的意義是'event'參數的值,這些'event'的意義可以在跟蹤伺服器的代碼分析中看到,它們确定了下載下傳的不同的階段。因 為平時也還需要經常補充一些對等客戶的資訊,是以_announce會經常被調用。它的主要任務是對url進行進一步的加工,計算出目前釋出時所用的 url,儲存在s中,然後用一個新的線程開始執行釋出,使用新的線程的原因是不希望到跟蹤伺服器的網絡阻塞影響到程式的其它部分的執行。 _rerequest就基本上可以隻管發出這個http請求了,當然,它開始的部分代碼是要排除一些自己的外部IP和實際IP不相同的這種情況。 Request是zurllib中的子產品,可以很輕松地發送一個http請求,然後擷取傳回的資訊。根據是否出錯來決定調用_postrequest的情 況。這裡出錯僅僅是http請求本身發生錯誤,如網絡問題等,跟蹤伺服器也可能會傳回一些其它的錯誤資訊,我們可以在_postrequest中看到。

    _postrequest首先便是檢查是否有錯誤資訊,然後把data進行bdecode,這個過程我們已經很熟悉了。接下來用check_peers檢 檢視這是不是一個規範的對等客戶資訊資料,check_peers在BitTorrent/btformats.py中定義,btformats.py還 有其它的檢查資訊格式的函數。下一步是檢查r中有沒有關鍵字'failure reason',如果有的話,那就是說到跟蹤伺服器的網絡沒有問題,但是跟蹤伺服器傳回了其它的失敗原因,這樣還是一種失敗的情況。下面就是把r中的關鍵 字為'peers'部分的資料解析出來了,這部分傳回來的資料有可能是緊湊的字元串也有可能是一個字典,在跟蹤伺服器的代碼分析中我們可以看到這一點。最 後就可以調用connect函數試圖逐個得與對等客戶建立聯系了。connect函數實際上是Encoder.start_connection。

    下一次就可以開始分析兩個對等客戶之間的通信協定了。

5.4 用戶端源代碼分析(對等客戶的連接配接建立及其握手協定)

    上一次我們分析到了一個客戶是如何得擷取到對等客戶的資訊,現在終于要開始建立連接配接了。這一次我們将分析兩個對等客戶之間的連接配接的建立以及連接配接對象為它們之間通信提供的基礎架構設施。

    Encoder.start_connection建立到某個對等客戶的連接配接。dns參數是IP位址和端口号,id是對方的peerid。首先檢查對方的 IP位址是否在banned清單内,如果在,直接傳回,就是說不會再和對方建立連接配接。這就是通過一個黑名單的機制,避免和某些對等客戶連接配接。這個黑名單也 有一些生成的政策,後面我們可以看到,通常是發現某個對等客戶傳來的錯誤資料過多就将其加入到黑名單中。然後,當然對方id不能等于自己的id,以及在已 有的連接配接中進行查找,不能重複連接配接。然後是檢查連接配接數,如果連接配接數大于某個配置值,那麼将這個連接配接的資訊暫存入spares清單,日後再取出來。接下來就 可以讓RawServer進行網絡連接配接了。如果網絡連接配接成功建立,那麼一個新的Connection對象就會被建立,并且該網絡連接配接的資料處理對象 (data_came_in)也會被交給這個Connection對象。

    現在我們來看看網絡的另外一頭,就是說對方收到連接配接後會執行什麼代碼。由于在Multitorrent中定義了一個SingleportListener 來偵聽本地端口,也就是說,所有的外部網絡連接配接的處理對象都是這唯一的SingleportListener,那麼很自然,對方收到連接配接 後,SingleportListener.external_connection_made會被調用。注意到SingleportListener和 Encoder都定義在BitTorrent/Encoder.py中,看來作者是認為它們關系比較緊密。 SingleportListener.external_connection_made中所做的事情也是建立一個Connection對象,并且完成 網絡連接配接的資料處理對象的重定位工作。

    兩種不同的情況都會有一個新的Connection對象被建立,但是初始化它們的參數不太相同,另外它們都會被維護到一個字典中,Encoder中的這個 字典記錄了某個種子檔案下載下傳任務的所有連接配接,而SingleportListener中的這個字典記錄了所有外部來的連接配接(就是說,還不知道應該把它們交 由哪個Encoder進行管理)。

    現在我們來看Connection對象被建立時所做的初始化工作。第一個參數是encoder,就是說該Connection對 象屬于哪個encoder管理,這個參數也有可能是SingleportListener,第二個參數就是由RawServer建立的 SingleSocket對象,它通常是用來作為所有連接配接的字典中的關鍵字,另外可以用它來完成具體的網絡讀寫操作。第三個參數是id,指的是對方的 peerid,如果是外部連接配接(SingleportListener處理的),那麼這個參數是None,即還不知道對方的peerid,最後一個參數是 一個布爾值,說明該連接配接是本地發起的(Encoder.start_connection)還是外部連入的 (SingleportListener.external_connection_made)。開始的初始化工作基本上是對一些變量的初始化,注意到 _reader變量,_read_messages()函數是一個多次使用了yield關鍵字的函數,是以這裡_read_messages沒有被執行, 然後下面的_next_len的指派部分,使_read_messages()執行了一些,即開始的yield 1。這樣_next_len就等于1,且_read_messages()函數執行當機在了這裡。最後,如果是主動連接配接的話,那麼就往網絡上發送後面的那 一串東西。如果不是主動連接配接就不用了。發送的那些資料就是BT通信協定的握手部分的内容。

    現在就可以來看Connection.data_came_in函數是如何處理到來的網絡資料。_next_len表示的是下一條完整的消息的長 度,_buffer是緩沖區中暫存的資訊(因為還沒有達到_next_len的要求),_buffer_len則是緩沖區中的資訊的長度。每一次試圖從網 絡資料s中得到組成下一個完整的消息的資料,是以首先計算長度,_next_len-_buffer_len說明要從s中得到多少資料。如果s中沒有這麼 多資料就把s中的資料暫存到緩沖區中,然後就傳回。這樣下一次調用data_came_in時就可以繼續組建需要的資料了。如果s中有足夠的資料,則将其 組成合适的長度(_next_len),放入_message中,以友善處理。然後讓_read_messages()繼續往下執行。如果s中還有資料, 則while循環要繼續進行。我們看到,在data_came_in的這種設計架構下,_read_messages()函數每次yield一個值,當它 接下來恢複執行時,_message中就已經有它要的值了。

    這樣,_read_messages()就可以專門處理協定。我們現在就可以對比本地發起的網絡連接配接的初始化過程中發出的那個握手字元串來分析握手協定。 首先yield 1,然後一個位元組的資料進入了_message,這就是chr(len(protocol_name)),協定名稱的長度。通過把這個字元還原成整數,看 它是否和協定名稱相同。接下來yield len(protocol_name),然後進入_message的資料就是protocol_name,檢檢視它是否是'BitTorrent protocol',然後yield 8,這是8個位元組的保留串,不用進行任何處理,繼續yield 20。這是download_id的值,encoder.download_id是什麼呢?就是種子檔案的infohash。然後檢查 self.encoder.download_id,如果是None,那麼說明這個Connection對象是SingleportListener建立 的,就是說這是網絡中來的連接配接,是以程式運作到這裡就可以做的一件事情就是看看這個infohash到底是本地的哪個下載下傳任務,更準确的說,這個 Connection對象應該交給哪個Encoder進行管理。是以它調用了encoder.select_torrent(其實就是 SingleportListener.select_torrent),這個函數從維護的torrents字典中根據infohash查找對應的 Encoder,然後讓Encoder.singleport_connection進行Connection的交接。在 Encoder.singleport_connection中做的事情包括檢查對方的IP是否在banned清單中,否則拒絕其連接配接。然後将 Connection對象添加到自己維護的字典中,并且将其從SingleportListener的字典中删除,然後将Connection的 encoder指向自己,這樣這個Connection就正式歸這個Encoder管理了。再傳回到_read_messages()函數中,對 encoder.download_id的檢查就可以證明以上過程是否成功完成了。下面一個elif則是主動的連接配接,對方傳回的download_id在 _message中,如果和自己的encoder.download_id不符,則中斷該連接配接。下面檢查是否是本地發起的連接配接,如果不是本地發起的連接配接, 則也向對方發送握手協定(這樣對方的_read_messages()函數也可以開始運作了)。接下來yield 20,得到peer id。這是對方的ID,Connection中保留的id都是對方的ID,自己的ID保留在Encoder中。下面的這一段代碼對得到peer id進行處理,如果需要則保留到自己的id變量中,并且根據自己的Encoder的Connection字典進行檢查,以避免兩個對等客戶在同一種子檔案 下載下傳任務中的重複連接配接。

    在握手協定的最後一步調用了Encoder.connection_completed,說明這個連接配接建立成功,可以正式進行資料的互動了。在這個函數中 做的工作就是為這個Connection生成一個Upload和SingleDownload對象,并且把這個Connection交給Choker進行 管理。

    回到_read_messages()中,下面我們可以看到,握手協定已經成功完成,開始傳送其它資料。每一條消息都分割成一個四位元組長的長度和消息本身,是以while循環中不斷的yield 4和yield l,然後_got_message來處理每個消息。

    通過這一次的分析,我們知道了兩個對等客戶之間的連接配接的握手協定,以及Encoder, Connection, Upload, SingleDownload這些基本對象在連接配接建立時的基本關系。下一次就可以開始分析BT通信協定中的其它部分。

5.5 用戶端源代碼分析(對等客戶連接配接中的阻塞管理)

    從上一次我們的分析可以看出當對等客戶建立連接配接後,通過握手協定交換資訊,這樣對于每個連接配接都有一個Connection對象,然後有一個 SingleDownload和Upload與其對應。這一次将從握手協定完成後繼續分析,然後介紹Choker,阻塞政策控制器的工作原理。

    SingleDownload在初始化時沒有做什麼特殊的操作,僅僅是建立了一個BadDataGuard對象和它對應。這個對 象是用來統計壞資料的資訊,以便确定壞的對等客戶的。而Upload對象在建立的時候,如果自己已經有部分下載下傳資料,就把自己的塊擁有情況發送出去 (send_bitfield)。現在就可以來看send_bitfield,我們可以看到在Connection中定義了不少send_xxx函數用來 發送某種消息,并且在Connection對象定義之前,定義了那些消息的類型的對應的常數項。另外這些send_xxx函數大都調用了 _send_message,它的作用就是在要發送的消息前面添加上它的長度(4個位元組),然後發送出去,如果有必要,則放入隊列中稍後發送。這樣,每一 次_got_message得到的就是消息的内容了。

    現在來看_got_message,它直接取第一個位元組就行了,這就是消息的類型。以後我們可以再檢查其它類型的消息,現在我們直接看elif t == BITFIELD這部分,得到對方的塊擁有狀況比特數組後,讓自己的download對象記錄下來,即調用 SingleDownload.got_have_bitfield()。這個函數首先檢查自己是否下載下傳完成,然後檢查對方的比特數組中"假"值的個數, 如果自己下載下傳完成了且對方的比特數組中"假"值為0,則說明對方也下載下傳完成了,而兩個都在做種的對等客戶之間的連接配接是沒有意義的,可以關閉它。然後 self.have = have這一句記錄下對方的塊擁有情況。是以前面提到StorageWrapper儲存自己的塊擁有狀況,而對應于每個Connection對象的 SingleDownload對象中則保留了對方的塊擁有狀況。然後讓PiecePicker記錄下别人有一塊(got_have,complete記錄 的是自己有一塊,在_SingleTorrent的代碼中可以看到)。下面的這個endgame則是一種政策模式,即表示進入收尾階段,它檢查自己所有的 網絡請求all_requests(後面還會分析到它),如果對方有某一塊(再次注意,這裡self.have[piece]是對方有第piece塊,而 不是自己),那麼發送一條消息,send_interested()。表示說我對你(所擁有的内容)感興趣。而如果沒有進入收尾階段,則隻是檢查自己有那 塊沒有而對方有,如果有的話,則send_interested()。注意send_interested()調用一次,對方知道這個意思就行了。

    看Connection._got_message中得到這個消息後怎麼處理。是self.upload.got_interested()。這個函數中 維持自己的interested變量為真值,然後通知choker這件事情,choker.interested則選擇是否要進行一次 _rechoke()。

    現在應該注意到choked和interested這兩個變量,這兩個變量的值的意義分别是是否阻塞和是否感興趣,它們對下載下傳起到直接開關的作用。在每個 SingleDownload和Upload對象中都有這兩個變量。在初始化時,choked都為真而interested都為假,這樣就不會有實際的内 容(即種子檔案的共享資源)在流通,而要有實際的内容流通必須這兩個變量的值和它們初始化時的值剛好相反才行,也就是說,隻有當一方對另外一方感興趣,而 對方又沒有拒絕你(choked=false)的時候,你們之間在這個方向才可能會存在實際的下載下傳流量。另外這兩個變量在網絡連接配接的每個方向都是保持一緻 的,即Upload中的這兩個變量和連接配接另外一頭的SingleDownload中的這兩個變量保持一緻,如果有某個變量發生變化,要發送消息給對方,讓 對方能繼續保持一緻。注意這裡的保持一緻指的是網絡連接配接的兩頭,而不是本地的Connection對象對應的SingleDownload和 Upload,即本地的Connection的SingleDownload和對等客戶的Upload保持一緻,而本地的Connection的 Upload和對等客戶的SingleDownload保持一緻,而在同一個連接配接中,下載下傳和上傳的兩個方向有可能不一緻,即一個方向阻塞了,另一個方向還 在下載下傳。

    前面已經注意到,interested這個變量的改變很容易,隻要發現對方有自己沒有的塊,就會發送這條消息,而choked這個變量的控制就有一定的策 略了。Choker就控制所有的連接配接(_SingleTorrent級别)的阻塞。它在初始化時即保證_round_robin每十秒種執行一次,而每次 有連接配接進入時,用connection_made來進行登記,Choker中維護了所有連接配接的清單,且這個清單是故意打亂順序的。在BT的控制政策中,我 們還可以多次看到随機打亂順序的情況發生,因為有時随機數就是最好的政策。在_round_robin中,首先檢查是否已經完成,如果完成則調用 _rechoke_seed(),按照自己已經開始做種的情況進行處理。而計算count%3的餘數就可以保證_round_robin執行三次這部分代 碼會執行一次,因為count隻有在_round_robin中會被加一。這部分代碼就是選擇一個choked和interested同時為真的連接配接放到 清單的開頭(不要讓喜歡你的人等待太久)。

    在_rechoke()中,首先選擇出一些符合解除choked狀态的連接配接(條件是interested和下載下傳方向的is_snubbed,即目前時間是 否距離上次下載下傳到東西的時間過短),然後把所有的這些連接配接按照下載下傳的速度排序,由于前面增加了一個負号,是以下載下傳速度最塊的排在前面。然後根據配置項中的 最大上傳數計算一個配額,這個配額不能等于最大上傳數,最多隻能對于這個數減一,從這個清單中取出排名前面的若幹位,設定一個mask标志。下面計算出最 小上傳數,count。count至少要為一,如果最小上傳數比前面的配額還大,那麼count也相應增大。下面就是解除choke狀态了,首先mask 為1的,無條件解除,如果mask不為1,但是count還大于0,那麼用掉一個count,解除choke狀态,其它的連接配接,一律choke掉。

    Upload的choke和unchoke都是在确定狀态改變的情況下,開始向對方通知這一消息。

    這一次結合連接配接中開始的部分消息互動過程,介紹了choker這一阻塞政策管理器的工作原理。下一次将開始介紹在連接配接的雙方的已經同意交換資料(choked為假而interested為真)時的情況。

5.6 用戶端源代碼分析(下載下傳過程中的塊選取政策)

    上一次介紹了對等客戶之間在連接配接建立後的一些動作,以及BT中的阻塞控制政策。這一次将介紹當某個連接配接終于暢通時,雙方的資料互動,也以此為基礎介紹BT中另一重要的政策控制器PiecePicker。

    Choker在選擇了解除一個連接配接的阻塞後,Upload.unchoke()将會執行,Connection對象的send_unchoke()也在此 被執行。當網絡的另一端收到這條消息後,它對應的SingleDownload.got_unchoke()将會開始進行處理。它再檢查自己的 interested狀态,如果自己也感興趣的話,那麼就用_request_more()開始向對方請求資料了。

    _request_more()可以給一個indices作為參數,這個參數是一個清單,意思就是說優先下載下傳号碼在這個清單中的塊。如果這個參數為 None,那意思就是說你自己看着辦吧,覺得下哪塊合适就下哪塊。首先檢查自己的active_requests,就是目前連接配接中已經發出去的請求,如果 已經發出去的請求太多了(而還沒有資料傳回),就暫時不發出新的請求了而是直接傳回。下面檢查endgame,如果已經進入這個階段則按照這個階段的方式 去下載下傳(fix_download_endgame(),收尾階段特殊方式下載下傳)。

    接下來就開始生成請求了,首先檢查indices,如果是None,那麼讓PiecePicker來挑一塊,否則,逐個的檢查indices中的值,如果 這個号碼的塊對方有(have[i])而自己又想要(do_I_have_requests(i)),那麼就是它了。PiecePicker如何進行塊的 選取的政策我們稍後再分析,現在我們知道的就是它已經決定下載下傳某一塊了。然後要檢查interested,如果有必要,還要通知一下對方。下面一段就是不 斷向StorageWrapper要網絡請求的參數,new_request根據自己在硬碟上的某一塊的擁有情況,不斷得傳回塊内相對偏移和長度。在這 裡,我們可以看出,對等客戶之間要求傳輸實際的資料的請求有三個參數,即第幾塊,塊内偏移多少,長度多少。而這個長度是根據配置檔案中的參數決定的,通常 就是一個slice,它要能一次下載下傳完。當然,一塊的長度不一定是slice的整數倍,是以最後一個slice的長度要短一些,不過,這些細節在 StorageWrapper中已經處理好了。從StorageWrapper得到請求後,就把它加到自己的active_requests中,然後讓自 己的Connection對象去send_request()。現在我們也應該更加清楚active_requests和 inactive_requests的意義了,即平時StorageWrapper根據實際情況,準備好inactive_requests,然後在 SingleDownload對象中請求發出時,把它們逐漸轉移到自己的active_requests中。

    在兩個while循環的下面,檢查active_requests,意思就是說如果經過以上的所有過程,如果active_requests還是空的,那 麼說明什麼呢?隻能說明對方根本就沒有(或者說曾經有,但是現在已經沒有了)自己感興趣的資料,而如果自己還是interested的話,要調用一個 send_not_interested(),意思是我不再對你感興趣了。下面檢查lost_interests中的值,這些都是在下載下傳過程中曾經是自己 想要的,但是現在已經不想要了(主要原因是自己已經擁有了)。接下來這個for循環的意思就是檢查所有的SingleDownload對象,告訴它們某一 塊已經有了,不用再去下了,而且有些SingleDownload要是以發出NOT_INTERESTED。最後再次檢查是否進入endgame階段,如 果是,則按照這種階段的行為進行處理。

    現在我們就可以來研究PiecePicker這個塊選擇政策控制器的行為了,從前面的分析我們知道,每個PiecePicker對應一個 _SingleTorrent,使用它時經曆了以下幾步:首先是初始化,然後根據自己已經有的塊,把它告訴給 PiecePicker(complete(i)), 以後就不要從這中間選了,還有就是當一個SingleDownload對象擷取對方的塊擁有狀況位圖 時,也要告訴PiecePicker(got_have(i)),意思是這一塊有人有了。最後當需要PiecePicker做出選擇時,隻要調用其 next函數,它需要一個判斷函數(_want),以及一個對方是否是種子的标志(self.have.numfalse == 0),_want函數就是這樣的一個函數,當PiecePicker選了一塊後,要拿給它檢查,看看這一塊是不是它确實想要的,如果不是的 話,PiecePicker會重新選擇。而_want()函數的判斷标準很簡單,那就是别人有而自己又想要的。

    PiecePicker的初始化工作主要是對自己的内部變量進行。這裡要解釋一下這些變量的作用,這樣能夠更加友善地對後面的部分進行了解。 numpieces,總的塊數。interests是按照擁有者的數量排序的塊清單的清單,就是說,它是一個清單,清單中的第0個元素是所有的自己感興趣 而沒有人有的塊的清單,第1個元素是所有的自己感興趣而隻有一個人有的塊的清單,等。pos_in_interests,就是每一塊在interests 中的對應元素所表示的清單中的位置,如果某一塊比如說i,自己已經有了,那麼pos_in_interests[i]的值沒有意義。 numinterests的值就是某塊有多少人擁有(不包括自己),以上三個變量保持這樣的關系:如果一塊i,自己沒有,那麼 interests[numinterests[i]][pos_in_interests[i]]=i。have是一個布爾數組,表示自己已經有那塊, 在初始化完成後,它應該和StorageWrapper中的實際情況保持一緻。crosscount則是一個統計情況數組,即有多少塊沒有人擁有,有多少 塊有一個人擁有,等,自己擁有的某一塊也在這裡參加計數。numgot,已經得到的塊數。scrambled,一個包含從0到numpieces-1的序 列,但是被随機打亂了。

    現在來看PiecePicker.complete,即自己有了某塊,首先have中的值要設定,然後從numinterests中查到自己原來有多少人 擁有,把crosscount中對應的項減一,然後把它下一項加一,如果沒有下一項,那麼就在後面添加一項。由此我們可以看到,crosscount數組 是逐漸增大的。然後它做的事情是把interests中的對應的項删除掉,因為它已經不在自己感興趣的範圍内了,其它幾行代碼是為了保持這些變量值的一緻 性。然後試圖從started和seedstarted中删除這一塊(如果沒有這一塊也無所謂,什麼也不用做)。

    PiecePicker.got_have,處理的情況是别人有了某一塊。首先還是保持crosscount的一緻,然後處理interests清單。調 用_shift_over把piece從interests清單中的一個元素轉移到另一個元素(同時還要保持其它變量的值的意義的一緻性)。 _shift_over做的事情就是從第一個清單中删除一個元素,然後将其插入第二個元素随機的位置,同時維護pos_in_interests值的意 義。

    PiecePicker.requested,哪一塊已經開始下載下傳了,這個在SingleDownload中會被調用,它隻是維護兩個清單,started和seedstarted。

    PiecePicker.next,可以說是PiecePicker中提供的最重要的功能,選擇一塊進行下載下傳。它選擇的第一條原則是,已經開始下載下傳的優先 把它下載下傳完(return choice(bests)及其前面的代碼)。它檢查選擇的兩個數組,根據對方是否是種子選擇一個數組。然後在所有的這個數組中選擇出自己想要的,檢查它 們的numinterests,即擁有此塊的人數,選出擁有人數最少的塊,放入bests中,如果有并列的,則添加到bests,是以在這裡結束 後,bests中的元素是所有正在下載下傳的且自己想要的塊中擁有人數最少的塊的清單,那麼就從中間随機選擇一個傳回即可。選擇的第二條原則是,當自己擁有的 塊數少于一定的數量時,随機選擇自己想要的塊進行下載下傳(第一階段結束後的那個if塊),是以它用到了那個scrambled清單,而當自己所擁有的塊數超 過一定的值(config['rarest_first_cutoff'])後,執行第三階段的方案。選擇的第三條原則是,優先選擇下載下傳擁有的人數最少的 塊,我們看到,它從interests中第1個元素開始檢查,選擇最先找到的自己想要的塊,第0個元素不用檢查,因為沒有人擁有的塊肯定下載下傳不到。我們可 以看出,它的選擇原理是比較簡單但是又很有效的,優先下載下傳擁有人數最少的塊就能夠保證所有的塊能夠在最短的時間内盡可能得讓更多的人擁有,換一個術語說就 是能盡快提高要下載下傳的内容的副本率。

    這一次我們分析了對等客戶在下載下傳的過程中,如何進行下載下傳的政策控制。下一次将分析收到對方的下載下傳請求後的處理方式等。

5.7 用戶端源代碼分析(實際資料的傳輸及其速率限制政策)

    上一次分析了下載下傳過程中如何進行下載下傳某一塊的選取。這次分析在收到對方的下載下傳請求後程式的處理行為。

    首先,仍然看Connection._got_message中收到請求消息的處理代碼,即elif t == REQUEST:後面的部分。首先檢查這個消息是否符合格式,它的長度必須是13(1個位元組的消息類型加上3個4位元組整數,分别代表塊的位置,塊内偏移, 請求長度),以及塊的位置必須小于自己擁有的總塊數,然後由Upload.got_request進行處理。在Upload.got_request中, 首先檢查狀态,如果對方還沒有聲明interested就或者申請的長度大于自己的max_slice_length,即一次能夠發送出去的最長的資料 塊,那麼中斷連接配接,由此可見,在BT通信協定中,要先聲明interested才可以向對方請求資料。然後在自己的Connection沒有發送 choke時就可以發送資料了,但是這裡發送資料它并不是直接發送資料,而是把請求保持在自己的buffer中,然後讓RateLimiter把自己的 Connection加入到它的隊列中。

    RateLimiter,在Multitorrent中定義,作用是對全局的速度進行限制。由于BT通信協定中,隻有發送實際的資料會需要比較多的帶寬, 因而也隻有在這種情況下會需要用RateLimiter來對其進行限制。現在我們可以注意到在每個Connection中還有一個next_upload 變量,它在其它地方都沒有用到,僅僅是在這裡,它的作用就是把若幹個連接配接通過這種方式組成一個連結清單。next_upload的類型是 Connection,不是Upload,這裡要注意。我們看到RateLimiter.queue函數中進行的就是資料結構中很常見的連結清單操作,其中 self.last指向了上一個Connection對象,插入新的Connection對象時,last會指向它。另外如果原來隊列是空的話,那麼開始 try_send,否則就不用做什麼,因為try_send會檢查隊列,逐個從中取出連接配接對象,并且發送資料。try_send中首先計算 offset_amount,這個值的意義就相當于可以發送多少位元組,也就是一種“配額”,它的值小于0就可以繼續發送,發送了一些位元組後增加相應的字 節,如果大于0,那麼就停下來,把發送的任務往後延一段時間。其中如果check_time标志為真的話,那就清0,以前的時間不算,重新開始計算。配額 每次減少的位元組數是上一次的時間(self.lasttime)和這次的時間差乘以upload_rate,這也很好了解,隔了這麼些時間,又可以上傳若 幹位元組了。下面的while循環就是在配額還有的情況下,不斷調用send_partial函數進行資料的發送,然後發送完畢後,檢查該連接配接是否已經暫時 沒有發送需求了(即傳回的實際發送的位元組數是0或者連接配接還未重新整理,即flushed),如果該連接配接暫時沒有需求,則将其從連結清單中删除。但是無論它還有沒有 需求,接下來發送的都是連結清單中的下一個元素。另外,在python中允許while循環後跟一個else語句,它被執行的條件是循環正常結束,即因為 while的循環條件不滿足而結束循環,而當使用break來退出循環時,這個else語句後面的内容是不會被執行的。在這裡,while的結束條件是配 額用完。那麼意味着還有資料要下載下傳,那麼就等待一段新的時間繼續執行此任務,等待多久呢?它等待的時間是剛好能把配額又降到0的時間。另外,由于直接執行 可能會有一些延遲,是以,這裡肯定可以保證下次運作時有上傳配額。另外這個while循環中唯一的break隻有在發現隊列已經清空的情況下被執行到。

    Connection.send_partial,負責實際發送資料。它有個參數bytes,指定了它最多隻能發送這麼多資料。 _partial_message是它維護的分塊消息變量,如果它不能一次把它發送出去,就把它截斷,然後下次發送。首先看看它是否是空的,如果是,先從 Upload處擷取一塊代上傳的消息(get_upload_chunk),這個函數的做法是從自己的buffer(Upload.buffer,前面提到,表示自己要上傳的請求,但是當時隻是把自己的連接配接對象加到RateLimiter的隊列中)中擷取一塊請求,然後讓 StorageWrapper.get_piece去實際得按照要求把某一塊的某一部分讀取出來,然後再更新一些速率統計對象的值,最後把這塊資料傳回。 回到send_partial中,得到資料塊後,把_partial_message制造出來,做成可以直接往網絡上發送的那種格式。下面檢查 bytes,如果這次不讓發送這麼多資料,則隻發送開始的部分,然後截斷剩餘的部分。這樣下次調用該連接配接的send_partial時就會繼續發送剩下的 資料。而如果可以一次發送完,則在其隊列尾部增加上choke或者unchoke消息,這裡,我們看到,程式保證了一部分(其實就是一個slice)如果 要發送的話一定能發送完,即使阻塞控制器要求阻塞某個連接配接,它也隻能阻止發送完一部分後再繼續發送下一部分。

    好了,現在終于能夠收到實際的資料了,我們繼續來看Connection._got_message中的elif t == PIECE:這一段。再次提醒,如果程式執行到這裡的話,收到的部分一定是完整的,因為每一條消息都是先發送了它的長度然後才是它的内容,而如果隻收到部 分消息的話,程式最多執行到Connection._read_messages。當收到對方的發送的資料塊後,先把開始的兩個整數解出來,即第幾塊,塊 内偏移多少(長度多少不用給出,因為已經有資料塊的實際内容),然後做一些基本檢查。檢查通過後,将其交給 SingleDownload,SingleDownload.got_piece會對其進行進一步處理。如果這個函數傳回真值,意思就是說這一塊已經完 整了,因為每一塊被分成了若幹個slice進行下載下傳,是以下載下傳到一個slice不一定能使一塊完整。而如果這一塊确實完整了,那麼給此Encoder的所 有的正常Connection都發出HAVE消息(send_have),意思就是通知所有和自己連接配接的對等客戶,我剛剛下到了某一塊,以後你們要下載下傳這 一塊也可以來找我。

    現在來研究SingleDownload.got_piece,它的作用就是處理網絡上到來的實際資料。首先,從自己的active_requests中 試圖清除掉該資料對應的請求,如果發現自己根本就沒有請求那些資料,就直接丢棄它們。然後進行endgame檢查以及更新一些速率測量器。接下來要注 意,StorageWrapper.piece_came_in會對資料進行檢查,如果它傳回真并不是說明這一塊資料下載下傳完了,隻是說明它沒有檢查出問 題,而如果它傳回假的值,那麼後果就很嚴重了,說明這一塊資料有問題,整塊的資料都需要重新下載下傳。這個if塊内的代碼做的工作就是重新配置設定下載下傳任務。要調 用StorageWrapper.do_I_have後才知道這個部分(slice)下載下傳完後是不是整個的這一塊也完成了,如果是則再将這一資訊通知 PiecePicker(PiecePicker.complete)。下載下傳完後要進行一些檢查,确定下一步的下載下傳政策,這些在以下的代碼中可以看到。最 後傳回的值是自己是否已經下載下傳完了這一塊。

    現在我們已經把BT的運作原理,即對等客戶之間是如何交換資料基本上分析完了,剩下的未分析的部分代碼基本上可以自行閱讀。