DHT协议介绍
DHT協議介紹
- 1、前言
- 2、DHT協議介紹
- 2.1、概述
- 2.2、路由表
- 2.3、BitTorret協議擴展
- 2.4、Torrent文件擴展
- 2.5、KRPC協議
- 2.6、聯系信息編碼
- 2.7、DHT請求
- 2.7.1、ping
- 2.7.2、find_node
- 2.7.3、get_peers
- 2.7.4、announce_peer
1、前言
英文版官方地址:http://www.bittorrent.org/beps/bep_0005.html
DHT協議BitTorrent使用一種叫做分布式哈希表(distributedsloppy hashtable)的技術,來實現在無tracker的torrent文件中peer的聯系信息存儲。這個時候,每個peer都是一個tracker。這個協議是基于Kademila協議的,并且在UDP協議基礎上實現。
請注意本文檔所使用的術語以免引起混淆。“peer”是一個實現了BT協議,且正在監聽TCP端口的client/server。“node”是實現了DHT協議的,且正在監聽UDP端口的client/server。DHT由nodes組成并保存peer的位置信息。BitTorrent客戶端也包括DHTnode,這個DHTnode主要是用來聯系DHT中的其他nodes,以得peer的位置信息,從而通過BitTorrent協議下載。
2、DHT協議介紹
2.1、概述
每一個node都有一個全局的唯一標識“nodeID”。NodeIDS的產生是隨機的,且使用與BitTorrent的infohashes相同的160-bit空間。“distancemetric”用來比較2個nodeIDs或者nodeID與infohash的接近程度。Nodes必須維護一個路由表,其中保存了一部分其他nodes的聯系信息。越接近自身節點時,路由表的信息會更加詳細。nodes保存了很多接近自己的節點,但是離自己很遠的節點的聯系信息確知道得很少。
在Kademlia中,“distancemetric”采用XOR異或計算,并轉換為一個無符號整數。distance(A,B)= |A xor B| ,并且距離越小表示2個節點越接近。
當一個node想得到某個torrent文件的peers,它首先使用distancemetric來比較torrent文件的info_hash和路由表中節點的nodeID。接下來向路由表中nodeID與info_hash最接近的那些節點發送請求,得到當前正在下載這個torrent文件數據的peers的聯系信息。如果被請求的節點知道這個torrent文件的peers,那么peer的聯系信息將包含在回復中。否則,被請求的節點必須返回他的路由表中更接近info_hash得那些節點。原始的請求node不斷向新獲得的那些node中,更接近目標info_hash的那些node發送請求,直到不能獲得更近的nodes。當查找結束時,client將自己的信息作為一個peer插入到在剛才請求中給出回復的那些節點中,nodeid與info_hash最接近的哪個節點上,這樣,哪個節點又多保存了一個peer信息。
在請求peers的時候,對方給我們的回復必須還包含一個不透明的令牌,我們稱他為“token”。這樣當我們宣布我們正在下載某個torrent,想讓對方保存我們的信息時,我們必須使用對方向我們發送的最近的一個token。這樣當我們宣布我們在下載一個torrent時,被請求的node檢查這個token和IP是否與之前他們向我們回復的一樣。這樣是為了防止惡意的攻擊。由于token僅僅由請求的節點返回,所以我們不規定他的具體實現。但是token必須有一個可接受的時間范圍,超過這個時間,token將失效。在BitTorrent的實現中,token是在IP地址后面連接一個secret(可以視為一個隨機數),這個secret每五分鐘改變一次,其中token在十分鐘以內是可接受的。
2.2、路由表
每一個node維護一個路由表保存已知的好節點。這些路由表中的nodes被作為DHT請求的起始節點。路由表中的nodes是在不斷的向其他node請求過程中,對方節點回復的。
并不是我們在請求過程中收到得節點都是平等的,有的node是好的,而有的node是死掉的。很多使用DHT協議的nodes都可以發送請求并接收回復,但是不能主動回復其他節點的請求(我認為這是由于防火墻或者NAT的原因)。對每一個node的路由表,只包含好的nodes是很重要的。好的node是指在過去的15分鐘以內,曾經對我們的某一個請求給出過回復的節點;或者曾經對我們的請求給出過一個回復(不用在15分鐘以內),并且在過去的15分鐘給我們發送過請求。上述兩種情況都可將node視為好的node。在15分鐘之后,對方沒有上述2種情況發生,這個node將變為可疑的。當nodes不能給我們的一系列請求給出回復時,這個節點將變為壞的。相比未知狀態的nodes,我們將給好的節點更高的優先權。
路由表覆蓋從0到2160完整的nodeID空間。路由表又被劃分為buckets(桶),每一個bucket包含一個子部分的nodeID空間。一個空的路由表只有一個bucket,它的ID范圍從min=0到max=2160。當一個nodeID為“N”的node插入到表中時,它將被放到ID范圍在min<=N<max的bucket中。一個空的路由表只有一個bucket所以所有的node都將被放到這個bucket中。每一個bucket最多只能保存K個nodes,當前K=8。當一個bucket放滿了好的nodes之后,將不再允許新的節點加入,除非我們自身的nodeID在這個bucket的范圍內。在這樣的情況下,這個bucket將被分裂為2個新的buckets,每一個新桶的范圍都是原來舊桶的一半。原來舊桶中的nodes將被重新分配到這兩個新的buckets中。如果是一個只有一個bucket的新表,這個包含整個范圍的bucket將總被分裂為2個新的buckets,第一個的覆蓋范圍從0…2159,第二個的范圍從2159…2160。
當bucket裝滿了好的nodes,那么新的node將被丟棄。一旦bucket中的某一個node變為了壞的node,那么我們就用新的node來替換這個壞的node。如果bucket中有在15分鐘內都沒有活躍過的節點,我們將這樣的節點視為可疑的節點,這時我們向最久沒有聯系的節點發送ping。如果被pinged的節點給出了回復,那么我們向下一個可疑的節點發送ping,不斷這樣循環下去,直到有某一個node沒有給出ping的回復,或者當前bucket中的所有nodes都是好的(也就是所有nodes都不是可疑nodes,他們在過去15分鐘內都有活動)。如果bucket中的某個node沒有對我們的ping給出回復,我們最好再試一次(再發送一次ping,因為這個node也許仍然是活躍的,但由于網絡擁塞,所以發生了丟包現象,注意DHT的包都是UDP的),而不是立即丟棄這個node或者直接用新node來替代它。這樣,我們得路由表將充滿穩定的長時間在線的nodes。
每一個bucket都應該維持一個“lastchange”字段來表明bucket中的nodes有多新鮮。當一個bucket中的node被ping并給出了回復,或者一個node被加入到了bucket,或者一個node被一個新的node所替代,bucket的“lastchanged”字段都應當被更新。如果一個bucket的“lastchange”在過去的15分鐘內都沒有變化,那么我們將更新它。這個更新bucket操作是這樣完成的:從這個bucket所覆蓋的范圍中隨機選擇一個ID,并對這個ID執行find_nodes查找操作。常常收到請求的nodes通常不需要常常更新自己的buckets,反之,不常常收到請求的nodes常常需要周期性的執行更新所有buckets的操作,這樣才能保證當我們用到DHT的時候,里面有足夠多的好的nodes。
在第一個node插入路由表并開始服務后,這個node應該試著查找離自身更近的node,這個查找工作是通過不斷的發布find_node消息給越來越近的nodes來完成的,當不能找到更近的節點時,這個擴散工作就結束了。路由表應當被啟動工作和客戶端軟件保存(也就是啟動的時候從客戶端中讀取路由表信息,結束的時候客戶端軟件記錄到文件中)。
2.3、BitTorret協議擴展
BitTorrent協議已經被擴展為可以在通過tracker得到的peer之間互相交換nodeUDP端口號(也就是告訴對方我們的DHT服務端口號),在這樣的方式下,客戶端可以通過下載普通的種子文件來自動擴展DHT路由表。新安裝的客戶端第一次試著下載一個無tracker的種子時,它的路由表中將沒有任何nodes,這是它需要在torrent文件中找到聯系信息。
peers如果支持DHT協議就將BitTorrent協議握手消息的保留位的第八字節的最后一位置為1。這時如果peer收到一個handshake表明對方支持DHT協議,就應該發送PORT消息。它由字節0x09開始,payload的長度是2個字節,包含了這個peer的DHT服務使用的網絡字節序的UDP端口號。當peer收到這樣的消息是應當向對方的IP和消息中指定的端口號的node發送ping。如果收到了ping的回復,那么應當使用上述的方法將新node的聯系信息加入到路由表中。
2.4、Torrent文件擴展
一個無tracker的torrent文件字典不包含announce關鍵字,而使用一個nodes關鍵字來替代。這個關鍵字對應的內容應該設置為torrent創建者的路由表中K個最接近的nodes。可供選擇的,這個關鍵字也可以設置為一個已知的可用節點,比如這個torrent文件的創建者。請不要自動加入router.bittorrent.com到torrent文件中或者自動加入這個node到客戶端路由表中。
nodes= [["", ], ["",], …]
nodes= [[“127.0.0.1”, 6881], [“your.router.node”, 4804]]
2.5、KRPC協議
KRPC協議是由B編碼組成的一個簡單的RPC結構,他使用UDP報文發送。一個獨立的請求包被發出去然后一個獨立的包被回復。這個協議沒有重發。它包含3種消息:請求,回復和錯誤。對DHT協議而言,這里有4種請求:ping,find_node,get_peers,和announce_peer。
一個KRPC消息由一個獨立的字典組成,其中有2個關鍵字是所有的消息都包含的,其余的附加關鍵字取決于消息類型。每一個消息都包含t關鍵字,它是一個代表了transactionID的字符串類型。transactionID由請求node產生,并且回復中要包含回顯該字段,所以回復可能對應一個節點的多個請求。transactionID應當被編碼為一個短的二進制字符串,比如2個字節,這樣就可以對應2^16個請求。另一個每個KRPC消息都包含的關鍵字是y,它由一個字節組成,表明這個消息的類型。y對應的值有三種情況:q表示請求,r表示回復,e表示錯誤。
2.6、聯系信息編碼
Peers的聯系信息被編碼為6字節的字符串。又被稱為"CompactIP-address/port info",其中前4個字節是網絡字節序的IP地址,后2個字節是網絡字節序的端口。
Nodes的聯系信息被編碼為26字節的字符串。又被稱為"Compactnode info",其中前20字節是網絡字節序的nodeID,后面6個字節是peers的"CompactIP-address/port info"。
請求
請求,對應于KPRC消息字典中的“y”關鍵字的值是“q”,它包含2個附加的關鍵字“q”和“a”。關鍵字“q”是一個字符串類型,包含了請求的方法名字。關鍵字“a”一個字典類型包含了請求所附加的參數。
回復
回復,對應于KPRC消息字典中的“y”關鍵字的值是“r”,包含了一個附加的關鍵字r。關鍵字“r”是一個字典類型,包含了返回的值。發送回復消息是在正確解析了請求消息的基礎上完成的。
錯誤
錯誤,對應于KPRC消息字典中的y關鍵字的值是“e”,包含一個附加的關鍵字e。關鍵字“e”是一個列表類型。第一個元素是一個數字類型,表明了錯誤碼。第二個元素是一個字符串類型,表明了錯誤信息。當一個請求不能解析或出錯時,錯誤包將被發送。下表描述了可能出現的錯誤碼:
| 201 | 一般錯誤 |
| 202 | 服務錯誤 |
| 203 | 協議錯誤,比如不規范的包,無效的參數,或者錯誤的token |
| 204 | 未知方法 |
2.7、DHT請求
所有的請求都包含一個關鍵字id,它包含了請求節點的nodeID。所有的回復也包含關鍵字id,它包含了回復節點的nodeID。
2.7.1、ping
ping是最基礎的請求。此時KPRC協議中的“q”=“ping”。Ping請求包含一個參數id,它是一個20字節的字符串包含了發送者網絡字節序的nodeID。對應的ping回復也包含一個參數id,包含了回復者的nodeID。
請求參數:{“id” : “<querying nodes id>”}
回復參數:{“id” : “<queried nodes id>”}
報文包例子:
請求:{“t”:“aa”, “y”:“q”, “q”:“ping”, “a”:{“id”:“abcdefghij0123456789”}}
對應的B編碼:d1:ad2:id20:abcdefghij0123456789e1:q4:ping1:t2:aa1:y1:qe
回復:{“t”:“aa”, “y”:“r”, “r”:{“id”:“mnopqrstuvwxyz123456”}}
對應的B編碼:d1:rd2:id20:mnopqrstuvwxyz123456e1:t2:aa1:y1:re
2.7.2、find_node
find_node被用來查找給定ID的node的聯系信息。此時KPRC協議中的q=“find_node”。find_node請求包含2個參數,第一個參數是id,包含了請求node的nodeID。第二個參數是target,包含了請求者正在查找的node的nodeID。當一個node接收到了find_node的請求,他應該給出對應的回復,回復中包含2個關鍵字id和nodes,nodes是一個字符串類型,包含了被請求節點的路由表中最接近目標node的K(8)個最接近的nodes的聯系信息。
請求參數:{“id”:"<querying nodes id>", “target”:"<id of target node>"}
回復參數:{“id”:"<queried nodes id>", “nodes”:"<compact node info>"}
報文包例子
請求:{“t”:“aa”, “y”:“q”, “q”:“find_node”, “a”:{“id”:“abcdefghij0123456789”, “target”:“mnopqrstuvwxyz123456”}}
對應的B編碼:d1:ad2:id20:abcdefghij01234567896:target20:mnopqrstuvwxyz123456e1:q9:find_node1:t2:aa1:y1:qe
回復:{“t”:“aa”, “y”:“r”, “r”:{“id”:“0123456789abcdefghij”, “nodes”:“def456…”}}
對應的B編碼:d1:rd2:id20:0123456789abcdefghij5:nodes9:def456…e1:t2:aa1:y1:re
2.7.3、get_peers
get_peers與torrent文件的info_hash有關。此時KPRC協議中的q=”get_peers”。get_peers請求包含2個參數。第一個參數是id,包含了請求node的nodeID。第二個參數是info_hash,它代表torrent文件的infohash。如果被請求的節點有對應info_hash的peers,他將返回一個關鍵字values,這是一個列表類型的字符串。每一個字符串包含了"CompactIP-address/portinfo"格式的peers信息。如果被請求的節點沒有這個infohash的peers,那么他將返回關鍵字nodes,這個關鍵字包含了被請求節點的路由表中離info_hash最近的K個nodes,使用"Compactnodeinfo"格式回復。在這兩種情況下,關鍵字token都將被返回。token關鍵字在今后的annouce_peer請求中必須要攜帶。token是一個短的二進制字符串。
請求參數:{“id”:"<querying nodes id>", “info_hash”:"<20-byte infohash of targettorrent>"}
回復參數:{“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>"}
報文包例子
請求:{“t”:“aa”, “y”:“q”, “q”:“get_peers”, “a”:{“id”:“abcdefghij0123456789”,“info_hash”:“mnopqrstuvwxyz123456”}}
對應的B編碼: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”]}}
對應的B編碼:d1:rd2:id20:abcdefghij01234567895:token8:aoeusnth6:valuesl6:axje.u6:idhtnmee1:t2:aa1:y1:re
回復最接近的nodes: {“t”:“aa”, “y”:“r”, “r”:{“id”:“abcdefghij0123456789”, “token”:“aoeusnth”, “nodes”: “def456…”}}
對應的B編碼:d1:rd2:id20:abcdefghij01234567895:nodes9:def456…5:token8:aoeusnthe1:t2:aa1:y1:re
2.7.4、announce_peer
這個請求用來表明發出announce_peer請求的node,正在某個端口下載torrent文件。announce_peer包含4個參數。第一個參數是id,包含了請求node的nodeID;第二個參數是info_hash,包含了torrent文件的infohash;第三個參數是port包含了整型的端口號,表明peer在哪個端口下載;第四個參數數是token,這是在之前的get_peers請求中收到的回復中包含的。收到announce_peer請求的node必須檢查這個token與之前我們回復給這個節點get_peers的token是否相同。如果相同,那么被請求的節點將記錄發送announce_peer節點的IP和請求中包含的port端口號在peer聯系信息中對應的infohash下。
請求參數:{“id”:"<querying nodes id>", “info_hash”:"<20-byte infohash of target torrent>", “port”:<port number>, “token”:"<opaque token>"}
回復參數:{“id”:"<queried nodes id>"}
報文包例子
請求:{“t”:“aa”, “y”:“q”, “q”:“announce_peer”, “a”:{“id”:“abcdefghij0123456789”, “info_hash”:“mnopqrstuvwxyz123456”, “port”:6881, “token”: “aoeusnth”}}
對應的B編碼:d1:ad2:id20:abcdefghij01234567899:info_hash20:mnopqrstuvwxyz1234564:porti6881e5:token8:aoeusnthe1:q13:announce_peer1:t2:aa1:y1:qe
回復:{“t”:“aa”, “y”:“r”, “r”:{“id”:“mnopqrstuvwxyz123456”}}
對應的B編碼:d1:rd2:id20:mnopqrstuvwxyz123456e1:t2:aa1:y1:re
總結
- 上一篇: php yyuc框架,求一份YYUC框架
- 下一篇: DM数据库查询错误码