作了一個磁力連接和BT種子的搜索引擎 {Magnet & Torrent},所以把 DHT 協議從新看了一遍。html
BitTorrent 使用"分佈式哈希表"(DHT)來爲無 tracker 的種子(torrents)存儲 peer 之間的聯繫信息。這樣每一個 peer 都成了 tracker。這個協議基於 Kademila[1] 網絡而且在 UDP 上實現。node
請注意本文檔中使用的術語,以避免混亂。服務器
DHT 由節點組成,它存儲了 peer 的位置。BitTorrent 客戶端包含一個 DHT 節點,這個節點用來聯繫 DHT 中其餘節點,從而獲得 peer 的位置,進而經過 BitTorrent 協議下載。網絡
每一個節點有一個全局惟一的標識符,做爲 "node ID"。節點 ID 是一個隨機選擇的 160bit 空間,BitTorrent infohash[2] 也使用這樣的 160bit 空間。
"距離"用來比較兩個節點 ID 之間或者節點 ID 和 infohash 之間的"遠近"。節點必須維護一個路由表,路由表中含有一部分其它節點的聯繫信息。其它節點距離本身越近時,路由表信息越詳細。所以每一個節點都知道 DHT 中離本身很"近"的節點的聯繫信息,而離本身很是遠的 ID 的聯繫信息卻知道的不多。dom
在 Kademlia 網絡中,距離是經過異或(XOR)計算的,結果爲無符號整數。distance(A, B) = |A xor B|
,值越小表示越近。分佈式
當節點要爲 torrent 尋找 peer 時,它將本身路由表中的節點 ID 和 torrent 的 infohash 進行"距離對比"。而後向路由表中離 infohash 最近的節點發送請求,問它們正在下載這個 torrent 的 peer 的聯繫信息。若是一個被聯繫的節點知道下載這個 torrent 的 peer 信息,那個 peer 的聯繫信息將被回覆給當前節點。不然,那個被聯繫的節點則必須回覆在它的路由表中離該 torrent 的 infohash 最近的節點的聯繫信息。最初的節點重複地請求比目標 infohash 更近的節點,直到不能再找到更近的節點爲止。查詢完了以後,客戶端把本身做爲一個 peer 插入到全部回覆節點中離種子最近的那個節點中。搜索引擎
請求 peer 的返回值包含一個不透明的值,稱之爲"令牌(token)"。若是一個節點宣佈它所控制的 peer 正在下載一個種子,它必須在回覆請求節點的同時,附加上對方向咱們發送的最近的"令牌(token)"。這樣當一個節點試圖"宣佈"正在下載一個種子時,被請求的節點核對令牌和發出請求的節點的 IP 地址。這是爲了防止惡意的主機登記其它主機的種子。因爲令牌僅僅由請求節點返回給收到令牌的同一個節點,因此沒有規定他的具體實現。可是令牌必須在一個規定的時間內被接受,超時後令牌則失效。在 BitTorrent 的實現中,token 是在 IP 地址後面鏈接一個 secret(一般是一個隨機數),這個 secret 每五分鐘改變一次,其中 token 在十分鐘之內是可接受的。編碼
每一個節點維護一個路由表保存已知的好節點。路由表中的節點是用來做爲在 DHT 中請求的起始點。路由表中的節點是在不斷的向其餘節點請求過程當中,對方節點回復的。code
並非咱們在請求過程當中收到得節點都是平等的,有的節點是好的,而另外一些則不是。許多使用 DHT 協議的節點均可以發送請求並接收回復,可是不能主動回覆其餘節點的請求。節點的路由表只包含已知的好節點,這很重要。好節點是指在過去的 15 分鐘之內,曾經對咱們的某一個請求給出過回覆的節點,或者曾經對咱們的請求給出過一個回覆(不用在15分鐘之內),而且在過去的 15 分鐘給咱們發送過請求。上述兩種狀況均可將節點視爲好節點。在 15 分鐘以後,對方沒有上述 2 種狀況發生,這個節點將變爲可疑的。當節點不能給咱們的一系列請求給出回覆時,這個節點將變爲壞的。相比那些未知狀態的節點,已知的好節點會被給於更高的優先級。orm
路由表覆蓋從 0 到 2^160 所有的節點 ID 空間。路由表又被劃分爲桶(bucket),每一個桶包含一部分的 ID 空間。空的路由表只有一個桶,它的 ID 範圍從 min=0 到 max=2^160。當 ID 爲 N
的節點插入到表中時,它將被放到 ID 範圍在 min <= N < max
的 桶 中。空的路由表只有一個桶,因此全部的節點都將被放到這個桶中。每一個桶最多隻能保存 K 個節點,當前 K=8。當一個桶放滿了好節點以後,將再也不容許新的節點加入,除非咱們自身的節點 ID 在這個桶的範圍內。在這樣的狀況下,這個桶將被分裂爲 2 個新的桶,每一個新桶的範圍都是原來舊桶的一半。原來舊桶中的節點將被從新分配到這兩個新的桶中。若是一個新表只有一個桶,這個包含整個範圍的桶將總被分裂爲 2 個新的桶,每一個桶的覆蓋範圍從 0..2^159 和 2^159..2^160。
當桶裝滿了好節點,新的節點會被丟棄。一旦桶中的某個節點變爲了壞的節點,那麼咱們就用新的節點來替換這個壞的節點。若是桶中有在 15 分鐘內都沒有活躍過的節點,咱們將這樣的節點視爲可疑的節點,這時咱們向最久沒有聯繫的節點發送 ping。若是被 ping 的節點給出了回覆,那麼咱們向下一個可疑的節點發送 ping,不斷這樣循環下去,直到有某一個節點沒有給出 ping 的回覆,或者當前桶中的全部節點都是好的(也就是全部節點都不是可疑節點,他們在過去 15 分鐘內都有活動)。若是桶中的某個節點沒有對咱們的 ping 給出回覆,咱們最好再試一次(再發送一次 ping,由於這個節點也許仍然是活躍的,但因爲網絡擁塞,因此發生了丟包現象,注意 DHT 的包都是 UDP 的),而不是當即丟棄這個節點或者直接用新節點來替代它。這樣,咱們得路由表將充滿穩定的長時間在線的節點。
每一個桶都應該維持一個 lastchange
字段來代表桶中節點的"新鮮"度。當桶中的節點被 ping 並給出了回覆,或者一個節點被加入到了桶,或者一個節點被新的節點所替代,桶的 lastchange
字段都應當被更新。若是一個桶的 lastchange
在過去的 15 分鐘內都沒有變化,那麼咱們將更新它。這個更新桶操做是這樣完成的:從這個桶所覆蓋的範圍中隨機選擇一個 ID,並對這個 ID 執行 find_nodes
查找操做。經常收到請求的節點一般不須要經常更新本身的桶,反之,不經常收到請求的節點經常須要週期性的執行更新全部桶的操做,這樣才能保證當咱們用到 DHT 的時候,裏面有足夠多的好的節點。
在插入第一個節點到路由表並啓動服務後,這個節點應試着查找 DHT 中離本身更近的節點,這個查找工做是經過不斷的發出 find_node
消息給愈來愈近的節點來完成的,當不能找到更近的節點時,這個擴散工做就結束了。路由表應當被啓動工做和客戶端軟件保存(也就是啓動的時候從客戶端中讀取路由表信息,結束的時候客戶端軟件記錄到文件中)。
BitTorrent 協議已經被擴展爲能夠在經過 tracker 獲得的 peer 之間互相交換節點的 UDP 端口號(也就是告訴對方咱們的 DHT 服務端口號),在這樣的方式下,客戶端能夠經過下載普通的種子文件來自動擴展 DHT 路由表。新安裝的客戶端第一次試着下載一個無 tracker 的種子時,它的路由表中將沒有任何節點,這是它須要在 torrent 文件中找到聯繫信息。
peers 若是支持 DHT 協議就將 BitTorrent 協議握手消息的保留位的第 8 字節的最後一位置爲 1
。這時若是 peer 收到一個 handshake
代表對方支持 DHT 協議,就應該發送 PORT 消息。它由字節 0x09
開始,payload
的長度是 2 個字節,包含了這個 peer 的 DHT 服務使用的網絡字節序的 UDP 端口號。當 peer 收到這樣的消息是應當向對方的 IP 和消息中指定的端口號的節點發送 ping。若是收到了 ping 的回覆,那麼應當使用上述的方法將新節點的聯繫信息加入到路由表中。
一個無 tracker 的 torrent 文件字典不包含 announce
關鍵字,而使用 nodes
關鍵字來替代。這個關鍵字對應的內容應該設置爲 torrent 建立者的路由表中 K 個最接近的節點。可供選擇的,這個關鍵字也能夠設置爲一個已知的可用節點,好比這個 torrent 文件的建立者。請不要自動加入 router.bittorrent.com
到 torrent 文件中或者自動加入這個節點到客戶端路由表中。
[["<host>", <port>], ["<host>", <port>], ...]
[["127.0.0.1", 6881], ["your.router.node", 4804]]
KRPC 協議是由 bencode 編碼組成的一個簡單的 RPC 結構,他使用 UDP 報文發送。一個獨立的請求包被髮出去而後一個獨立的包被回覆。這個協議沒有重發。它包含 3 種消息:請求,回覆和錯誤。對DHT協議而言,這裏有 4 種請求:ping
,find_node
,get_peers
和 announce_peer
。
一條 KRPC 消息由一個獨立的字典組成,其中有 2 個關鍵字是全部的消息都包含的,其他的附加關鍵字取決於消息類型。每條消息都包含 t
關鍵字,它是一個表明了 transaction ID 的字符串類型。transaction ID 由請求節點產生,而且回覆中要包含回顯該字段,因此回覆可能對應一個節點的多個請求。transaction ID 應當被編碼爲一個短的二進制字符串,好比 2 個字節,這樣就能夠對應 2^16 個請求。另外每一個 KRPC 消息還應該包含的關鍵字是 y
,它由一個字節組成,代表這個消息的類型。y
對應的值有三種狀況:q
表示請求,r
表示回覆,e
表示錯誤。
Peers 的聯繫信息被編碼爲 6 字節的字符串。又被稱爲 "CompactIP-address/port info",其中前 4 個字節是網絡字節序的 IP 地址,後 2 個字節是網絡字節序的端口。
節點的聯繫信息被編碼爲 26 字節的字符串。又被稱爲 "Compactnode info",其中前 20 字節是網絡字節序的節點 ID,後面 6 個字節是 peers 的 "CompactIP-address/port info"。
請求,對應於 KPRC 消息字典中的 y
關鍵字的值是 q
,它包含 2 個附加的關鍵字 q
和 a
。關鍵字 q
是字符串類型,包含了請求的方法名字。關鍵字 a
一個字典類型包含了請求所附加的參數。
回覆,對應於 KPRC 消息字典中的 y
關鍵字的值是 r
,包含了一個附加的關鍵字 r
。關鍵字 r
是字典類型,包含了返回的值。發送回覆消息是在正確解析了請求消息的基礎上完成的。
錯誤,對應於 KPRC 消息字典中的 y
關鍵字的值是 e
,包含一個附加的關鍵字 e
。關鍵字 e
是列表類型。第一個元素是數字類型,代表了錯誤碼。第二個元素是字符串類型,代表了錯誤信息。當一個請求不能解析或出錯時,錯誤包將被髮送。下表描述了可能出現的錯誤碼:
錯誤包例子 Example Error Packets:
{"t":"aa", "y":"e", "e":[201, "A Generic Error Ocurred"]}
d1:eli201e23:A Generic Error Ocurrede1:t2:aa1:y1:ee
全部的請求都包含一個關鍵字 id
,它包含了請求節點的節點 ID。全部的回覆也包含關鍵字id
,它包含了回覆節點的節點 ID。
最基礎的請求就是 ping。這時 KPRC 協議中的 "q" = "ping"
。Ping 請求包含一個參數 id
,它是一個 20 字節的字符串包含了發送者網絡字節序的節點 ID。對應的 ping 回覆也包含一個參數 id
,包含了回覆者的節點 ID。
{"id" : "<querying nodes id>"}
{"id" : "<queried nodes id>"}
報文包例子 Example Packets
{"t":"aa", "y":"q", "q":"ping", "a":{"id":"abcdefghij0123456789"}}
d1:ad2:id20:abcdefghij0123456789e1:q4:ping1:t2:aa1:y1:qe
{"t":"aa", "y":"r", "r": {"id":"mnopqrstuvwxyz123456"}}
d1:rd2:id20:mnopqrstuvwxyz123456e1:t2:aa1:y1:re
find_node
被用來查找給定 ID 的節點的聯繫信息。這時 KPRC 協議中的 "q" == "find_node"
。find_node
請求包含 2 個參數,第一個參數是 id
,包含了請求節點的ID。第二個參數是 target
,包含了請求者正在查找的節點的 ID。當一個節點接收到了 find_node
的請求,他應該給出對應的回覆,回覆中包含 2 個關鍵字 id
和 nodes
,nodes
是字符串類型,包含了被請求節點的路由表中最接近目標節點的 K(8) 個最接近的節點的聯繫信息。
{"id" : "<querying nodes id>", "target" : "<id of target node>"}
{"id" : "<queried nodes id>", "nodes" : "<compact node info>"}
報文包例子 Example Packets
{"t":"aa", "y":"q", "q":"find_node", "a": {"id":"abcdefghij0123456789", "target":"mnopqrstuvwxyz123456"}}
d1:ad2:id20:abcdefghij01234567896:target20:mnopqrstuvwxyz123456e1:q9:find_node1:t2:aa1:y1:qe
{"t":"aa", "y":"r", "r": {"id":"0123456789abcdefghij", "nodes": "def456..."}}
d1:rd2:id20:0123456789abcdefghij5:nodes9:def456...e1:t2:aa1:y1:re
get_peers
與 torrent 文件的 infohash 有關。這時 KPRC 協議中的 "q" = "get_peers"
。get_peers
請求包含 2 個參數。第一個參數是 id
,包含了請求節點的 ID。第二個參數是 info_hash
,它表明 torrent 文件的 infohash。若是被請求的節點有對應 info_hash
的 peers,他將返回一個關鍵字 values
,這是一個列表類型的字符串。每個字符串包含了 "CompactIP-address/portinfo"
格式的 peers 信息。若是被請求的節點沒有這個 infohash 的 peers,那麼他將返回關鍵字 nodes
,這個關鍵字包含了被請求節點的路由表中離 info_hash
最近的 K 個節點,使用 "Compactnodeinfo"
格式回覆。在這兩種狀況下,關鍵字 token
都將被返回。token
關鍵字在從此的 annouce_peer
請求中必需要攜帶。token
是一個短的二進制字符串。
{"id" : "<querying nodes id>", "info_hash" : "<20-byte infohash of target torrent>"}
{"id" : "<queried nodes id>", "token" :"<opaque write token>", "values" : ["<peer 1 info string>", "<peer 2 info string>"]}
{"id" : "<queried nodes id>", "token" :"<opaque write token>", "nodes" : "<compact node info>"}
報文包例子 Example Packets:
{"t":"aa", "y":"q", "q":"get_peers", "a": {"id":"abcdefghij0123456789", "info_hash":"mnopqrstuvwxyz123456"}}
d1:ad2:id20:abcdefghij01234567899:info_hash20:mnopqrstuvwxyz123456e1:q9:get_peers1:t2:aa1:y1:qe
{"t":"aa", "y":"r", "r": {"id":"abcdefghij0123456789", "token":"aoeusnth", "values": ["axje.u", "idhtnm"]}}
d1:rd2:id20:abcdefghij01234567895:token8:aoeusnth6:valuesl6:axje.u6:idhtnmee1:t2:aa1:y1:re
{"t":"aa", "y":"r", "r": {"id":"abcdefghij0123456789", "token":"aoeusnth", "nodes": "def456..."}}
d1:rd2:id20:abcdefghij01234567895:nodes9:def456...5:token8:aoeusnthe1:t2:aa1:y1:re
這個請求用來代表發出 announce_peer
請求的節點,正在某個端口下載 torrent 文件。announce_peer
包含 4 個參數。第一個參數是 id
,包含了請求節點的 ID;第二個參數是 info_hash
,包含了 torrent 文件的 infohash;第三個參數是 port
包含了整型的端口號,代表 peer 在哪一個端口下載;第四個參數數是 token
,這是在以前的 get_peers
請求中收到的回覆中包含的。收到 announce_peer
請求的節點必須檢查這個 token
與以前咱們回覆給這個節點 get_peers
的 token
是否相同。若是相同,那麼被請求的節點將記錄發送 announce_peer
節點的 IP 和請求中包含的 port 端口號在 peer 聯繫信息中對應的 infohash 下。
{"id" : "<querying nodes id>", "implied_port": <0 or 1>, "info_hash" : "<20-byte infohash of target torrent>", "port" : <port number>, "token" : "<opaque token>"}
{"id" : "<queried nodes id>"}
報文包例子 Example Packets:
{"t":"aa", "y":"q", "q":"announce_peer", "a": {"id":"abcdefghij0123456789", "implied_port": 1, "info_hash":"mnopqrstuvwxyz123456", "port": 6881, "token": "aoeusnth"}}
d1:ad2:id20:abcdefghij01234567899:info_hash20:<br /> mnopqrstuvwxyz1234564:porti6881e5:token8:aoeusnthe1:q13:announce_peer1:t2:aa1:y1:qe
{"t":"aa", "y":"r", "r": {"id":"mnopqrstuvwxyz123456"}}
d1:rd2:id20:mnopqrstuvwxyz123456e1:t2:aa1:y1:re
This document has been placed in the public domain.