转载: BitTorrent Trackerless DHT协议规范V1.0试行草案

来源:互联网 发布:mysql主从复制ssl 编辑:程序博客网 时间:2024/04/29 14:08

一直以来我都有一个疑问: 一个新加入的BT客户端如何得到其他BT客户端的IP地址和端口?
如果所有的BT客户端地址都保存在BT服务器的话似乎又走CS结构的老路了,而且需要数据库支持.
以下这篇文章难道就是答案?

BitTorrent Trackerless DHT协议规范V1.0试行草案

DHT 协议

摘自 BitTorrentDev

BitTorrent 使用一个"分布式sloppy哈希表" (DHT)来为"trackerless"流存储peer联系信息。有效地使每个peer都成了一个tracker,这个协议基于Kademila网络并且在UDP上实现。

请注意本文档中使用的术语,以免混乱。"peer"是在一个TCP端口上监听的一个客户端/服务器,它实现了BitTorrent协议。"节点"是在一个UDP端口上监听的一个客户端/服务器,它实现了分布式哈希表协议。DHT由节点组成,它存储了peer的位置。BitTorrent客户端包含一个DHT节点,这个节点是用来联系DHT中其他节点以得到peer的位置,从而通过BitTorrent协议下载。

内容
·        1 概述
·        2 路由表
·        3 BitTorrent协议扩展
·        4 Torrent文件扩展
·        5 KRPC协议
o        5.1 Contact Encoding
o        5.2 Queries
o        5.3 Responses
o        5.4 Errors
§        5.4.1错误包例子
·        6 DHT请求
o        6.1 ping
§        6.1.1包例子
o        6.2 find_node
§        6.2.1包例子
o        6.3 get_peers
§        6.3.1包例子
o        6.4 announce_peer
§        6.4.1包例子
·        7脚注

概述

每个节点有一个全局唯一的标识符,称为"节点ID"。节点ID是从一个160位空间随机选择的,这个空间同样用来表示BitTorrent infohash。"米制距离"用来比较两个节点ID之间或者一个节点ID和一个infohash之间的"远近"。节点必须维护一个含有少量其它节点联系信息的路由表。ID越靠近节点本身的ID时,路由表越详细。节点知道DHT中很多ID离自己很"近"的节点的联系信息,而离自己非常远的ID的联系信息却很少。

在Kademlia网络中,米制距离是异或计算的,结果视为无符号整数。
distance(A,B) = |A ⊗ B| ,值越小表示越近。

当节点要为流寻找peer时,它使用米制距离来比较那些ID在自己路由表中节点的流的infohash。然后它联系它所知的ID离infohash最近的节点,问它们正在下载这个流的peer的联系信息。如果一个被联系的节点知道下载这个流的peer信息,那个peer的联系信息将随着回复信息被返回。否则,那个被联系的节点必须回复在它路由表中离流的infohash最近的节点的联系信息。最初的节点重复地请求比目标infohash更近的节点直道不能再找到更近的节点为止。查询完了之后,客户端把peer联系信息加入到所有回复节点中ID离流最近的那个节点中。

请求peer的返回值包含一个不透明的值,称之为"令牌"。为了一个节点宣布它所控制的peer正在下载一个流,它必须赠送从同一个被请求的节点收到的令牌给新来的寻找peer的请求。当一个节点试图"宣布"一个流时,被请求的节点核对令牌和发出请求的节点的IP地址。这是为了防止恶意的主机雇用其它主机。由于令牌仅仅由请求节点返回给收到令牌的同一个节点,所以执行未被定义。令牌在被分布之后必须在一个合理数量的时间被接受。BitTorrent执行使用每五分钟变换一次以保持连接秘密的IP地址的SHA1哈希,和接受十分钟以内旧的令牌。

路由表

每个节点维护一个已知的好的节点路由表。路由表中的节点是用来作为在DHT中请求的起始点。路由表中的节点以回复的形式被返回给其它发出请求节点。

不是所有我们学习的的节点都是相等的。一些是"好的"一些不是。使用DHT的许多节点能够发送请求和接受回复,但是不能回复来自其它节点的请求。节点的路由表必须只包含已知的好的节点,这是很重要的。一个好的节点是一个在过去15分钟内回复过其中一个我们的请求的一个节点。一个节点如果在过去15分钟内曾经回复过其中一个我们的请求,而且发送过一个请求给我们。一个节点静止15分钟以后,变成可疑的。当未能回复连续多个请求时节点变成坏的。我们知道好的节点被给于高于未知状态的节点优先权。

路由表覆盖了ID空间从0到2^160的全部节点。路由表被细分为"桶",每个桶覆盖一部分ID空间。一个空表有一个桶,其ID空间范围是min=0,max=2^160。当一个ID为"N"的节点被插入到表中时,它被放置在min <= N < max的桶中。一个空表只有一个桶所以任何节点必须适合放在其中。在变成"满"以前,每个桶只能装K个节点,确切的说是八个。当一个桶装满了已知的好的节点后,没有更多的节点可以加入,除非我们自己的节点ID落在桶的范围之内。那样的话,那个桶被两个新的桶代替,每个覆盖旧桶的半个范围,旧桶里的节点被分布在两个新桶之中。由于一个新表只有一个桶,满的桶总是被分裂成两个新的桶,覆盖着范围0..2^159 和 2^159..2^160。

当桶装满了好的节点,新的节点被丢弃。如果桶中的任何节点被知道已经变成坏的,那么那个就被新的节点代替。如果在桶中有任何在过去的15分钟内未被看见可疑的节点,最近最少见的节点被ping。如果被ping的节点回复了,那么下一个最近最少见的可疑节点被ping,如此直到有一个未能回复或者桶中的所有节点被知道是好的。如果桶中的一个节点未能回复ping,建议在丢弃节点,替换新的好的节点之前重试一次。这样,表中充满稳定的长期运行的节点。

每个桶必须维持一个"最近更新"属性来指明内容多"新鲜"。当桶中的一个节点被ping并且它回复了,或者一个节点被加入到桶中,或者桶中的一个节点被另一个节点代替,这个桶的最近更新属性必须被更新。15分钟内未被改变的桶必须被"刷新"。这个可以这样来做,在桶范围内挑选一个随机ID,在上面执行一个find_node搜索。能够收到其它节点的请求的节点一般不需要经常刷新桶。不能够接收来自其它节点的请求的节点一般将需要定期刷新整个桶以确保当DHT需要时有好的节点在它们的表中。

在插入第一个节点到它的路由表之后和启动之后,那个节点应该试图找到DHT中离它最近的节点。它这样来做,发出find_node消息给越来越近的节点直到不能发现再近的为止。路由表应该保存在客户端软件的祈祷之间。

BitTorrent 协议扩展

BitTorrent协议被扩展以便用来在tracker介绍的peer之间交换节点的UDP端口号。这样,客户端能够得到通过下载的常规流自动传播的路由表。新安装的客户端在试图下载一个trackerless流时,在第一次尝试时在它们的路由表中没有任何节点,将需要包含在torrent文件中的联系信息。

支持DHT的peer设置BitTorrent协议握手交换的8字节保留标志的最后一位。收到一个指出远程peer支持DHT的握手包的peer应该发送一个PORT消息。以字节0x09开始,还有两个字节以网络字节顺序承载了包含DHT的节点的UDP端口。收到这个消息的peer应该试图ping这个节点,在远程peer的IP地址和接收端口上。如果一个ping的回复被收到,这个节点应该试图根据一般规定把新的联系信息插入到他们的路由表中去。

Torrent文件扩展

一个trackerless的torrent字典没有一个"announce"关键字。代之,一个trackerless的torrent有一个"nodes"关键字。这个关键字应该设置成在生成客户端路由表的torrent中第K个最近节点。或者选第二种设置,这个关键字应该设置成一个已知的好的节点,例如生成torrent的那个人的节点。请不要自动添加"router.bittorrent.com"到torrent文件或自动添加这个节点到客户端路由表中。

nodes = [["<host>", <port>], ["<host>", <port>], ...] nodes = [["127.0.0.1", 6881], ["your.router.node", 4804]]

KRPC协议

KRPC协议是一个简单的RPC机制,由在UDP上发送的bencode编码的字典组成。发出单个请求包,单个包作为回复。没有重试。有三种消息类型: query,response,和error。在DHT协议中,有四种query: ping,find_node,get_peers,和announce_peer。

一个KRPC消息是单个字典有和每个消息一样的两个关键字,不同消息类型还有附加的关键字。每个消息有一个关键字"t",和一个单字符串值称为transaction ID。这个transaction ID 是请求节点产生的,在回复中被反射,所以回复可能关联多个给同一个节点的请求。包含在每个KRPC信息里的另一个关键字是"y",和一个单字符值描述了消息类型。关键字"y"的值是以下其中一个:"q" 代表请求,"r"代表回复,或者"e"代表错误。

CONTACT ENCODING

peer的联系信息以一个6字节的字符串编码。也就是"联系IP-地址/端口信息",网络字节排序的4字节的IP地址和网络字节排序2字节端口在连接在最后。

节点的联系信息以一个26字节的字符串编码。也就是"联系节点信息",网络字节顺序的20字节的节点ID,还有联系IP-地址/端口信息连接在最后。

QUERIES

请求,也就是KRPC消息字典和一个关键字"y",值是"q",包含两个附加关键字:"q"和"a"。关键字"q"有一个字符串值包含请求的方法名。关键字"a"有一个字典值包含请求的 名字参数。

RESPONSES

回复,也就是KRPC消息字典和一个"y",值是"r",包含一个附加关键字"r"。"r"的值是一个字典包含着返回值。回复消息在请求完成的基础上被发送。

ERRORS

错误,也就是KRPC消息字典和一个"y",值是"e",包含一个附加关键字"e"。"e"的值是一个列表。第一个元素是一个整数,它回复了错误代码。第二个元素是一个字符串包含错误信息。当一个请求不能被履行的时候发送错误。以下表格描述了可能的错误代码:

201        一般错误
202        服务器错误
203        协议错误,例如畸形包,无效参数,坏令牌
204        方法未知

错误包例子

generic error = {'t':0, 'y':'e', 'e':[201, "A Generic Error Ocurred"]} bencoded = d1:eli201e23:A Generic Error Ocurrede1:ti0e1:y1:ee

DHT 请求

所有请求有一个"id"关键字,它的值包含了发出请求节点的节点ID。所有回复有一个"id"关键字,它的值包含了发出回复节点的节点ID。

PING

最基础的请求是一个ping。"q" = "ping",一个ping请求有一个参数,"id",它的值是一个20字节的字符串,包含网络字节排序的发送者的节点ID。一个ping的适当回复有一个关键字"id",包含发出回复的节点的节点ID。

arguments: {"id" : "<querying 节点s id>"} response: {"id" : "<queried 节点s id>"}

包例子

ping Query = {"t":"0", "y":"q", "q":"ping", "a":{"id":"abcdefghij0123456789"}} bencoded = d1:ad2:id20:abcdefghij0123456789e1:q4:ping1:t1:01:y1:qe
Response = {"t":"0", "y":"r", "r": {"id":"mnopqrstuvwxyz123456"}} bencoded = d1:rd2:id20:mnopqrstuvwxyz123456e1:t1:01:y1:re

FIND_NODE

find node用来找给出ID的节点的联系信息。"q" == "find_node",一个find_node请求有两个参数,"id"包含请求节点的节点ID,"target"包含请求者要寻找的节点的ID。当一个节点收到了一个find_node请求,它应该回复一个关键字"nodes"和一个包含目标节点的联系节点信息的字符串,或者在它们自己的路由表中的K(8)最近的好的节点。

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

包例子

find_node Query = {'t':0, 'y':'q', 'q':'find_node', 'a': {'id':'abcdefghij0123456789', 'target':'mnopqrstuvwxyz123456'}} bencoded = d1:ad2:id20:abcdefghij01234567896:target20:mnopqrstuvwxyz123456e1:q9:find_node1:ti0e1:y1:qe

Response = {'t':0, 'y':'r', 'r': {'id':'0123456789abcdefghij', 'nodes': 'def456...'}} bencoded = d1:rd2:id20:0123456789abcdefghij5:nodes9:def456...e1:ti0e1:y1:re

GET_PEERS

get peers联合一个流infohash。"q" = "get_peers",一个get_peers请求有两个参数,"id"包含发出请求节点的节点ID,"info_hash"包含流的info_hash。如果那个被请求的节点有适合info_hash的peer,它们将包含在一个关键字"values"中被返回,"values"的值是一个单字符串列表,由包含"compact"格式的peer信息的连接在一起组成。如果那个被请求的节点没有适合info_hash的peer,一个关键字"nodes"被返回,它包含了在被请求的节点的路由表中最接近请求提供的info_hash的节点K。无论那种情况一个"token"关键字也包含在返回值中。令牌的值是一个将来的announce_peer 请求中用到的的一个需要的参数。

arguments: {"id" : "<querying nodes id>", "info_hash" : "<20-byte infohash of target torrent>"} response: {"id" : "<queried nodes id>", "values" : ["<compact peer info string>"]} or: {"id" : "<queried nodes id>", "nodes" : "<compact node info>"}

包例子

get_peers Query = {'t':0, 'y':'q', 'q':'get_peers', 'a': {'id':'abcdefghij0123456789', 'info_hash':'mnopqrstuvwxyz123456'}} bencoded = d1:ad2:id20:abcdefghij01234567899:info_hash20:mnopqrstuvwxyz123456e1:q9:get_peers1:ti0e1:y1:qe

Response with peers = {'t':0, 'y':'r', 'r': {'id':'abcdefghij0123456789', 'token':'aoeusnth', 'values': ['axje.uidhtnmbrl']}} bencoded = d1:rd2:id20:abcdefghij01234567895:token8:aoeusnth6:valuesl15:axje.uidhtnmbrlee1:ti0e1:y1:re

Response with closest nodes = {'t':0, 'y':'r', 'r': {'id':'abcdefghij0123456789', 'token':'aoeusnth', 'nodes': 'def456...'}} bencoded = d1:rd2:id20:abcdefghij01234567895:nodes9:def456...5:token8:aoeusnthe1:ti0e1:y1:re

ANNOUNCE_PEER

宣布控制发出请求的节点的那个peer正在一个端口上下载那个一个流。announce_peer有四个参数:"id"包含发出请求节点的节点ID,"info_hash"包含流的infohash,"port"包含作为一个整数的端口,还有给前一个get_peers请求的回复中的"token"。被请求的节点必须检验令牌是以前发送给和发出请求的节点一样的IP地址。然后被请求的节点应该把那个发出请求的节点的IP地址和infohash中提供的端口号存储在它的peer联系信息存档中。

arguments: {"id" : "<querying nodes id>", "info_hash" : "<20-byte infohash of target torrent>", "port" : <port number>, "token" : "<opaque token>"} response: {"id" : "<queried nodes id>"}

包例子

announce_peers Query = {'t':0, 'y':'q', 'q':'announce_peers', 'a': {'id':'abcdefghij0123456789', 'info_hash':'mnopqrstuvwxyz123456', 'port' : 6881, 'token' : 'aoeusnth'}} bencoded = d1:ad2:id20:abcdefghij01234567899:info_hash20:
mnopqrstuvwxyz1234564:porti6881e5:token8:aoeusnthe1:q14:announce_peers1:ti0e1:y1:qe

Response = {"t":"0", "y":"r", "r": {"id":"mnopqrstuvwxyz123456"}} bencoded = d1:rd2:id20:mnopqrstuvwxyz123456e1:t1:01:y1:re

脚注
1.        "Kademlia: A Peer-to-peer Information System Based on the XOR Metric",
Petar Maymounkov and David Mazieres,
2.        Use SHA1 and plenty of entropy to ensure a unique ID