這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
之前寫了原理篇,在原理篇裡簡單的介紹了一下DHT,但是還不夠詳細。今天我們就專門詳細的講一下嗅探器的核心-DHT,這裡預設原理篇你已經讀了。
背景知識
DHT全稱 Distributed Hash Table,中文翻譯過來就是分布式雜湊表。它是一種去中心化的分布式系統,特點主要有自動去中心化,強大的容錯能力,支援擴充。另外它規定了自己的架構,包括keyspace和overlay network(覆蓋網路)兩部分。但是他沒有規定具體的演算法細節,所以出現了很多不同的實現方式,比如Chord,Pastry,Kademlia等。BitTorrent中的DHT是基於Kademlia的一種變形,它的官方名稱叫做 Mainline DHT。
DHT人如其名,把它看成一個整體,從遠處看它,它就是一張雜湊表,只不過這張表是分布式的,存在於很多機器上。它同時支援set(key, val),get(key)操作。DHT可以用於很多方面,比如Distributed File System,DNS,立即訊息(IM),以及我們最熟悉的點對點檔案分享權限設定(比如BT協議)等。
下面我們提到的DHT預設都是Mainline DHT,例子都是用虛擬碼來表示。讀下面段落的時候要時刻記著,DHT是一個雜湊表。
Mainline DHT
Mainline DHT遵循DHT的架構,下面我們分別從Keyspace和Overlay network兩方面具體說明。
Keyspace
keyspace主要是關於key的一些規定。
Mainline dht裡邊的key長度為160bit,注意是bit,不是byte。在常見的編譯型程式設計語言中,最長的整型也才是64bit,所以用整型是表示不了key的,我們得想其他的方式。我們可以用數組方式表示它,數群組類型你可以選用長度不同的整型,比如int8,int16,int32等。這裡為了下邊方便計算,我們採用長度為20的byte數組來表示。
在mainline dht中,key之間唯一的一種計算是xor,即異或(還記得異或的知識吧?)。我們的key是用長度為20的byte數組來表示,因此我們應該從前往後依次計算兩個key的相對應的byte的異或值,最終結果得到的是另外一個長度為20的byte數組。演算法如下:
for i = 0; i < 20; i++ { result[i] = key1[i] ^ key2[i];}
讀到這裡,你是不是要問xor有啥用?還記得原理篇中DHT的工作方式嗎?
xor是為了找到好友表中離key最近的k個節點,什麼樣的節點最近?就是好友中每個節點和key相異或,得到的結果越小就越近。這裡又衍生另外一個問題,byte數組之間怎麼比較大小?很簡單,從前往後,依次比較每一個byte的大小即可。
在Mainline DHT中,我們用160bit的key來代表每個節點和每個資源的ID,我們尋找節點或者尋找資源的時候實際上就是尋找他們的ID。回想一下,這是不是很雜湊表? :)
另外聰明的你可能又該問了,我們怎麼樣知道每個節點或者每個資源的ID是多少?在Mainline DHT中,節點的ID一般是隨機產生的,而資源的ID是用sha1演算法加密資源的內容後得到的。
OK,關於key就這麼多,代碼實現你可以查考這裡。
Overlay network
Overlay network主要是關於DHT內部節點是怎麼儲存資料的,不同節點之間又是怎樣通訊的。
首先我們回顧一下原理篇中DHT的工作方式:
DHT 由很多節點群組成,每個節點儲存一張表,表裡邊記錄著自己的好友節點。當你向一個節點A查詢另外一個節點B的資訊的時候,A就會查詢自己的好友表,如果裡邊包含B,那麼A就返回B的資訊,否則A就返回距離B距離最近的k個節點。然後你再向這k個節點重新查詢B的資訊,這樣迴圈一直到查詢到B的資訊,查詢到B的資訊後你應該向之前所有查詢過的節點發個通知,告訴他們,你有B的資訊。
整個DHT是一個雜湊表,它把自己的資料化整為零分散在不同的節點裡。OK,現在我們看下,一個節點內部是用什麼樣的資料結構儲存資料的。
節點內部資料存放區 - Routing Table
用什麼樣的資料結構得看支援什麼樣的操作,還得看各種操作的頻繁程度。從上面工作方式我們知道,操作主要有兩個:
首先看到“最近”、“k”,我們會聯想到top k問題。一個很straightforward的做法是,用一個數組儲存節點。這樣的話,我們看下演算法複雜度。top k問題用堆解決,查詢複雜度為O(k + (n-k)*log(k)),當k=8時,接近於O(n);插入操作為O(1)。註:n為一個節點的好友節點總數。
當n很大的時候,操作時間可能會很長。那麼有沒有O(log(n))的演算法呢?
聯想到上面堆的演算法,你可能說,我們可以維護一個堆啊,插入和查詢的時候都是O(log(n))。這種做法堆是根據堆中元素與某一個固定不變的key的距離來維護的,但是通常情況下,我們查詢的key都是變化的,因此這種做法不可行。
那還有其他O(log(n))的演算法嗎?
經驗告訴我們,很多O(log(n))的問題都和二叉樹相關,比如各種平衡二叉樹,我們能不能用二叉樹來解決呢?聯想到每個ID都是一個160bit的值,而且我們知道key之間的距離是通過異或來計算的,並且兩個key的異或結果大小和他們的共同首碼無關,我們應該想到用Trie樹(或者叫首碼樹)來解決。事實上,Mainline DHT協議中用的就是Trie樹,但是與Trie樹又稍微有所不同。在Trie樹裡邊,插入一個key時,我們要比對key的每一個char和Trie裡邊路徑,當不一致時,會立刻分裂成一個子樹。但是在這裡,當不一致時,不會立刻分裂,而是有一個長度為k的buffer(在Mainline DHT中叫bucket)。分兩種情況討論:
如果還沒有理解,你可以參照Kademlia這篇論文上面的圖。
插入的時候,複雜度為O(log(n))。查詢離key最近的k個節點時,我們可以先找到當前key對應的bucket,如果bucket裡邊不夠k個,那麼我們再尋找該節點前驅和後繼,最後根據他們與key的距離拍一下序即可,平均複雜度也為O(log(n))。這樣插入和查詢都是O(log(n))。
代碼實現你可以查考這裡。
節點之間的通訊 - KRPC
KRPC比較簡單,它是一個簡單的rpc結構,其是通過UDP傳送訊息的,報文是由bencode編碼的字典。它包含3種訊息類型,request、response和error。請求又分為四種:ping,find_node, get_peers, announce_peer。
ping 用來偵探對方是否線上
find_node 用來尋找某一個節點ID為Key的具體資訊,資訊裡包括ip,port,ID
get_peers 用來尋找某一個資源ID為Key的具體資訊,資訊裡包含可提供下載該資源的ip:port列表
announce_peer 用來告訴別人自己可提供某一個資源的下載,讓別人把這個訊息儲存起來。還記得Angelababy那個例子嗎?在我得到她的號後,我會通知所有我之前問過的人,他們就會把我有Angelababy號這個資訊儲存起來,以後如果有人再問他們有沒有Angelababy號的話,他們就會告訴那個人我有。BT種子嗅探器就是根據這個來得到訊息的,不過得到訊息後我們還需要進一步下載。
跳出節點,整體看DHT這個雜湊表,find_node和get_peers就是我們之前說的get(key),announce_peer就是set(ke, val)。
剩下的就是具體的訊息格式,你可以在官方文檔上看到,這裡就不搬磚了。
實現KRPC時,需要注意的有以下幾點:
每次收到請求或者回複你都需要根據情況更新你的Routing Table,或儲存或丟掉。
你需要實現transaction,transaction裡邊要包含你的請求資訊以及被請求的ip及連接埠,只有這樣當你收到回複訊息時,你才能根據訊息的transaction id做出正確的處理。Mainline DHT對於如何?transaction沒有做具體規定。
一開始你是不在DHT網路中的,你需要別人把你介紹進去,任何一個在DHT中的人都可以。一般我們可以向 router.bittorrent.com:6881、 dht.transmissionbt.com:6881 等發送find_node請求,然後我們的DHT就可以開始工作了。
KRPC的實現你可以參考這裡。
總結
DHT整體就是一張雜湊表,首先我們本身是裡邊的一個節點,我們向別人發送krpc find_node或get_peers訊息,就是在對這個雜湊表執行get(key)操作。向別人發送announce_peer訊息,就是在對這個雜湊表執行set(key, val)操作。
最後
https://github.com/shiyanhui/dht 完整代碼在這裡,喜歡這篇文章的話就到github上給個Star唄 :)
http://bthub.io 是基於上面這個嗅探器寫的一個BT種子搜尋引擎。
有任何問題可以在這裡提問:https://github.com/shiyanhui/dht/issues
OK,今天就說到這裡,關於怎麼樣下載,我們下篇再說。
你可以關注我的公眾號,及時獲得下一篇推送。