【P2P⽹络】BitTorrent的DHT协议(译⾃官⽅版本)
译者前序
DHT协议早在2005年就已经成为了官⽅BitTorrent协议的⼀部份,但是我竟然⼀直没有到国内的官⽅翻译稿,所以将其进⾏翻译,若⽂中错误,欢迎各位指正。
其次,若想彻底理解DHT协议的原理,建议各位阅读Kademlia协议,在本博客中,有其翻译稿,参见DHT协议基础1,2.
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协议下载。
概述
每⼀个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在⼗分钟以内是可接受的。
路由表
每⼀个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来完成的,当不能到更近的节点时,这个扩散⼯作就结束了。路由表应当被启动⼯作和客户端软件保存(也就是启动的时候从客户端中读取路由表信息,结束的时候客户端软件记录到⽂件中)。
BitTorret协议扩展
BitTorrent协议已经被扩展为可以在通过tracker得到的peer之间互相交换nodeUDP端⼝号(也就是告诉对⽅我们的DHT服务端⼝号),在这样的⽅式下,客户端可以通过下载普通的种⼦⽂件来⾃动扩展DHT路由表。新安装的客户端第⼀次试着下载⼀个⽆tracker的种⼦时,它的路由表中将没有任何nodes,这是它需要
在torrent⽂件中到联系信息。
peer
peers如果⽀持DHT协议就将BitTorrent协议握⼿消息的保留位的第⼋字节的最后⼀位置为1。这时如果peer收到⼀个handshake表明对⽅⽀持DHT协议,就应该发送PORT消息。它由字节0x09开始,payload的长度是2个字节,包含了这个peer的DHT服务使⽤的⽹络字节序的UDP端⼝号。当peer收到这样的消息是应当向对⽅的IP和消息中指定的端⼝号的node发送ping。如果收到了ping的回复,那么应当使⽤上述的⽅法将新node的联系信息加⼊到路由表中。
Torrent⽂件扩展
⼀个⽆tracker的torrent⽂件字典不包含announce关键字,⽽使⽤⼀个nodes关键字来替代。这个关键字对应的内容应该设置为torrent创建者的路由表中K个最接近的nodes。可供选择的,这个关键字也可以设置为⼀个已知的可⽤节点,⽐如这个torrent⽂件的创建者。请不要⾃动加⼊router.bittorrent到torrent⽂件中或者⾃动加⼊这个node到客户端路由表中。
nodes= [["<host>", <port>], ["<host>",<port>], ...]
nodes= [["127.0.0.1", 6881], ["de",4804]]
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表⽰错误。
联系信息编码
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未知⽅法
错误包例⼦:
⼀般错误={"t":"aa", "y":"e", "e":[201,"A Generic Error Ocurred"]}
B编码=d1:eli201e23:AGenericErrorOcurrede1:t2:aa1:y1:ee
DHT请求
所有的请求都包含⼀个关键字id,它包含了请求节点的nodeID。所有的回复也包含关键字id,它包含了回复节点的nodeID。
ping
最基础的请求就是ping。这时KPRC协议中的“q”=“ping”。Ping请求包含⼀个参数id,它是⼀个20字节的字符串包含了发送者⽹络字节序的nodeID。对应
的ping回复也包含⼀个参数id,包含了回复者的nodeID。
参数: {"id" : "<querying nodes id>"}
回复:{"id" : "<queried nodes id>"}
报⽂包例⼦
ping请求={"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
find_node
Findnode被⽤来查给定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>"}
报⽂包例⼦
find_node请求={"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":""}}
B编码=d1:rd2:id20:0123456789abcdefghij5:1:t2:aa1:y1:re
get_peers
Getpeers与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>"]}
or:{"id" : "<queried nodes id>","token" :"<opaque write token>","nodes" : "<compact node info>"}
报⽂包例⼦
get_peers请求={"t":"aa", "y":"q","q":"get_peers", "a":{"id":"abcdefghij0123456789","info_hash":"mnopqrstuvwxyz123456"}}
B编码=d1:ad2:id20:abcdefghij01234567899:info_hash20:mnopqrstuvwxyz123456e1:q9:get_peers1:t
2:aa1:y1:qe
回复peers ={"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": ""}}
B编码=d1:rd2:id20:abcdefghij01234567895:5:token8:aoeusnthe1:t2:aa1:y1:re
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>"}
报⽂包例⼦
announce_peers请求={"t":"aa", "y":"q","q":"announce_peer", "a":{"id":"abcdefghij0123456789","info_hash":"mnopqrstuvwxyz123456", "port":6881, "token": "aoeusnth"}}
B编码=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"}}
B编码=d1:rd2:id20:mnopqrstuvwxyz123456e1:t2:aa1:y1:re
译者注:
在实践观察中发现两种新的消息
1.vote:表⽰客户端发送速度过快,希望可以减慢
2.v:表⽰客户端的版本号
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论