上一篇博客中,客戶端已連接到ed2k網絡及客戶端與服務器交互的eMule源碼梳理,這裏將開始搜索資源並下載及客戶端與客戶端交互的eMule源碼梳理
emule 源碼下載地址 http://download.csdn.net/detail/huang_rong12/9506732搜索資源並下載,這是一個即包含和和服務器交互還包含與另一些客戶端交互。所以會先說明和服務端交互的情況(搜索資源,選擇資源),在說明下載(與客戶端交互)。
搜索資源是發送關鍵詞到服務器,服務器會根據關鍵詞返回信息,當然這些信息是通過消息傳遞的具體消息如下:
6.2.9 搜索請求
客戶發送給服務器的。消息使用用戶的一個搜索字符串搜索一個文件。消息大小是可變的。搜索字符串包括布爾運算符’AND’,’OR’, ’NOT’。用戶可以詳細設置文件的類型大小也可以設置開始位置(例如:展示給我至少5個其他客戶端的結果)。
名稱 |
佔用字節數 |
默認值 |
說明 |
Protocol |
1 |
0xE3 |
|
Size |
4 |
|
不包括標題和size字段的信息大小 |
Type |
1 |
0x16 |
OP_SEARCHRESULT opcode |
Parsed search string |
不定 |
NA |
下面所描述的搜索字符串結構 |
Result list |
不定 |
NA |
搜索結果列表 |
Parsed search string |
不定 |
NA |
解析搜索字符串,格式如下 |
File Type Constraint |
不定 |
NA |
可選的。一個字符串約束。字符串值是Audio”, ”Video”, ”Pro” or ”Image”三者之一。類型域分別對應0x1 0x0 0x3 |
Min Size Constraint |
不定 |
NA |
可選擇的,一個整型約束。以兆字節計算的文件大小。類型域有4位:0x1 0x1 0x0 0x2 |
Max Size constraint |
不定 |
NA |
可選擇的,一個整型約束。以兆字節計算的文件大小。類型域有4位:0x2 0x1 0x0 0x2 |
Availability Constraint |
不定 |
NA |
可選擇的,一個整型約束。搜索文件客戶數量的最小上限,類型域有4位:0x1 0x10x0 0x15 |
Filename Extension constrain |
不定 |
NA |
可選擇的,一個整型約束。類型域有3位:0x1 0x0 0x3 |
解析搜索字符串格式
解析字符串編碼是通過二差樹和’AND’,’OR’ ,’NOT’布爾運算符以及字符串操作數。二差樹是按照先序編碼進行的。操作是編碼兩位字節值是
The tree is encoded in pre-order . The operators0x0, 0x100, 0x200分別代表了’AND’, ’OR’ 和’NOT’。字符串按照TLV格式進行編碼是一個以字節的值和一個兩字節的長度。注意當字符串是以各自的時候它代表字符串操作(沒有運算符)。以後的eMule 編碼版本中指通過單獨的字符串’AND’來編碼搜索表達式,由空格代替’AND’。由’AND’運算符將連續的單詞分開來組成一個句子。
可選擇的約束格式
約束數列條目。每一個條目有’AND’描述符開始(2-byte0x00)緊跟着是編碼約束。因此一個完整的搜索行格式是:<’search-string’ AND constraint1 AND constraint2 etc>就想下面圖例中描述的一樣。編碼被分成三部分。
1.類型-一個字節的描述,表明是字符串(0x2)還是整型約束(0x3)。
2.值-一個定長字符串編碼或者一個4字節的整型值
3.類型-一個3或4字節的約束類型描述(看上面的主要表)
圖 6.1: 查找字符串編碼實例
6.2.10 搜索結果
服務器發送給客戶的搜索請求應答消息。這個消息通常是壓縮的。大小不固定。
名稱 |
佔用字節數 |
默認值 |
說明 |
Protocol |
1 |
0xE3 |
|
Size |
4 |
|
不包括標題和size字段的信息大小 |
Type |
1 |
0x16 |
OP_SEARCHRESULT opcode |
Result Count |
4 |
NA |
這個消息中搜索結果的個數 |
Result list |
不定 |
NA |
搜索結果列表 |
搜索結果列表項目的格式
下面的表格描述了一個搜索結果列表項目的格式。每一個搜索結果包含了一個唯一表示這個文件的哈西數和兩外一個又有該文件的客戶。還有幾個描述文件屬性的標記。這些標記將在下面描述:
名稱 |
佔用字節數 |
默認值 |
說明 |
File Hash |
16 |
NA |
一個哈西值,唯一的標示了一個文件 |
Client ID |
4 |
NA |
一個擁有該文件的peer的客戶ID |
Client Port |
2 |
0x16 |
擁有文的客戶端得端口號 |
Tag Count |
4 |
NA |
後面的描述標記的個數 |
Tag list |
不定 |
NA |
描述標記列表 |
注意大多數標記的位置是可以變動的,並不是固定的。標記編碼的規則在本章開始處已經詳細描述了。
名稱 |
標記名稱 |
標記類型 |
說明 |
File name |
Integer 0x01 |
String |
|
File size |
Integer 0x02 |
Integer |
|
File type |
Integer 0x03 |
String |
|
File format |
Integer 0x04 |
String |
|
Sources |
Integer 0x15 |
Integer |
這個文件可獲得的資源數 |
Artist |
String ”Artist” |
String |
|
Album |
String ”Album” |
String |
|
Title |
String ”Title” |
String |
|
Length |
String ”length” |
Integer |
|
Bitrate |
String ”bitrate” |
Intege |
|
Codec |
String ”codec” |
Integer |
|
表6.1: 搜索結果標記列表
在eMule源碼中和搜索相關的代碼分析如下
搜索信息集-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只負責和搜索有關的信息的儲存和讀取,本身並不進行搜索。
得到返回數據以後就可以下載了。這時候涉及的消息如下:
6.2.11 獲得資源
客戶向服務器發送一個請求來獲得文件資源(其他的客戶)。消息的大小是22字節。
名稱 |
佔用字節數 |
默認值 |
說明 |
Protocol |
1 |
0xE3 |
|
Size |
4 |
|
不包括標題和size字段的信息大小 |
Type |
1 |
0x19 |
OP_ GETSOURCES opcode |
File hash |
16 |
NA |
請求的文件哈西數 |
6.2.12 建立資源
服務器發送給客戶端一個包含客戶端所請求的文件資源(其他的客戶)的消息。消息大小是不定的。
名稱 |
佔用字節數 |
默認值 |
說明 |
Protocol |
1 |
0xE3 |
|
Size |
4 |
|
不包括標題和size字段的信息大小 |
Type |
1 |
0x42 |
OP_ FOUNDSOURCES opcode |
File hash |
16 |
NA |
請求的文件哈西數 |
Sources Count |
1 |
NA |
這個消息中的資源數 |
List of sources |
不定 |
NA |
資源列表 |
資源列表項目的格式
下面的表格描述了資源列表項目的格式。每一個資源包括客戶所擁有要下載文件的細節。
名稱 |
佔用字節數 |
默認值 |
說明 |
Client ID |
4 |
NA |
擁有文件的eMule peer得Client ID |
Client Port |
2 |
NA |
擁有文件的客戶的端口 |
這些消息完成以後文件下載基本結束了(以上這些是必須的消息,還有一些消息是沒介紹到的。)。前面已說明客戶端與服務器交互的源碼分析。這裏不再說明,這裏僅說明客戶端與客戶端的交互的eMule源碼。
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類負責的是從邏輯上對網絡另一邊的一個客戶端進行表達。由於ed2k網絡中文件是放在網絡中的客戶端上,當一個客戶端請求下載資源時,整個過程中可能會涉及網絡中多個客戶端要上傳文件(之所以是多個,是由於文件在ed2k網絡中可能是分段的,至於分段詳細細節後面會詳細說明。)而且同一個客戶端可能要把文件上傳到不同另外的客戶端(這時就會出現排隊問題,先傳給哪個)。接下來對於這些問題,會一個個解釋,在ed2k中有一種信用機制,爲了避免只下載不分享的問題,ed2k中分享文件個數與信用掛鉤,所以出現排隊時會選擇信用較高的先上傳。
下載任務即部分文件的表示
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。
最後提一下它的Process方法。該方法是emule中爲了儘量減少線程的使用而採取的一種有一些類似於輪詢的機制。其它很多類中也有Process方法,這個方法要做的事情就是在一些和日常運行有關的事情,例如檢查爲了下載該文件而鏈接到自己的各個客戶端的狀態,向它們發送下載請求等。
下載任務隊列
CDownloadQueue是下載隊列類。這個隊列中的項目是CPartFile指針。因此和emule中出現的很多其它的列表類一樣,它需要能夠提供對這個列表中的元素進行增加,查詢,刪除的功能。例如查詢的時候能夠根據該文件的hashID或者索引來進行查詢。CDownloadQueue同時還要完成一些統計工作。
和其它的列表類不一樣的是,它的所有元素的信息並不是集中存放於一個文件,而是對應於每一個下載任務,單獨得存放在一個元信息文件(.part.met)中,因此當該類進行初始化的時候,它需要尋找所有可能的下載路徑,從那些路徑中找到所有的.part.met文件,並且試圖用這些文件來生成CPartFile類,並且將這些通過.part.met文件正確生成的CPartFile類添加到自己的列表中,同樣,在退出時,所有的下載任務的元信息也是自行保存,不會合成爲一個文件。
CDownloadQueue中的Process方法的主要任務就是把它的列表中的CPartFile類中的Process方法都調一遍,另外主要的一些關於下載情況的統計信息也是在每一輪的Process後進行更新的。從這裏我們也可以看出Process方法在emule中的意義,就是一個需要經常執行的方法,通過經常執行它們來完成日常工作,而且所有的這些Process方法肯定是順序執行,因此可以減少很多多線程的同步之類的問題。emule中已經儘量減少了多線程的使用,但是在很多地方如果多線程是不可避免的話,也不會排斥。
上傳任務隊列
CUploadQueue是上傳隊列類。這個列表類中只有以CUpDownClient爲元素的列表,它和其它列表類還有一個很大的不同就是它所保存的信息都不需要持久化,即不需要在當前的emule退出後還記住自己正在給誰上傳文件,然後下次上線的時候再繼續給他們傳,這在大部分情況下是沒有意義的。
上傳隊列類列表中有兩個列表,上傳列表和排隊列表。當一個收到一個新的下載請求後,它會把對應的客戶端先添加到排隊列表中,以後再根據情況,把它們不斷添加到上傳列表中。在這裏,信譽機制將會對此產生影響。
CUploadQueue的Process方法就相對簡單了,那就是向上傳隊列中的所有客戶端依次發送數據,而排隊的客戶端是不會得到這個機會的。另外它還需要完成關於上傳方面的一些統計信息。
另外我們還需要注意在CUploadQueue的構造函數裏面,創建了一個以100毫秒爲間隔的定時器,這個定時器成爲以上所有的Process所需要的基礎。我們看它的UploadTimer就可以看出這一點。這裏面充斥了各個類的Process方法的執行,其中包括以前我們提到的一些類,但是沒有提到它們的Process方法,因爲其過於簡單,基本上就只是更新了一下要保存的信息。
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協議進行包裝,以便使它能夠儘可能地穿越更多的網絡的防火牆。
emule常規部分小結
emule中還有其它的很多類,它們使得emule的功能更加的強大和完善。有很多類在前面沒有提到,但是不代表它沒有作用。而且即時是前面提到的類也只是大體的介紹,它們之間互相配合的一些細節沒有體現。但是這些細節應該已經可以通過對它們的大體的功能的瞭解而更加容易被把握。至於GUI的設計,它也最終是要對應到某個功能實現類的數據的。
對於emule中的通信協議只是大體得描述了一下它的數據包的格式,但是並沒有詳細得描述它的每一個Opcode對應的包的意義,因爲我認爲這是沒有必要的,在知道通信協議的格式以及處理它們的代碼所在的位置後,可以很簡單的通過追蹤某條消息的前因後果把整個通信協議都分析出來。
這裏再稍微提一下在emule中使用到的其它類及其功能。我們可以看到,如果單純只是爲了能夠搜到以及下載到文件的話,有不少類是可以精簡的,但是,正是由於它們的存在,使得emule的功能更加的完善。CIPFilter,IP地址過濾器,通過識別各種類型的IP地址過濾信息,它能夠把不希望連接的網絡地址過濾掉,emule中所有需要連接網絡的地方使用的都是統一的過濾數據。CWebServer能夠在本地打開一個Web服務器,然後你可以通過瀏覽器來控制你的emule。CScheduler能夠實現下載任務的定時下載。CPeerCacheFinder爲前面提到的PeerCache技術的主控制類。另外,emule還內置了一個IRC客戶端,一個主要成員函數都爲靜態的CPartFileConvert類,能夠對其它版本的驢的下載文件進行轉換。它甚至還提供了一個自動處理zip和rar的類CArchiveRecovery。
到這裏已經將eMule中ed2k資源的基本下載過程介紹完畢,但是eMule中還有一些機制(分段及信譽)。後面會針對這些機制進行分析梳理。