天天看點

轉貼: wolfenstein工作室-eMule源代碼學習心得

1, eMule源代碼學習心得(1):eMule代碼的總體風格和其它相關工程

eMule的官方首頁上寫着:2002年05月13日 一個叫做 Merkur 的人,他不滿意原始eDonkey2000用戶端并且堅信他能夠做的更好,是以他開始制作。他聚集了其它開發人員在他的周圍,并且eMule工程就此誕生。

eMule是一個典型的MFC程式,它的圖形界面等,已經和MFC緊緊融合到了一起。是以通常情況下它隻能在windows平台下運作。有一些其它的工程,如aMule等,把它進行了移植,是以跨平台的功能要強些。

其 實還有另外一個叫做xMule的工程,不過現在已經人氣快不行了。在aMule的首頁上可以看到eMule移植到linux平台下的一些曆史,最早是有個 叫做lMule的工程,他使用wxwidgets來進行eMule的跨平台的移植,這個工程2003年就不再更新了,後來轉變成為xMule工程,它一度 是linux平台下eMule的事實上的替代品。但是他們的程式員之間由于理念不同,發生了内讧,導緻aMule分裂出來,他們後來沖突嚴重的時候曾經一 度從理念問題上升到互相對對方進行人身攻擊,并且曾經對對方的網站發動過DDos。後來aMule和xMule就是兩個完全不同的工程,xMule現在隻 有HopeSeekr一個人在維護,基本上也沒有什麼更新了。這一點不僅讓人感慨。今年寒假的時候我曾經和HopeSeekr進行過一些交流,感覺他非常 自信,經常拿着aMule的一部分代碼來給我看,說你看看他們的代碼這麼這麼寫,這簡直就是一陀xx嘛,這種代碼在某些情況下肯定會Crash掉嘛,相 反,你看看我們xMule的代碼,這裡是這樣這樣,肯定就不會有這種問題了。

eMule從0.42版開始支援Kad技術,這是一個非常重要 的裡程碑。Kad是一種DHT的協定,它可以使節點之間互相保留一些其它節點的聯系資訊,并且利用這樣一個“關系網”尋找到整個網絡中的任何一個節點以及 上面的資源,整個過程不需要任何中心伺服器。是以向當年搞napster那樣直接端掉中心伺服器就搞跨napster網絡一樣來對付eMule的Kad網 就毫無作用了。0.42版是2004年2月27日放出的,比eDonkey2000的OverNet晚了将近一年,但是它的Kad網絡的規模卻在迅速擴 大。Overnet和eMule中的Kad使用的都是Kademlia結構,但是具體的消息封包的格式有差別,是以兩個DHT網絡并不能互相相容。 OverNet直到現在,使用者也仍然維持在十萬左右,這是比較近的一篇文章寫的。但是eMule的Kad網的規模有多大,目前卻沒有一個很準确的說法。由 此也可以看出開源軟體的力量。目前aMule的Kad網和eMule的Kad網是相容的,xMule中還沒有Kad的支援。Kad協定的原始paper可 以在我們實驗室的機器上下到:

<a href="http://bigpc.net.pku.edu.cn:8080/paper/new/by%20conference/IPTPS/IPTPS02/Kademlia%20A%20Peer-to-Peer%20Information%20System%20Based%20on%20the%20XOR%20Metric%28IPTPS02%29.pdf">http://bigpc.net.pku.edu.cn:8080/paper/new/by%20conference/IPTPS/IPTPS02/Kademlia%20A%20Peer-to-Peer%20Information%20System%20Based%20on%20the%20XOR%20Metric%28IPTPS02%29.pdf</a>

2. eMule源代碼學習心得(2):從emule.cpp開始,順便談如何編譯emule

先說一聲抱歉,因為前兩天剛回到家中,休息了一下,是以這兩天沒有更新。今天繼續昨天的話題。

eMule 的代碼結構非常合理。雖然代碼量比較大,但是各個功能子產品之間的劃分都很合理。從它的工程檔案裡面就可以看出這一點。eMule把表示功能的代碼檔案和表 示界面的代碼檔案分開了,Source Files和Header Files是實作功能的代碼的源檔案和頭檔案,而Interface Source和Interface Header是實作圖形界面的源檔案和頭檔案。由于eMule的代碼量太大,本系列将跳過圖形界面的實作,着重分析eMule的功能實作部分的代碼,而且 功能實作方面也主要挑主要的部分分析。本節将從emule.cpp開始分析,引出eMule中使用到的幾個主要的功能實作的類,并大體描述它們的作用。最 後介紹一下如何在VS2003下編譯eMule。emule中還有不少子產品實作的功能挺有用,我說的是在其它的程式裡很有用,可以考慮在其它程式中進行複 用。

emule.cpp為類CemuleApp的實作。是以在運作時,首先會運作InitInstance進行一些初始化的工作。從這個函數裡面我們也可以第一次看出那些即将在整個程式中發揮作用的類了。

最 開始的時候是計算出程式常用的一些目錄,如配置檔案,日志檔案等。接下來是ProcessCommandline,它的作用有兩方面,第一是确認該 eMule的運作方式,即指令行後面有沒有參數,第二是确認目前eMule是不是隻有一個執行個體在運作。在一般的情況下,輕按兩下eMule可執行檔案是不會帶 參數的。但是通過點選連結或者打開關聯檔案的方式打開eMule則相當于帶參數運作eMule。通過在系統資料庫裡添加一些項目可以讓一個程式和某種連結或者 某個字尾的檔案産生關聯。具體辦法可以參見OtherFunctions.cpp中的Ask4RegFix,BackupReg,RevertReg三個 函數的功能。ProcessCommandline中通過建立帶有名稱的互斥信号量來确認是否有其它的eMule執行個體在運作。對于一個确定的名稱, CreateMutex隻能建立一個互斥信号量。是以通過該信号量是否建立成功就可以知道是否有其它eMule執行個體運作。如果有的話,而且又是帶參數的那 種模式,那麼直接把這個參數使用Windows的消息機制發給那個視窗即可,接下來的代碼無非就是如何找到另外一個叫"eMule"的家夥以及給它發個什 麼消息。pstrPendingLink是一個全局變量,表示将要被處理的指令行參數。它将會在初始化完成後一段時間後被處理。

下面兩個比 較重要的類是CPreferences和CStatistics。前者掌握着程式的大部配置設定置資料,後者則進行各種統計。它們的特點都是有很多的成員變 量,而且還是靜态的,這種方式可以保證它們的唯一性,而且把這些變量統一到一個類管理。但是實際上并不需要了解每個變量的含義。thePrefs和 theStats是這兩個類的唯一的執行個體。

在處理完其它一些事情,包括建立圖形界面對象CemuleDlg後,接下來可以看到一排一排的建立新對象的語句。這些類将會實作eMule程式運作的主要功能,後面的系列将詳細分析。

最 後描述一下如何在VS2003裡編譯eMule,由于eMule中用到了一些其它的庫,是以官方在提供eMule的源代碼下載下傳的同時如果也提供這些庫的下 載會使源碼包變得很大。是以eMule選擇了讓開發者去那些庫的官方網站下載下傳它們的方式。一般來說,編譯這種工程檔案,很重要的地方是要保持各個庫和主程 序編譯參數的一緻性。這些編譯參數中,最主要的參數有三個,字元集(多位元組/Unicode),調試/發行,單線程/多線程,這樣排列組合一下就有八個版 本了。是以編譯的時候如果不注意,就會出現和程式設計無關的錯誤。

eMule0.47a解壓後自帶id3lib庫,還需要下載下傳以下的庫:

zlib:

<a href="http://www.gzip.org/zlib/">http://www.gzip.org/zlib/</a>

ResizableLib:

<a href="http://sourceforge.net/projects/resizablelib/">http://sourceforge.net/projects/resizablelib/</a>

Crypto++:

<a href="http://www.eskimo.com/~weidai/cryptlib.html">http://www.eskimo.com/~weidai/cryptlib.html</a>

pnglib:

<a href="http://www.libpng.org/pub/png/libpng.html">http://www.libpng.org/pub/png/libpng.html</a>

下 載它們,解壓它們,編譯它們。編譯的時候注意要和eMule的工程的參數一緻:字元集為Unicode,支援多線程安全,調試版或者是發行版可以根據需要 選擇,但是也要和eMule的工程參數一緻。這些庫的解壓包裡通常都能找到VC的工程檔案,但是版本低一些,直接轉化就可以了。另外建議編譯這些庫的時候 都選擇生成靜态庫,不要生成動态的庫,這樣最後生成的可執行檔案就可以自己運作了。編譯完這些庫,包括從其它地方下載下傳的和eMule自帶的id3lib庫 和CxImage庫後,就可以開始編譯emule了。而編譯emule也無非就是注意讓它能夠在編譯的時候找到所有的頭檔案,以及在連結的時候能夠找到所 有的庫。在連結的時候能夠找到所有的庫可以通過修改工程檔案裡面的屬性 -&gt;配置屬性 -&gt;連結器 -&gt;輸入-&gt;附加依賴項來完成。但是能夠找到所有的頭檔案反而需要一些技巧了。由于emule的代碼中對于這些庫的頭檔案的包含,在一定程度 上限定了那些庫的路徑和emule的工程的路徑的相對位置,是以需要把那些解壓過的庫的目錄移到一些合适的地方,有時還需要給這些目錄改個名稱。

3. eMule源代碼學習心得(3):emule中最重要的幾個基礎設施

eMule 中要讀取的配置檔案數量較多,每種配置檔案都是自己定義的格式,為了友善讀取和存儲這些檔案,eMule中有一個很重要的基礎設施類來複制這些檔案操作, 它能夠很友善得處理一些常用資料類型的讀寫,并且帶有一定的安全保護機制。這項基礎設施在SafeFile.cpp和SafeFile.h中實作。在 kademlia\io目錄下有這項功能的另外一項實作。它們實作的功能基本上相似,但是kademlia\io目錄下的版本實作的時候多了一個以Tag 作為機關進行讀寫的功能。這些實作中和另外一項基礎設施,那就是字元串轉化密切相關。StringConversion.cpp和 StringConversion.h是eMule中專門複制各類字元串轉化的基礎設施,什麼Unicode啊,多位元組流啊,或者是UTF-8之類的,在 這裡轉化全部都不是問題。關于字元串轉化,個人推薦盡量使用Unicode的寬字元,這樣可以最大程度得避免亂碼。

SafeFile.cpp 或者kademlia\io目錄下的實作都有這樣的特點,那就是把資料操作的行為和資料操作的對象分割開來。它們都定義了一個抽象的資料操作的基類(在 SafeFile.cpp中是CFileDataIO,在kademlia目錄下是DataIO.cpp實作的Kademlia::CDataIO),這 個類中隻負責實作在邏輯上操作一項資料的行為,例如,要讀取出一個32位的整型,那麼就是讀出四個位元組到一個整型數值的位址中,要讀取或者寫入其它類型的 資料用的是類似的方法。但是這個類把實體上進行資料操作的方法全部都聲明為純虛函數,即讀出多少個位元組,寫入多少個位元組這樣的。有了這樣一個基類,就可以 非常友善得在它上面進行重載,把這些純虛函數定義為向某塊記憶體中進行讀寫的操作,就能很友善得将較為複雜的資料序列化到一塊連續的記憶體,而如果這些純虛函 數是向檔案讀寫的操作,那麼自然就可以很友善得用來讀寫各種格式比較奇怪的自己定義的配置檔案了。

這些類要讀取的資料對象通常有這些,各種 整型,字元串,以及Tag類型。整型讀寫起來比較簡單,從1個位元組的,2個位元組的到4個,8個或者16個位元組類型的資料讀寫方法都比較類似。這裡要稍微提 一下16個位元組的那種,16個位元組是128位,是eMule中的Kad網的随機生成的ID的長度,也是eMule中常用的MD4的hash算法生成的結果 的長度。通常用來直接存取整個的這樣的一個ID。在kademlia\utils目錄下的UInt128.cpp實作了一個表示128位的整數的類,功能 十分完善,可以進行一些算術操作,并且可以進行比較,這樣為它以後作為key出現在hash表中打下了基礎。仔細學習UInt128.cpp中的代碼實作 可以學到很多在編寫這種自定義的資料對象類型時應該注意的問題。

資料操作中另外一項很重要的操作是字元串,總的原則是先寫一個長度,再寫内 容。但是到具體的操作的時候就需要注意這些細節了,如長度是寫4個位元組還是兩個位元組,字元串的内容要不要用UTF-8進行編碼。這些操作就需要和 StringConversion.cpp緊密合作了。其實後者的字元串轉化函數很多也是調用ATL的相關函數,隻是在外面再包上一層MFC的 CString。

在kademlia\io\DataIO.cpp中實作的CDataIO中,還另外實作了按照Tag進行讀寫的功能。這在 網絡上交換共享檔案的元資訊非常重要,通常一個檔案的元資訊就可以分解成很多的Tag,如"檔案名=xxx","檔案長度=xxx"等等。也就是說,一個 Tag就是表示某項屬性等于某個值這樣一個事實。在Opcodes.h這個檔案中定義了很多的代碼,其中就有很多常見的Tag的屬性名稱。CDataIO 類中存儲Tag的屬性名都是先存一個位元組的類型,再存名稱,最後按照類型存值。

eMule中的這幾項基礎設施都是編寫得比較好的,可以很方 便得拿出來複用。像字元串編碼的處理和具有一定資料結構的檔案IO操作在很多地方都會很有用。eMule中這些類的實作基本上複制到其它的工程檔案中隻要 稍微修改一下很快就能使用。以後我們還将看到eMule中很多其它很有用的基礎設施。 

4. eMule源代碼學習心得(4):對自己的資源要了如指掌,CKnownFileList類的作用

emule作為一個檔案共享方面的程式,首先要對自己共享的所有的檔案的資訊都十厘清楚,類CKnownFileList的作用就是這樣的,它在emule.cpp中随着cmuleapp類建立的時候被建立。

CKnownFileList 類使用了MFC的CMap類來維護内部的hash表,這也可以看出emule和MFC的關系确實非常緊密。這裡如果用STL的map其實也是可以的。它内 部維護了一個已知的檔案的清單和取消了的檔案清單。這些hash表的關鍵字都是檔案的hash值。這樣能夠判斷出檔案名不同而内容相同的檔案,而一般要讓 不同内容的檔案有相同的hash值是非常困難的,這也是hash函數它設計的初衷。是以除非是碰上王小雲教授這樣的牛人,我們基本上可以認為,兩個檔案 hash值相同就代表了它們内容相同。再來看CKnownFileList.cpp,這個檔案其實并不長,因為管理一個清單确實不需要太多種類的操作,如 果對于每個具體的檔案有一個很強大的類來處理它的話。而這裡确實有,它就是CKnownFile。有了這麼一個類,我們就可以看到, CKnownFileList類所需要做的工作就是能夠根據一些資訊查找到對應的CKnownFile類,能夠複制其它的清單中的資訊,能夠把所有的這些 資訊存成檔案,然後下次emule運作的時候能夠把這些資訊快速恢複出來,最重要的是能夠在完成以上工作的情況下不造成記憶體洩漏。

CKnownFile 類就是一個專門關注某個特定檔案的資訊的類,它仍然有其基類CAbstractFile。但是它和CAbstractFile類的主要差別就是 CAbstractFile類隻有基本的資訊存取的功能,而CKnownFile能夠主動的生成這些資訊,例如,給一個檔案的路徑給 CKnownFile,它能夠主動地去擷取和這個檔案有關的一切資訊,并且把它儲存在自己的成員變量裡(CreateFromFile)。 CKnownFile.cpp檔案看上去比較長,是因為它做的工作比較多,現在版本的emule中,除了對某個檔案進行全文hash以外,還采用了BT的 方式,進行分塊hash,這樣在傳輸檔案的時候,即使發生出錯的情況,也可以不必重傳整個檔案,而隻是重傳有錯誤的那塊,這種機制叫做進階智能損壞處理 (AICH,Advanced Intelligent Corruption Handling),這個機制以後再繼續分析。

CKnownFile 把讀到的檔案資訊都儲存成一個一個的Tag。它在運作中會盡量得擷取更多的檔案資訊,例如,對于媒體類型的檔案,它能夠調用id3lib庫來擷取諸如作 者,唱片發行年代,風格等tag資訊。如果是視訊媒體檔案,它還會去抓圖(功能實作:CFrameGrabThread)。

CKnownFile 還能夠随時掌握目前該檔案的下載下傳情況(内部有個CUpDownClient的清單),當然,還會根據要求序列化和反序列化自己,LoadFromFile 和WriteToFile都以CFileDataIO為參數,這樣友善CKnownFileList儲存和讀取它的清單中的所有檔案的資訊。 

5. eMule源代碼學習心得(5):分塊機制--正确傳輸資源的保證

為 了加快内容分發的速度,分塊處理是一種簡單有效的方法。emule中對每個檔案都進行了分塊處理。另外分塊還有一個好處就是如果保留了每一分塊的hash 值,就能在隻下載下傳到檔案的一部分時判斷出下載下傳内容的有效性。emule在擷取每個共享檔案的資訊時,就對它進行了分塊處理,是以如果要知道emule中的 分塊處理和恢複機制,看CKnownFile::CreateFromFile函數的實作就行了。

這個函數中牽涉到的和分塊處理以及hash計算相關的類都在SHAHashSet.cpp和SHAHashSet.h中。下面介紹其中幾個主要的類:

CAICHHash 類隻負責一塊hash值,提供兩個CAICHHash類之間的直接指派,比較等基本操作。CAICHHashAlgo是一個hash算法的通用的接口,其 它hash算法隻要實作這種接口都能使用,這樣,可以很友善得使用不同的hash算法來計算hash值。CAICHHashTree則是一個樹狀的 hash值組織方式,它有一個左子樹和右子樹成員變量,類型是指向CAICHHashTree的指針,這是一個典型的實作樹狀結構的方法。 CAICHHashSet中包含了一個CAICHHashTree類型的變量,它直接向CKnownFile負責,代表的是一個檔案的分塊資訊。

SHAHashSet.h 檔案的開始的注釋部分向我們解釋了它的分塊的方式。這裡要用到兩個常量9728000和184320,它們分别是9500k和180k。這是emule中 兩種不同粒度的分塊方式,即首先把一個很大的檔案分割成若幹個9500k的塊,把這些塊組織成一顆樹狀的結構,然後每一個這樣的塊又分解成若幹個180k 的塊(52塊,再加一個140k的塊),仍然按照樹狀的結構組織起來。最後總的結構還是一顆樹。

CKnownFile::CreateFromFile 方法是在讀取目标檔案的内容時,逐漸建立起這樣一顆樹的。CAICHHashTree::FindHash能夠根據讀取到的目标檔案的偏移量和下一塊的大 小,來找出對應的樹枝節點(就是一個指向CAICHHashTree的指針)。如果有必要的話,還會自動建立這些樹枝節點。是以在進行分塊操作的時候,把 檔案從頭到尾讀一邊,整個CAICHHashTree就建立起來了,對應的分塊hash值也指派好了。最後我們還需要注意的就是CKnownFile類中 的hashlist變量。就是說它還單獨保留直接以9728000位元組為機關的所有分塊的MD4算法的hash值。這樣對于一個檔案就有了兩套分塊驗證的 機制,能夠适應不同場合。

6. eMule源代碼學習心得(6):網絡基礎設施--網絡基礎設施的基礎設施

MFC中已經有一些網絡基礎設施類,如CAsyncSocket等。但是emule在設計中,為了能夠更加高效得開發網絡相關的代碼,建構了另外的一些類作為基礎設施,這些基礎設施類的代碼也有很高的複用價值。

首 先是CAsyncSocketEx類。AsyncSocketEx.h中對這個類的特點已經給出了一定的說明。它完全相容CAsyncSocket類,即 把應用程式中是以的CAsyncSocket換成CAsyncSocketEx,程式仍然能夠和原來的功能相同,是以在使用上更加友善。但是在這個基礎 上,它的效率更高,主要是在消息分發機制上,即它處理和SOCKET相關的消息的效率要比原始的MFC的CAsyncSocket類更高。另外, CAsyncSocketEx類支援通過實作CAsyncSocketExLayer類的方式,将一個SOCKET分成若幹個層,進而可以很友善得實作許 多網絡功能,如設定代理,或者是使用SSL進行加密等。

另外還有ThrottledSocket.h中定義的 ThrottledControlSocket類和ThrottledFileSocket類,這兩個類隻定義了兩個接口。任何其它的網絡套接字類如果想 實作限速的功能,隻需要在其預設的發送函數(如Send或Sendto)中不發送資料而是把資料緩存起來,然後在實作 ThrottledControlSocket或者ThrottledFileSocket接口中的SendFileAndControlData或 SendControlData方法時才真正把資料發送出去,這樣就能實作上傳限速,而這也是需要UploadBandwidthThrottler類進 行配合,UploadBandwidthThrottler是一個WinThread的子類,平時單獨運作一個線程。下一次會較長的描述它是如何控制全局的 上傳速度的。 

7. eMule源代碼學習心得(7):網絡基礎設施--全局限速器UploadBandwidthThrottler

UploadBandwidthThrottler 是emule中使用的全局的上傳限速器。它繼承了CWinThread類,且在該類被建立的時候,就新建立一個線程開始單獨運作。在該類被析構時也會自動 停止相應的線程。這個線程的目标函數就是RunProc,然後為了避免在RunProc函數不能使用this指針的情況,它使用了RunInternal 來實際完成工作線程的工作。在emule中,還有另外一個類LastCommonRouteFinder有類似的結構。

UploadBandwidthThrottler 中儲存了若幹的套接字(Socket)隊列,這些隊列的處理方式略有不同。在标準隊列(m_StandardOrder_list)裡面排隊的都是實作了 ThrottledFileSocket接口的類,通常這些類能夠傳輸檔案内容也可以傳輸控制資訊。而其它四個隊列都是實作 ThrottledControlSocket接口的類的隊列,在這些隊列中的類主要以傳輸控制資訊為主。這四個隊列為臨時高優先級,臨時普通優先級,正 式高優先級,正式普通優先級。和把套件字直接添加到普通隊列(AddToStandardList)不同, QueueForSendingControlPacket把要添加到隊列的套接字全部添加到兩個臨時隊列。根據它們的優先級添加到普通的臨時隊列。在 RunInternal的大循環中,臨時隊列中的項目先被移到普通隊列中,然後再進行處理。

UploadBandwidthThrottler 使用了兩個臨界區,兩個事件。pauseEvent是用來暫停整個大循環的動作的。而threadEndedEvent是标志整個線程停止的事件。 sendLocker是大循環中使用的主要的臨界區,而tempQueueLocker是為兩個臨時隊列額外添加的鎖,這樣可以一邊發送已有隊列中的套界 字要發送的資料,一邊把新的套接字加到隊列中。

UploadBandwidthThrottler的RunInternal中的大循環是該 工作線程的日常操作。這個大循環中做了以下事情,計算本次配額,即本次循環中能夠發送多少位元組,好安排排程,計算本次循環應該睡眠多少時間,然後進行相應 的睡眠,進而進行限速。操作控制資訊隊列,發送該隊列中的資料,注意,控制隊列中的套接字(m_ControlQueueFirst_list和 m_ControlQueue_list)隻使用一次就離開隊列。而标準隊列中的套接字不會這樣。在一輪循環結束後,如果還有沒有用完的發送資料的配額, 則會有部配置設定額儲存到下一輪。

8. eMule源代碼學習心得(8):網絡基礎設施--emule套接字CEMSocket

CEMSocket 是CAsyncSocketEx和ThrottledFileSocket的子類,它把若幹功能整合到了一起,是以可以作為emule使用起來比較友善的 套接字。例如它可以很友善得指定代理,把CAsyncSocketEx中的建立一個新的代理層并且添加到清單中的功能對外屏蔽了。另外它可以分出狀态,如 目前是否在發送控制資訊等。

CEMSocket中我們需要仔細考察的是它的SendControlData和 SendFileAndControlData方法。如前所述,這些方法是用來和UploadBandwidthThrottler進行配合,以便完成全 局的限速功能的。它的功能應該是按照UploadBandwidthThrottler的要求,在本次輪到它發送資料時發送指定數量的位元組數。是以,應用 程式的其它部分在使用CEMSocket時,如果要達到上傳資料限速的目的,不應該直接調用标準的Send或者SendTo方法,而是調用 SendPacket。這裡就有了另外一個結構Packet,它通常包含一個emule協定中完整的包,例如有協定的頭部資料等,還内置了 PackPacket和UnPackPacket方法,可以自行進行壓縮和解壓的功能。SendPacket把要發送的Packet放到自己的隊列中,這 個隊列也有兩個,控制資訊包隊列,和标準資訊包隊列。如果有必要,把自己加入到UploadBandwidthThrottler的隊列中。

我 們注意到CEMSocket的SendControlData和SendFileAndControlData方法其實都是調用自己的另一個重載的 Send方法。而且我們也已經知道這個方法是在UploadBandwidthThrottler的工作線程中的大循環中被調用的,而這個Send方法的 内容本身也是一個大循環,但是意義很明了,就是在不超過自己本次發送的配額的情況下,把自己的包隊列中的包取出來,并且發出去。同樣,這裡也用到了一個臨 界區,它是為了保證從包隊列中取出包來發送和把包往隊列中放的操作是互斥的。是以,如果把它和UploadBandwidthThrottler結合起 來,我們就看到了一個兩層的隊列,即所有的套接字組成了一個發送隊列,在UploadBandwidthThrottler的控制下保證了對速度的限制, 而每個套接字即将發送的資料包又組成了一個隊列,保證了每次進行資料發送的時候都會滿足UploadBandwidthThrottler的要求。 

9. eMule源代碼學習心得(9):搜尋資訊集-CSearchList

CSearchList 是emule中的搜尋清單,掌管emule中所有的搜尋請求。CSearchFile是這個清單中的元素,代表了一次搜尋的相關資訊。它們的關系和之前描 述的已知檔案和已知檔案清單有一些類似的地方。CSearchList的主要任務就是對其一個叫做list的類型為CSearchFile清單的内部變量 進行維護,提供很友善得往這個清單中添加,删除,查詢,變更等操作的接口。另外,每一個搜尋都有一個ID,是一個32位的整數。CSearchList中 記錄了每個搜尋目前搜到的檔案個數和源的個數(m_foundFilesCount和m_foundSourcesCount)。

CSearchFile 是CAbstractFile的另一個子類(CKnownFile也是),它儲存了某個檔案和搜尋相關的資訊,而不是這個檔案本身的資訊(這些資訊在 CAbstractFile中已經包括了),這些和搜尋有關的資訊就是都在哪些機器上有這個檔案,以及哪個伺服器上搜到的這個檔案。甚至還可以向搜尋檔案 添加預覽。在這個類的定義中嵌套定義了兩個簡單的結構SServer和SClient,表示了該搜尋檔案的可能來源,伺服器或者其它用戶端。 m_aClients和m_aServers是這兩個簡單結構的一個數組,CSearchFile自然也提供了對這個數組的操作的接口,友善 CSearchList使用。

CSearchList對外提供了搜尋表達的接口,即每當有一個新的搜尋送出時CSearchList:: NewSearch會建立一個新的搜尋項,但是此時還沒有任何對應的搜尋檔案,是以隻是在檔案個數和搜尋ID的對應表 (m_foundFilesCount和m_foundSourcesCount)中建立新的項目。另外當有搜尋結果傳回時 ProcessSearchAnswer或ProcessUDPSearchAnswer能夠對傳回的包直接做處理,建立相應的搜尋檔案資訊 CSearchFile對象,并加入到自己的清單中。當然,要把重複的搜尋結果去除,發現同一個hash的檔案的多個源時也會給它們建立一個二級清單 (CSearchFile::m_list_parent)。現在我們可以看出,CSearchList隻負責和搜尋有關的資訊的儲存和讀取,本身并不進 行搜尋。 

10. eMule源代碼學習心得(10):伺服器資訊集-CServerList

嗯,明天要回北京了,今天打算好好休息一下,故挑了個内容少的來說,呵呵。

盡 管目前有了Kad網絡,但是使用伺服器來擷取各個emule使用者的共享檔案清單仍然是emule中主要的資源擷取方式。CServerList就是 emule中負責管理伺服器清單的類。和前面若幹清單類結構類似,CServerList需要對外提供清單的增加,删除,查找,修改等接口。在 CServerList中,每個伺服器的資訊是一個CServer類。和搜尋資訊不一樣,但是和已知檔案清單一樣,伺服器的資訊清單是需要長期保留的,因 此CServerList和CKnownFileList類一樣提供了把它所包含的所有資訊儲存到一個檔案中,以及從這個檔案中讀回其資訊的功能。

CServer 中的結構比較簡單,隻需要保留伺服器的各種資訊即可。它可以通過IP位址和端口來建立,也可以通過一個簡單的結構ServerMet_Struct來創 建,其中後者是用來直接從檔案中讀取的。該結構僅僅包含IP位址和端口以及屬性的個數,CServer中其它的屬性在儲存到檔案中時,均采用Tag方式保 存。

CServerList除了提供通常的CServer資訊外,還提供一些統計資訊諸如所有的伺服器的使用者數,共享的檔案數等。這些統計資訊也是基于每個單獨的CServer的相關資訊計算出來的。

11. eMule源代碼學習心得(11):emule的通信協定-一些基本的約定

接 下來将不可避免得要碰到emule的協定。emule的通信協定格式設計成一種便于擴充的格式。對于TCP連接配接來說,連接配接中的資料流都能夠劃分成為一個一 個的Packet,CEMSocket類中就完成了把接收到的資料劃分成Packet這一工作。但是具體的對于每個Packet進行處理的工作被轉移到它 的子類中進行。CEMSocket類的兩個子類CServerSocket和CClientReqSocket所代表的TCP連接配接就分别是用戶端和伺服器 之間的TCP連接配接以及用戶端之間的TCP連接配接。在資料流中的第一個位元組代表的是通信的協定簇代碼,如0xE3為标準的edonkey協定,0xE4為 kademlia協定等等。接下來的四個位元組代表包内容的長度,所有的包都用這種方式發送到TCP流中,就可以區分出來了。另外每個包内容中的第一個位元組 為opcode,即在确定了某個具體協定後,這個opcode确定了這個包的具體含義。

對于走UDP協定的包,處理起來更加得簡 單,因為UDP本來就是以一個包一個包作為機關在網絡上流傳的,是以不需要在包的内容中再包含表示長度的字段。每個UDP包的第一個位元組是協定簇代碼,其 它内容就是包的内容。CClientUDPSocket類負責處理用戶端和用戶端之間的UDP包,而CUDPSocket類負責處理用戶端和伺服器之間的 UDP包。另外還有個Kademlia::CKademliaUDPListener類,專門處理和Kademlia協定相關的UDP包。

最 後說一下Packet類,這個類以前隻是提到過。它是emule的通信協定的最小機關。我們可以看出,它的構造函數有多個版本,這也是為了可以用不同的方 式來建立Packet。例如隻包含一個頭部資訊的緩沖區,或者隻是指定協定簇代碼等。而且它内部實作了壓縮和解壓的方法,該方法直接調用zlib庫中的壓 縮方法,可以減少資料的傳輸量。這裡要注意一點的就是壓縮的時候協定簇代碼是不參與壓縮的,壓縮完畢後會更換協定簇代碼,例如代碼為标準edonkey協 議0xE3的包在壓縮後,協定代碼就變成0xD4了,這裡進行協定代碼變化是為了使接受方能夠正确識别并且進行相應的解壓操作。

12. eMule源代碼學習心得(12):emule的通信協定-用戶端和伺服器之間的通信概述

客 戶端和伺服器之間的所有通信由類CServerConnect掌握。CServerConnect本身不是套接字的子類,但是它的成員變量 CServerSocket類型的connectedsocket是。CServerConnect内部有一清單,可以儲存若幹 CServerSocket類型的指針。但是這并不說明它平時連接配接到很多伺服器上。它隻是可以同時試圖連接配接到若幹個伺服器上,這隻是因為連接配接到伺服器上的 行為不一定能成功。

CServerSocket類是CEMSocket的子類,它比CEMSocket要多儲存一些狀态,比如目前的伺服器 連接配接狀态。它同時還保留它目前所連接配接的伺服器的資訊。通過分析CServerSocket::ProcessPacket就可以直接把emule用戶端和 伺服器之間的通信協定了解清楚,這裡是伺服器發回的包。TCP連接配接建立後的第一個包是在CServerConnect:: ConnectionEstablished中發出的,即向伺服器發出登陸資訊。如果登陸成功,則能夠從伺服器處擷取自己的ID,這是一個32位的長整 數。如果這個數小于16777216,那麼我們稱它為LowID。具有LowID的用戶端通常情況下其它用戶端将不能直接連接配接它。得到LowID的原因比 較多,例如當自己處于NAT的後端的時候。擷取自己的ID後将會向伺服器發送自己的共享檔案清單,這一動作由共享檔案清單類 CSharedFileList來完成。

其它類型包沒有必要全部都列出來,以後可以通過在分析其它部分時,因為牽涉到往伺服器發送或者接受資料的時候再進行相應的分析。 

13. eMule源代碼學習心得(13):emule的通信協定-用戶端和用戶端之間的通信概述

客 戶端和用戶端之間的TCP通信由CListenSocket和CClientReqSocket完成。這也是提供網絡服務的應用程式的典型寫法。其中 CListenSocket隻是CAsyncSocketEx的子類,隻負責監聽某個TCP端口。它隻是内部有一個CClientReqSocket類的 清單。而CClientReqSocket是CEMSocket的子類,是以它能夠自動完成emule的packet識别工作。它有 ProcessPacket和ProcessExtPacket來處理用戶端和用戶端之間的包,其中前者是經典的eDonkey協定的包,後者是 emule擴充協定的包。

CListenSocket和CClientReqSocket類之間的關系和前面分析的清單類和它對應的成員類 的關系是相似的,CListenSocket提供對自身的CClientReqSocket清單中的元素的增加,查詢,删除等操作。同時也維護關于這些成 員的一些統計資訊。我們注意到CListenSocket在其構造函數中就把自己添加到CListenSocket類 (theApp.listensocket,該類的唯一實際示例)的清單中。

CClientReqSocket類和 CUpDownClient類之間存在着對應關系。它們都表示了另外一個用戶端的一些資訊,但是CClientReqSocket類主要側重在網絡資料方 面,即負責兩邊的互相通信,而CUpDownClient類負責的是從邏輯上對網絡另一邊的一個用戶端進行表達。CUpDownClient類代碼很長, 以後再說。 

14. eMule源代碼學習心得(14):emule中的信譽機制

信譽機制在P2P系統中有非常重要的作用。為 了使使用者更加願意共享自己的資源,需要有一些機制能夠讓對整個P2P系統貢獻更大的使用者有更多的激勵。在emule中,激勵機制的設計方案是tit- for-tat這種最直覺的方案。這種方案的意義就是最簡單的如果别人對你好,那麼你也對别人好。

下面看實際的實作。 CClientCreditsList和CClientCredits類負責emule中的信譽機制。我們再次見到這種清單和元素之間的關系,不必再重複 那些語言。和信譽相關的資訊是需要永久儲存的,這樣才有意義,是以CClientCreditsList提供了LoadList和SaveList方法。 我們另外注意到,CClientCredits類可以使用CreditStruct結構來建立,而CreditStruct結構隻包含靜态資訊。主要是上 傳量和下載下傳量等。

信譽機制的資訊需要有一定的可靠性,在emule中采用了數字簽名的方式來做到這一點。Crypto++庫為emule全 程提供和數字簽名驗證相關的功能。CClientCreditsList在建立時,會裝載自己的公鑰私鑰,如果沒有的話,會建立一對。 CClientCreditsList中包含的有效的資訊都是經過其它人數字簽名的,是以更加有信服力。在實際使用中,這些資訊和自己的私鑰要注意儲存。 重裝emule後應該把配置檔案目錄先備份,這樣能夠保留自己辛辛苦苦積攢的信譽。 

15. eMule源代碼學習心得(15):下載下傳任務即部分檔案的表示

CPartFile 類是emule中用來表示一個下載下傳任務的類。從它的名字也可以看出來,這就是一個還沒有完成的檔案。當一個下載下傳任務被建立時,emule會在下載下傳目錄中創 建兩個檔案,以三位數字加字尾part的檔案,例如001.part,002.part等。還有一個以同樣的數字加上.part.met的檔案,表示的是 對應檔案的元資訊。part檔案會建立得和原始檔案大小一樣,當下載下傳完成後,檔案名會修改成它本來的名稱。而事實上,諸如這個檔案原來叫什麼名稱,修改日 期等等資訊都在對應的.part.met元檔案中。.part.met中還包含了該檔案中那些部分已經下載下傳完成的資訊。

CPartFile 類中Gap_Struct來表示檔案的下載下傳情況,一個Gap_Struct就是一個坑,它表示該檔案從多少位元組的偏移到多少位元組偏移是一個坑。下載下傳的過程 就是一個不斷填坑的過程。CPartFile類中有個成員變量gaplist就是該檔案目前的坑的狀況清單。需要主要的是有時填了坑的中間部分後,會把一 個坑變成兩個坑。坑的清單也會被存進.part.met中。

CPartFile類的代碼很龐大,但是這是必須的。首先,它的建立就有幾種可 能,從搜尋檔案CSearchFile中建立,這種情況發生在使用者搜尋到他想要的檔案後點選下載下傳時發生。從一個包含了ed2k連結的字元串中建立,它會提 取出該ed2k連結中的資訊,并用來建立CPartFile。剩下的一種,就是當emule程式重新開機後,恢複以前的下載下傳任務。這時就是去下載下傳目錄中尋找那 些.part和.met檔案了。另外它還需要不斷得處理下載下傳到的資料,為了減少磁盤開銷,使用了Requested_Block_Struct結構來暫存 寫入的資料。它内部維護一個CUpDownClient的清單,如果知道了該檔案的一個新的來源資訊,就會建立一個對應的CUpDownClient。後 者是emule中代碼量最大的類。它還要把它的狀态用彩色的條裝物顯示出來提供給GUI。

前面提到的AICH機制對于最大程度得保證下載下傳檔案的正确性以及盡量減少重複傳輸都有很大的幫助,在下載下傳的過程中,該機制會經常對下載下傳到的資料進行校驗。

最 後提一下它的Process方法。該方法是emule中為了盡量減少線程的使用而采取的一種有一些類似于輪詢的機制。其它很多類中也有Process方 法,這個方法要做的事情就是在一些和日常運作有關的事情,例如檢查為了下載下傳該檔案而連結到自己的各個用戶端的狀态,向它們發送下載下傳請求等。

16. eMule源代碼學習心得(16):下載下傳任務隊列

CDownloadQueue 是下載下傳隊列類。這個隊列中的項目是CPartFile指針。是以和emule中出現的很多其它的清單類一樣,它需要能夠提供對這個清單中的元素進行增加, 查詢,删除的功能。例如查詢的時候能夠根據該檔案的hashID或者索引來進行查詢。CDownloadQueue同時還要完成一些統計工作。

和 其它的清單類不一樣的是,它的所有元素的資訊并不是集中存放于一個檔案,而是對應于每一個下載下傳任務,單獨得存放在一個元資訊檔案(.part.met) 中,是以當該類進行初始化的時候,它需要尋找所有可能的下載下傳路徑,從那些路徑中找到所有的.part.met檔案,并且試圖用這些檔案來生成 CPartFile類,并且将這些通過.part.met檔案正确生成的CPartFile類添加到自己的清單中,同樣,在退出時,所有的下載下傳任務的元信 息也是自行儲存,不會合成為一個檔案。

CDownloadQueue中的Process方法的主要任務就是把它的清單中的 CPartFile類中的Process方法都調一遍,另外主要的一些關于下載下傳情況的統計資訊也是在每一輪的Process後進行更新的。從這裡我們也可 以看出Process方法在emule中的意義,就是一個需要經常執行的方法,通過經常執行它們來完成日常工作,而且所有的這些Process方法肯定是 順序執行,是以可以減少很多多線程的同步之類的問題。emule中已經盡量減少了多線程的使用,但是在很多地方如果多線程是不可避免的話,也不會排斥。

17. eMule源代碼學習心得(17):上傳任務隊列

CUploadQueue是上傳隊列類。這個清單類中隻有以 CUpDownClient為元素的清單,它和其它清單類還有一個很大的不同就是它所儲存的資訊都不需要持久化,即不需要在目前的emule退出後還記住 自己正在給誰上傳檔案,然後下次上線的時候再繼續給他們傳,這在大部分情況下是沒有意義的。

上傳隊列類清單中有兩個清單,上傳清單和排隊清單。當一個收到一個新的下載下傳請求後,它會把對應的用戶端先添加到排隊清單中,以後再根據情況,把它們不斷添加到上傳清單中。在這裡,信譽機制将會對此産生影響。

CUploadQueue的Process方法就相對簡單了,那就是向上傳隊列中的所有用戶端依次發送資料,而排隊的用戶端是不會得到這個機會的。另外它還需要完成關于上傳方面的一些統計資訊。

另 外我們還需要注意在CUploadQueue的構造函數裡面,建立了一個以100毫秒為間隔的定時器,這個定時器成為以上所有的Process所需要的基 礎。我們看它的UploadTimer就可以看出這一點。這裡面充斥了各個類的Process方法的執行,其中包括以前我們提到的一些類,但是沒有提到它 們的Process方法,因為其過于簡單,基本上就隻是更新了一下要儲存的資訊。 

18. eMule源代碼學習心得(18):emule中代碼量最大的類CUpDownClient

CUpDownClient 類的作用是從邏輯上表示一個其它的用戶端的各種資訊,它是emule中代碼量最大的類。我們注意到,定義它的頭檔案是UpDownClient.h,但是 卻沒有對應的CUpDownClient.cpp,而它的實作,都分散到BaseClient.cpp,DownloadClient.cpp, PeerCacheClient.cpp,UploadClient.cpp和URLClient.cpp中。

BaseClient.cpp 中實作的是該類的一些基本的功能,包括基本的各種狀态資訊的擷取和設定,以及按照要求處理和發送各種請求。在這裡,邏輯實作和網絡進行了區分, CUpDownClient類本身不從網絡接受或者發送消息,它隻是提供各種請求的處理接口,以及在發送請求時,構造好相應的Packet,并交給自己對 應的網絡套接字發出去。

DownloadClient.cpp中實作的是和下載下傳相關的功能,它包括了各種下載下傳請求的發送以及相應的資料的接 收。另外還有一個A4AF的機制,它是emule中的一個機制,因為一個用戶端在同一個時間内隻能向另外一個用戶端請求同一個檔案。這樣,對于很多個下載下傳 任務(CPartFile),有可能出現它們的源(即有該檔案的用戶端)有部分重疊的現象,而這時,如果其它下載下傳任務正在從這個源下載下傳,那麼目前的下載下傳任 務就不能從這個源下載下傳了。但是emule允許使用者對其手動進行控制,如對下載下傳任務的優先級進行區分,這樣他就可以将一個源從另外一個下載下傳任務那裡切換過 來。A4AF其實就是ask for another file的簡稱。

UploadClient.cpp中實作的是上傳相關功能,即接受進來的下載下傳請求,并且生成相應的檔案塊發送出去。

PeerCacheClient.cpp 實作的是和PeerCache相關的功能,PeerCache是一個由Joltid公司開發的技術,它可以允許你從ISP提供的一些快照伺服器上快速得上 傳或者下載下傳一些檔案(或者是一部分),這個技術的好處是可以減少骨幹網絡的帶寬消耗,将部分本來需要在骨幹網上走的流量轉移到ISP的内部。當然這個功能 需要ISP的配合。如果發現ISP提供了這項服務的話,emule會利用它來減少骨幹網的帶寬消耗。

URLClient.cpp實作的功能是利用http協定對原有的emule協定進行包裝,以便使它能夠盡可能地穿越更多的網絡的防火牆。 

19. eMule源代碼學習心得(19):emule正常部分小結

emule 中還有其它的很多類,它們使得emule的功能更加的強大和完善。有很多類在前面沒有提到,但是不代表它沒有作用。而且即時是前面提到的類也隻是大體的介 紹,它們之間互相配合的一些細節沒有展現。但是這些細節應該已經可以通過對它們的大體的功能的了解而更加容易被把握。至于GUI的設計,它也最終是要對應 到某個功能實作類的資料的。

對于emule中的通信協定隻是大體得描述了一下它的資料包的格式,但是并沒有詳細得描述它的每一個 Opcode對應的包的意義,因為我認為這是沒有必要的,在知道通信協定的格式以及處理它們的代碼所在的位置後,可以很簡單的通過追蹤某條消息的前因後果 把整個通信協定都分析出來。

這裡再稍微提一下在emule中使用到的其它類及其功能。我們可以看到,如果單純隻是為了能夠搜到以及下載下傳到文 件的話,有不少類是可以精簡的,但是,正是由于它們的存在,使得emule的功能更加的完善。CIPFilter,IP位址過濾器,通過識别各種類型的 IP位址過濾資訊,它能夠把不希望連接配接的網絡位址過濾掉,emule中所有需要連接配接網絡的地方使用的都是統一的過濾資料。CWebServer能夠在本地 打開一個Web伺服器,然後你可以通過浏覽器來控制你的emule。CScheduler能夠實作下載下傳任務的定時下載下傳。CPeerCacheFinder 為前面提到的PeerCache技術的主要制類。另外,emule還内置了一個IRC用戶端,一個主要成員函數都為靜态的 CPartFileConvert類,能夠對其它版本的驢的下載下傳檔案進行轉換。它甚至還提供了一個自動處理zip和rar的類 CArchiveRecovery。

Kademlia網絡是emule中相當重要的一部分,是以特意把這一部分單獨拿出來,把它放在這個小結之後進行描述。

20. eMule源代碼學習心得(20):emule中的Kademlia代碼總體描述

首先要說一下抱歉,這兩個星期的事情比較多,是以現在才來寫。不過總算是把這些東西寫完了。而且最後用的代碼是0.47c。

當emule 中開始使用Kademlia網絡後,便不再會有中心伺服器失效這樣的問題了,因為在這個網絡中,沒有中心伺服器,或者說,所有的使用者都是伺服器,所有的用 戶也是用戶端,進而完完全全得實作了P2P。接下來講針對emule中的Kademlia網絡進行分析,會有一節進行原理方面的分析。另外的幾節将會根據 emule中實作Kademlia所使用的不同的類分别進行講述。其中:

CKademlia是整個Kademlia網絡的主要類,可以直接開始或者停止Kademlia網,并且含有Process方法來處理日常事務。

CPrefs負責處理自身的Kademlia相關資訊,如自身的ID等。

CRoutingZone,CRoutingBin和CContact三個類組成了每個節點所了解的聯系資訊以及由這些聯系資訊所組成的資料結構。

CKademliaUDPListener負責處理網絡資訊。

CIndexed負責處理本地存儲的索引資訊。

CSearch,CSearchManager負責處理和搜尋有關的操作,其中前者表示的是一個單一的搜尋任務,後者負責對所有搜尋任務進行處理。

CUInt128負責處理一個128位的長整數,并且内置其各種運算。前面已經提到過

21. eMule源代碼學習心得(21):emule中的Kademlia的基本原理

Kademlia 是一種結構化的覆寫網絡(Structured Overlay Network),所謂的覆寫網絡,就是一種在實體的Internet上面再次建構的虛拟網絡,所有參與的節點都知道一部分其它節點的IP位址,這些節點 稱為它的鄰居,如果需要查找什麼東西,它先在本地尋找,如果找不到,就把這個查詢轉發到它的鄰居處,希望能夠有可能查找到相應的結果。覆寫網絡裡面分成了 結構化和非結構化的兩種情況,它們的差別在于每個節點知道哪些其它節點的資訊是否有特定的規律。在非結構化的覆寫網中,每個節點的鄰居狀況沒有特定的規 律。是以在非結構化網絡中,如果要進行查詢,會采取一種叫做泛洪(flooding)的方法,每個節點如果在本地沒有查找到想要的結果,會把查找請求轉發 到它的鄰居中,然後再通過鄰居的鄰居這種方式來進行一步步的查找。但是這種方法如果處理不好,會造成整個網絡的消息負載過大。已經有不少文章對于優化非結 構化覆寫網絡中的查詢進行了很深入的探讨。

對于結構化的覆寫網絡,它的特點是每個節點它會選擇和哪些節點做鄰居是有一定的規律的,進而在進 行搜尋的時候,節點把搜尋請求進行轉發的時候它能夠通過一定的規律進行選擇把請求轉發到哪些鄰居節點上。這樣同時也能減少搜尋代價。結構化的覆寫網絡通常 要求每一個節點随機生成一個ID,用以判斷各個節點之間的關系。這個ID和它所在的實體網絡必須是沒有關系的。

對于Kademlia網絡來 說,這個ID是一個128位的數值,所有的節點都用這個ID來衡量自己與其它節點的邏輯距離。而邏輯距離的計算方法就是将兩個節點進行異或(XOR)操 作。在Kademlia網絡的形成過程中,每個節點選擇鄰居的原則是離自己邏輯距離越近的節點越有可能被加入到自己的鄰居節點清單中,具體來說就是在每次 新得到一個節點的資訊的時候,是否把它加入到自己的鄰居節點清單是根據距離的遠近來處理的。後面分析具體程式的代碼時會有說明。

結構化的網 絡的好處就是如果我們要尋找一個距離某個ID邏輯距離足夠近的節點,我們可以保證在O(logn)級别的跳數找到。隻要先尋找自己已知的離目标ID邏輯距 離足夠斷的節點,然後再問它知不知道更近的,然後就這樣下去。是以在搜尋的時候也是這樣,當需要釋出資源的時候,把檔案進行hash,這樣就能夠計算出一 個128位的ID,或者把關鍵字進行hash。然後尋找到離這個結果邏輯距離最近的節點,把檔案或者關鍵字的資訊發送給它,讓它存起來。當有人要搜尋同樣 的東西的時候,由于它用的是同一個hash算法,是以能夠計算出對應的ID,并且去搜尋那些和這個ID邏輯距離相近的節點,因為它知道,如果網絡中真有這 些資源的話,這些節點是最有可能知道這些資訊的。由此我們可以看出,結構化的網絡的資源查找效率是很高的,但是它和非結構化的覆寫網絡比起來,缺點是不能 進行複雜查詢,即隻能通過簡單的關鍵字或者檔案的hash值進行查找。非結構化的網絡的查找本身就是随意轉發的,每個收到的查詢請求的節點都對本地的資源 掌握的很清楚,是以自然可以支援複雜查詢,但是顯然非結構化的網絡支援的複雜查詢不太可能動員所有的節點都來做這一動作。目前還沒有方法能夠把兩種覆寫網 絡的優點結合起來,我也非常想知道這樣的一種方法。

22. eMule源代碼學習心得(22):emule中的Kademlia的基礎設施類

Kademlia 的主要類是CKademlia,它負責啟動和關閉整個Kademlia網的相關代碼。在它的Process函數中,會處理和Kademlia網相關的事 務,例如隔一段時間檢查某個區間的節點數是否過少,如果是則尋找一些新的節點。另外經常對自己的鄰居進行檢查等,這些都是屬于需要進行日常安排的工作。所 有搜尋任務的日常處理也需要它來排程。它還作為Kademlia網的代表,向emule其它部分的代碼傳回Kademlia網的一些統計資訊。

另一個基礎設施類是CPrefs,它和emule普通代碼中的CPreferences作用類似,但是CPrefs隻保留和Kademlia網相關的,需要長期儲存的本地資訊。具體到這個版本來說,主要就是本地的ID。

還有一個很重要的基礎設施就是CUInt128,實作對128位的ID的各種處理,前面的部分已經提到。

23. eMule源代碼學習心得(23):emule中的Kademlia的聯系人清單管理

CRoutingZone,CRoutingBin和CContact三個類組成了聯系人清單資料結構。它要達到我們搜尋的要求,即搜尋到目标的時間要能夠接受,而且所占用的空間也要能夠接受。

首 先CContact類包含的是一個聯系人的資訊,主要包括對方的IP位址,ID,TCP端口,UDP端口,kad版本号和其健康程度 (m_byType)。其中健康程度有0-4五個等級。剛剛加入的聯系人,也就是健康狀況未知的,這個數值設定為3。系統會經常通過與各個聯系人進行聯系 的方式對其進行健康狀況檢查,經常能夠聯系上的聯系人,這個數值會慢慢減少到0。而很就沒有聯系的,這個數值會慢慢增加,如果增加到4後再過一段時間未能 成功聯系上的,則将會被從聯系人清單中删除。

CRoutingBin類包含一個CContact的清單。這裡要注意的是要通路聯系人的資訊 必須通過某個CRoutingBin,CRoutingZone内部是不直接包含聯系人資訊的。可以把新的聯系人資訊往一個特定的CRoutingBin 中加,當然也可以進行聯系人查找。它也提供方法能夠尋找出離某個ID距離最近的聯系人,并給出這樣的一個清單。這是相當重要的。最後,一個 CRoutingBin類中能夠包含的CContact的數量也是有限制的。

CRoutingZone類處于聯系人資料結構的最上層,直接 為Kademlia網提供操作接口。該類的結構為一個二叉樹,内含兩個CRoutingZone指向它的左子樹和右子樹,另外也包含一個 CRoutingBin類型的指針。但是隻有在目前的CRoutingZone類為整個二叉樹的葉節點時,這個指向CRoutingBin類型的指針才有 意義。這個二叉樹的特點是,每個節點以下的所有聯系人的ID都包含一個共同字首,節點的層數越深,這個共同字首越長。例如,根節點的左子樹的所有的節點的 ID一定有一個字首"0",而右子樹的所有節點一定有字首"1"。同樣,根節點的左子樹的右子樹下的所有節點的ID一定有字首"01",等等,依此類推。 我們設想一下節點不斷得往這個二叉樹添加的過程。剛開始隻有一個根節點,它也就是葉節點,這時它内部的CRoutingBin是有意義的,當聯系人資訊不 斷得被添加進去以後,這個CRoutingBin的容量滿了,這時要進行的就是一個分裂的操作。這時,會添加兩個左子節點和右子節點,然後把自身的 CRoutingBin中的聯系人資訊按照它們的字首特點分别複制往左節點和右節點,最後把自身的CRoutingBin廢除掉,這樣這個分裂過程就完 了。當分裂完成後,就會再次試圖添加該聯系人資訊,此時會試圖按照它的ID,把它添加到對應的子樹中。但是并不是所有的這種情況節點都會發生分裂,因為如 果允許任意分裂的話,本地所需存儲的節點資訊數量就會急劇上升。這裡,自身ID的作用就展現了。隻有當自身ID和目前準備分裂的節點有共同字首時,這個節 點才會分裂,而如果判斷到一個節點不能分裂,而它的CRoutingBin又滿掉了,那麼就會拒絕添加聯系人資訊。

我們可以看出,在以上政 策的進行下,離自身ID邏輯距離越近(也就是共同字首越長)的聯系人資訊越有可能被加入,因為它所對應的節點越有可能因為分裂而獲得更多的子節點,也就對 應了更多的容量。這樣,在Kademlia網中,每一個參與者知道的其它參與者資訊中,離自己邏輯距離越近的參與者比例越高。由于在搜尋的時候也隻需要不 斷得尋找更近的ID,而且每一步都一定會有進展,是以尋找到目标ID所需要的時間上的代價是O(logn),從這個二叉樹的結構來看,我們也可以看到,由 于隻有部分節點會分裂,是以實質上存儲所需要的空間代價也是O(logn)。

實際上CRoutingZone在實作時和理論上的Kademlia有一些差別,如從根節點開始,有一個最低分裂層數,也就是說,如果層數過低的話,是永遠允許分裂的,這樣它知道的其它地區的聯系人資訊就能夠稍微多一些。

24. eMule源代碼學習心得(24):emule中的Kademlia網絡消息處理

CKademliaUDPListener 負責處理所有和Kademlia網相關的消息。前面已經對emule的通信協定的基本情況做了一個大概的描述,我們就可以知道, CKademliaUDPListener處理的消息一定是隻和Kademlia網相關的,分揀工作已經在emule的普通UDP用戶端處理代碼那裡處理 好了。具體的消息格式前面也有一些介紹,下面會就一些具體的消息分類做說明。

首先是健康檢查方面的消息,這樣的消息就是一般的ping- pong機制。對應的消息有KADEMLIA_HELLO_REQ和KADEMLIA_HELLO_RES。當對本地聯系人資訊清單進行檢查時,會對它們 發出KADEMLIA_HELLO_REQ消息,然後處理收到的KADEMLIA_HELLO_RES消息。

最常用的消息是節點搜尋消息, 在Kademlia網絡中,進行節點搜尋是日常應用所需要傳輸的主要消息,它的實作方式是疊代式的搜尋。這種方式就是說當開始搜尋某個ID時,在本地聯系 人資訊清單中查找到距離最近的聯系人,然後向它們發出搜尋請求,這樣通常都能夠得到一些距離更近的聯系人資訊,然後再向它們發送搜尋請求,通過不斷得進行 這樣的搜尋查詢,就能夠得到距離目标ID最近的那些聯系人資訊。這裡對應的消息代碼是KADEMLIA_REQ和KADEMLIA_RES。

接 下來就是對内容進行釋出或者搜尋。這一點結合後面的CIndexed類的分析可以知道得更加清楚。emule中存儲在Kademlia網中的資訊主要有三 類:檔案源,關鍵字資訊和檔案的評論。檔案源對應的是每一個具體的檔案,每個檔案都用它的内容的hash值作為該檔案的唯一标示,一條檔案源資訊就是一條 關于某人擁有某個特定的檔案的這樣一個事實。一條關鍵字資訊則是該關鍵字對應了某個檔案這樣一個事實。很顯然,一個關鍵字可能會對應多個檔案,而一個特定 的檔案的檔案源也很有可能不止一個。但是它們的索引都以固定的hash算法作為依據,這樣使得搜尋和釋出都變得很簡單。

我們來看釋出過程。 每個emule用戶端把自己的共享檔案的底細已經摸清楚了,在傳統的有中心索引伺服器的場景裡,它把自己的所有檔案的資訊都上傳到中心索引伺服器裡。但是 在Kademlia網裡,它就需要分散傳播了,它首先做的事情是把檔案名進行切詞,即從檔案名中分解出一個一個的關鍵詞出來,它切詞的方法非常簡單,就是 在檔案名中尋找那些有分割符含義的字元,如下劃線等,然後把檔案名切開。計算出這些關鍵字的hash值後,它把這些關鍵字資訊釋出到對應的聯系人那裡。并 且把檔案資訊也釋出到和檔案内容hash值接近的聯系人那裡。對應的消息是KADEMLIA_PUBLISH_REQ和 KADEMLIA_PUBLISH_RES。另外emule允許使用者對某個檔案發表評論,評論的資訊單獨儲存,但是原理也是一樣的。

當使用者 使用Kademlia網絡來進行搜尋并且下載下傳檔案的時候,首先是對一個關鍵詞進行搜尋,由于使用的是同樣的hash算法,這樣它隻要找到ID值和計算出來 的hash值結果相近的聯系人資訊後,它就可以直接向它們發送搜尋特定關鍵詞的請求了。如果得到了傳回資訊,那麼搜尋者就知道了這個關鍵詞對應了多少文 件,然後把這些檔案的資訊都列出來。當使用者決定下載下傳某個檔案的時候,針對這一特定檔案的搜尋過程就開始了,這一次如果搜尋成功,那麼傳回的就是這個檔案的 檔案源資訊。這樣emule接下來就隻需要按照這些資訊去連接配接相應的位址,并且使用傳統的emule協定去和它們協商下載下傳檔案了。這裡對應的消息是 KADEMLIA_SEARCH_REQ和KADEMLIA_SEARCH_RES。

實際的實作中有Kademlia2這種協定,它的原理 是一樣的,隻有協定代碼和具體的消息格式不一樣,例如KADEMLIA_REQ和KADEMLIA_RES對應了KADEMLIA2_HELLO_REQ 和KADEMLIA2_HELLO_RES,但是後者在具體的消息中包含了比前者豐富一些的資訊。在實作的時候0.47c更加傾向于使用 Kademlia2,而0.47a更加傾向于使用Kademlia。當然,它們兩種協定都能夠處理。另外,0.47c增加了一個對于已發出的請求的追蹤的 特性,就是一個包含TrackPackets_Struct類型的清單,這裡面詳細紀錄了什麼時間曾經對哪個IP發出過那種opcode對應的請求。為什 麼要這樣呢?這是為了防止針對DHT的一種路由污染攻擊,因為在搜尋聯系人的時候,如果搜尋到了一些聯系人資訊,也會試圖把它先加入到本地的聯系人資訊列 表中。這樣如果有人想惡意攻擊的話,它隻要不斷得往它想攻擊的emule用戶端發送KADEMLIA_RES,并且在消息的内容中包含大量的虛假聯系人信 息,就可以使對方的聯系人資訊清單中充滿垃圾。這樣,由于缺少正确有效的聯系人資訊,它的Kademlia網功能基本上就廢了。而在0.47c裡面增加的 這個特性,就會對那種還沒有送出請求就收到回應的情況直接無視,進而避免被愚弄。

25. eMule源代碼學習心得(25):emule中的Kademlia的分布式索引管理

Kademlia 網絡的最大的好處是把原來需要存儲到中心索引伺服器中的資訊分散存儲到各個用戶端當中,如果要說得更加準确一點,那我們就可以說它把這些資訊分散得存儲到 各個emule用戶端的CIndexed類當中。我們可以具體開始看CIndexed的設計,看它是如何完成這一工作的。在這之前我們要稍微詳細得說一下 emule釋出到Kademlia網絡中的資訊的各種類型。

一個檔案源資訊是一個檔案内容的hash值和擁有這個檔案的用戶端的IP位址, 各種端口号以及其它資訊之間的對應關系。而一個關鍵詞資訊則是該關鍵詞和它對應的檔案之間的關系。在關鍵詞資訊中,它對應的檔案資訊要更加詳細,通常包括 這個檔案的檔案名,檔案大小,檔案内容的hash值,如果是MP3或者其它媒體檔案,還會包含包括作者,生産時間,檔案長度(這個長度是用時間來衡量的媒 體檔案的播放長度),流派等等tag資訊。其中檔案内容的hash值用來區分該關鍵詞對應的不同檔案。

CIndexed中利用了一系列的 Map來存儲這些對應資訊,CMap是MFC中實作标準STL中的map的模闆類,CIndexed中包含了四個這樣的類,分别用來存儲檔案源資訊,關鍵 詞資訊,檔案評論資訊以及負載資訊。其中檔案評論資訊是不長久儲存的,而其它的資訊都會在退出的時候寫到檔案中,下次重新啟動emule時再重新調入。另 外負載資訊不是等其它聯系人來釋出的,而是根據檔案源資訊和關鍵詞資訊的釋出情況自行進行動态調整的。每一次收到釋出資訊時,對應的ID的負載會增大,這 一事實會在回應消息(KADEMLIA_PUBLISH_RES)中展現。

CIndexed中的資訊會經常進行檢查,每隔三十分鐘它會把自 己存儲的所有資訊中太老的資訊清除掉。其中檔案源資訊的儲存時間為五小時,關鍵詞資訊為二十四小時,檔案評論的資訊儲存時間也為二十四小時。是以檔案的發 布和關鍵詞也要周期性得反複進行。其實這對于整個Kademlia網絡的穩定性也是有好處的,因為每一次聯系都會試圖把對方添加到自己的聯系人清單中,或 者在聯系人清單中标注上一次見到對方的時間。

CIndexed為其它部分的代碼提供了它們所需要的增加資訊和搜尋資訊的接口,這樣在從網絡中擷取到相關的搜尋或者釋出請求,并且CKademliaUDPListener完成消息的解釋後,就可以交給CIndexed來進行處理了。

26. eMule源代碼學習心得(26):emule中的Kademlia搜尋任務管理

CSearch 和CSearchManager是完成具體搜尋任務的。CSearch對應的是一個具體的搜尋任務,它包括了一個搜尋任務從發起到結束的全部過程,要注意 的是搜尋任務并不隻是指搜尋檔案源或者關鍵詞的任務,一次釋出任務它也需要建立一個CSearch對象,并且讓它開始執行。CSearchManager 則掌握所有的搜尋任務,它包含了一個包含所有CSearch指針對象的CMap,使用CMap的原因是因為所有的CSearch都一定對應一個ID,那個 ID就是該CSearch所對應的目标,不管是要查找節點,還是要搜尋或者釋出資訊,一定都要找到和目标ID相近的聯系人。是以 CSearchManager可以使用CMap來表示所有的搜尋任務。

我們注意到CSearch在建立的時候就把自己加入到 CSearchManager當中。另外CSearch在建立的時候需要說明它的類型,例如是隻是為了搜尋節點還是要搜尋關鍵詞資訊或者檔案源資訊,當然 也有可能是釋出檔案源資訊或者關鍵詞資訊。我們介紹一下CSearch的幾個方法的作用就可以大概了解CSearch的工作過程。Go是它的啟動過程,它 會開始第一次從本地的聯系人清單中尋找候選的聯系人,然後開始發動搜尋。SendFindValue的功能就是向某個聯系人發送一個搜尋某ID的聯系人信 息這樣一個請求。JumpStart則是在搜尋進行到一定地步的時候,如得到了一些中間結果,開始進行下一步的行動,下一步的行動仍然可能是 SendFindValue,也有可能認為搜尋到的聯系人離目标已經足夠近了,于是就可以開始實質性的請求。StorePacket就是這樣一個實質性的 請求,例如在一個以釋出檔案源為任務的CSearch中,StorePacket會向目标聯系人發送 KADEMLIA2_PUBLISH_SOURCE_REQ(如果不支援Kademlia2,那麼是KADEMLIA_PUBLISH_REQ)。最後, CSearch能夠處理各種搜尋結果,然後向調用它的代碼傳回處理好的結果。

CSearchManager直接和Kademlia網的其它 部分代碼接觸,例如,如果CKademliaUDPListener搜尋到了一些結果,它會把這些結果交給CSearchManager,然後 CSearchManager再去尋找這個結果是屬于那個搜尋任務的,并且進行轉交。另外CSearchManager對外提供建立各種新的搜尋任務的接 口,作用類似于設計模式中的Factory,其它部分的代碼隻需要說明需要開始一個什麼樣的搜尋任務即可,CSearchManager來完成相應的建立 CSearch的任務。

27. eMule源代碼學習心得(27):後記

終于把自己想幹的另一件事情幹完了。eMule的代碼寫得 很好,裡面有很多都是值得我們學習的地方。由于時間關系,我也隻看了那些和實作最基本的功能相關的代碼,而實際上eMule裡面還有很多代碼實作了很多很 有意思的功能。eMule的GUI設計的代碼也是很不錯的,但是我也沒有來得及看。最後,歡迎大家來和我讨論關于P2P技術方面的問題。

繼續閱讀