BEP 5 DHT Protocol

英文原文

BitTorrent使用了DHT来存储无tracker的torrent中的peer联系信息。作用是让每一个peer节点都成为了一个tracker。这个协议是基于Kademila和UDP。

注意以下学术描述信息防止误解:
peer:通过TCP方式联系的 client/server 方式实现BitTorrent协议的节点
node:通过UDP方式联系的 client/server 方式实现分布式hash table协议节点。
DHT网络是由 nodes 组成的,node 节点中存储着 peer 信息。BitTorrent 客户端包含一个 DHT 节点,这个节点用来联系 DHT 中其他节点,从而得到 peer 的位置,进而通过 BitTorrent 协议下载。

Overview

每一个node节点都拥有着全球唯一的ID。一个node ID用和BitTorrent协议中infohash相同大小160bit(20字节)来表示。distance metric用来表示两个node id或者node id和infohash间的"最短距离"。Node节点需要维护一个路由表来记录离他较近的node信息,对于远的node不应该知道太多。

在Kademlia协议中,distance metric用XOR来计算,计算结果用无符号整型表示,值越小距离越小。distance(A,B)=|A xor B|。

当一个node想要去寻到一个torrent的peers信息,它将用距离度量来计算torrent的infohash与其路由表中的node id信息距离大小。然后向路由表中离 infohash 最近的节点发送请求,问它们正在下载这个 torrent 的 peer 的联系信息。如果一个被联系的节点知道下载这个 torrent 的 peer 信息,那个 peer 的联系信息将被回复给当前节点。否则,那个被联系的节点则必须回复在它的路由表中离该 torrent 的 infohash 最近的节点的联系信息。最初的节点重复地请求比目标 infohash 更近的节点,直到不能再找到更近的节点为止。查询完了之后,客户端把自己作为一个 peer 插入到所有回复节点中离种子最近的哪个节点中。

get_peers的返回值包含一个不透明的值,称之为"token(令牌)"。如果一个节点宣布它所控制的 peer 正在下载一个种子,它需要发向提供给它相关peer信息的被请求node及其附带返回的token值。这样当一个节点试图"宣布"正在下载一个种子时,被请求的节点核对令牌和发出请求的节点的 IP 地址。这是为了防止恶意的主机登记其它主机的种子。由于令牌仅仅由请求节点返回给收到令牌的同一个节点,所以没有规定他的具体实现。但是令牌必须在一个规定的时间内被接受,超时后令牌则失效。在 BitTorrent 的实现中,token 是在 IP 地址后面连接一个 secret(通常是一个随机数),这个 secret 每五分钟改变一次,其中 token 在十分钟以内是可接受的。

Routing Table

每一个node节点都维护着一个已知好的node信息路由表。它是作为该node的DHT网络起始信息,也是返回来自其他node请求信息的基础值。

并不是我们所认识的node都是平等,它们一些是"good"的,一些不是。许多node使用DHT可以发送或者接收请求,但并不是都可以回复来自其他node的请求。值得注意的一点是每一个node节点的路由表必须只包含好的node。一个好的节点定义是在15分钟内回复过我们,或者它曾经回复过我们(15分钟以前)但同时在15分钟内它向我们发送过请求。超过15分钟没有活跃,一个node将变成questionable。一个node如果无法回复一系列请求时它将是bad。我们所知的好的node优先级高于未知状态的节点。

路由表覆盖从 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 消息给越来越近的节点来完成的,当不能找到更近的节点时,这个扩散工作就结束了。路由表应当被启动工作和客户端软件保存(也就是启动的时候从客户端中读取路由表信息,结束的时候客户端软件记录到文件中)。

KRPC Protocol

KRPC协议是一个简单的通过UDP传输由bencode编码字典集合的RPC传输机制。通过一个简单的请求包发出和一个简单的回复包传回,没有重试的机制。一共有三种传输信息方式:query,response,error。对于DHT协议来说,有四中query的方式:ping,find_node,get_peers,announce_peer。

对于每一个KRPC信息是由一个简单的三个keys共同组成的字典集合,如若有其他信息根据信息类型增加相应的keys值。
每一个信息都有一个["t",string value]字典对代表了事务ID。这个事务ID由querying node产生,并且回复时需要携带该ID,这样就可以在同一个node中进行多个信息请求。事务ID需要编码成一个短的二进制字符串,一般是两个字符就足够,因为它包含了2^16种请求可能。
每一个信息都有一个["y",single character value]字典对描述该类型信息。该value值中,"q"表示query,"r"表示response,"e"表示error。
每一个信息都有一个["v",two character value]字典对代表客户端版本号鉴别,该value值是用作客户端版本识别,识别号是注册在BEP 20协议中。当然并不是所有的实现都需要该字典对所以客户端不应该假设它必须存在的。

Contact Encoding

peers的联系信息被编码为6字节二进制串。又被称为"Compact IP-address/port info",信息是由4字节ip地址和紧跟其后的2字节端口号的网络字节序组成。
nodes的联系信息被编码为25字节二进制串。又被称为"Compact node info",是由20字节的node id和紧跟其后6字节的"Compact ip-address/port"组成。

Queries

queries或者KRPC信息字典集包含有["y","q"]字典对的,将包括两个额外的键对"q"和"a","q"的值表示的是请求方式的名字,"a"的值表示请求方式所附带的参数字典集。

Responses

response或者KRPC信息字典集包含有["y","r"]字典对的,将包括一个额外的键对“r","r"的值表示返回的参数字典集。应答信息是建立在成功完成解析请求的基础上才发送的。

Errors

errors或者KRPC信息字典集包含有["y","e"]字典对的,将包含一个额外的键对"e","e"的值是list类型。第一个参数是一个代表错误代码的整型数。第二个参数是一个包含错误信息的string。Errors是建立在一个请求不能被解析或出错的情况下才发送的。下表描述了可能的错误代码:

错误包例子:
generic error = {"t":"aa", "y":"e", "e":[201, "A Generic Error Ocurred"]}
bencoded = d1:eli201e23:A Generic Error Ocurrede1:t2:aa1:y1:ee

DHT Queries

所有的请求都有一个"id"键,它的值表示请求node的node ID。所有的应答也有一个"id"键,它的值表示回应的node的node ID。

ping

最基础的请求就是ping。"q"="ping"一个ping请求有一个单一的参数,"id"的值是一个表示发送请求的node的ID的20字节长的二进制字符串网络字节序。完整合适的应答ping也需要一个单一的键"id"包含了应答node的node ID序列号。

arguments: {"id" : "<querying nodes id>"}
response: {"id" : "<queried nodes id>"}

Example Packets

ping Query = {"t":"aa", "y":"q", "q":"ping", "a":{"id":"abcdefghij0123456789"}}
bencoded = d1:ad2:id20:abcdefghij0123456789e1:q4:ping1:t2:aa1:y1:qe

Response = {"t":"aa", "y":"r", "r": {"id":"mnopqrstuvwxyz123456"}}
bencoded = d1:rd2:id20:mnopqrstuvwxyz123456e1:t2:aa1:y1:re

find_node

Find node是用于寻找给定ID的node节点的相关联系信息。"q"=”find_node"一个find_node的请求有两个参数,"id"表示请求node的ID,"target"表示待查询的node的ID。当一个node接收到find_node请求,它应该回复"nodes"键对,其值是一个它的路由表中离target ID理论上最近的8个节点的node的"compact node info"紧排一起的208字节二进制串。

arguments: {"id" : "<querying nodes id>", "target" : "<id of target node>"}
response: {"id" : "<queried nodes id>", "nodes" : "<compact node info>"}

Example Packets

find_node Query = {"t":"aa", "y":"q", "q":"find_node", "a": {"id":"abcdefghij0123456789", "target":"mnopqrstuvwxyz123456"}}
bencoded = d1:ad2:id20:abcdefghij01234567896:target20:mnopqrstuvwxyz123456e1:q9:find_node1:t2:aa1:y1:qe
Response = {"t":"aa", "y":"r", "r": {"id":"0123456789abcdefghij", "nodes": "def456..."}}
bencoded = d1:rd2:id20:0123456789abcdefghij5:nodes9:def456...e1:t2:aa1:y1:re

get_peers

Get peers关联着一个torrent infohash。"q"="get_peers"。一个get_peers的请求有俩个参数,"id"表示请求node的ID,"info_hash"表示torrent的infohash值。如果被请求的node有关于infohash的peers节点信息,那么这些信息将会组成一个list包含在"values"的键对中。Each string containing "compact" format peer information for a single peer. 如果被请求节点没有相关的peers信息,那么将会回复"nodes"键对,其值是一个它的路由表中离infohash理论上最近的8个节点的node的"compact node info"紧排一起的200字节二进制串。
另一方面,"token"键值对需要包含在返回字典的。这个值将会在未来可能的announce_peer中被使用到。其值应该要比较短的的二进制串。

arguments: {"id" : "<querying nodes id>", "info_hash" : "<20-byte infohash of target torrent>"}
response: {"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>"}

Example Packets:

get_peers Query = {"t":"aa", "y":"q", "q":"get_peers", "a": {"id":"abcdefghij0123456789", "info_hash":"mnopqrstuvwxyz123456"}}
bencoded = d1:ad2:id20:abcdefghij01234567899:info_hash20:mnopqrstuvwxyz123456e1:q9:get_peers1:t2:aa1:y1:qe
Response with peers = {"t":"aa", "y":"r", "r": {"id":"abcdefghij0123456789", "token":"aoeusnth", "values": ["axje.u", "idhtnm"]}}
bencoded = d1:rd2:id20:abcdefghij01234567895:token8:aoeusnth6:valuesl6:axje.u6:idhtnmee1:t2:aa1:y1:re
Response with closest nodes = {"t":"aa", "y":"r", "r": {"id":"abcdefghij0123456789", "token":"aoeusnth", "nodes": "def456..."}}
bencoded = d1:rd2:id20:abcdefghij01234567895:nodes9:def456...5:token8:aoeusnthe1:t2:aa1:y1:response

announce_peer

Announce that the peer, controlling the querying node, is downloading a torrent on a port.该请求有四个参数,"id"代表请求node的ID,"info_hash"代表torrent的infohash值,"port"代表对应的端口,"token"代表前一个发送的get_peers请求返回的token值。该请求的必须是发向提供该token的IP地址的节点。接受方需存储请求方的ip地址和更新存储该infohash的peer关联信息的端口信息。
这里有一个可选的参数implied_port,它的值可以是0或1.当它是非0,那么该端口参数应该被无视将源UDP包的端口地址替代该peer的端口值。这对于peer处于NAT网络下,未能知道其实际的端口值有一定意义。同时支持UTP,它们接受来自相同的端口的信息作为DHT端口。

arguments: {"id" : "<querying nodes id>",
"implied_port": <0 or 1>,
"info_hash" : "<20-byte infohash of target torrent>",
"port" : <port number>,
"token" : "<opaque token>"}

response: {"id" : "<queried nodes id>"}

Example Packets:

announce_peers Query = {"t":"aa", "y":"q", "q":"announce_peer", "a": {"id":"abcdefghij0123456789", "implied_port": 1, "info_hash":"mnopqrstuvwxyz123456", "port": 6881, "token": "aoeusnth"}}
bencoded = d1:ad2:id20:abcdefghij012345678912:implied_porti1e9:info_hash20:mnopqrstuvwxyz1234564:porti6881e5:token8:aoeusnthe1:q13:announce_peer1:t2:aa1:y1:qe
Response = {"t":"aa", "y":"r", "r": {"id":"mnopqrstuvwxyz123456"}}
bencoded = d1:rd2:id20:mnopqrstuvwxyz123456e1:t2:aa1:y1:re

 

posted @ 2018-05-24 16:03  Grim_Reaper  阅读(377)  评论(0编辑  收藏  举报