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可
以在我們實驗室的機器上下到:
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
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:
http://www.gzip.org/zlib/
ResizableLib:
http://sourceforge.net/projects/resizablelib/
Crypto++:
http://www.eskimo.com/~weidai/cryptlib.html
pnglib:
http://www.libpng.org/pub/png/libpng.html
下
載它們,解壓它們,編譯它們。編譯的時候注意要和eMule的工程的參數一致:字符集為Unicode,支持多線程安全,調試版或者是發行版可以根據需要
選擇,但是也要和eMule的工程參數一致。這些庫的解壓包里通常都能找到VC的工程文件,但是版本低一些,直接轉化就可以了。另外建議編譯這些庫的時候
都選擇生成靜態庫,不要生成動態的庫,這樣最后生成的可執行文件就可以自己運行了。編譯完這些庫,包括從其它地方下載的和eMule自帶的id3lib庫
和CxImage庫后,就可以開始編譯emule了。而編譯emule也無非就是注意讓它能夠在編譯的時候找到所有的頭文件,以及在鏈接的時候能夠找到所
有的庫。在鏈接的時候能夠找到所有的庫可以通過修改工程文件里面的屬性 ->配置屬性 ->鏈接器
->輸入->附加依賴項來完成。但是能夠找到所有的頭文件反而需要一些技巧了。由于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技術方面的問題。