自己写论文用翻译的,请轻喷
原文链接:http://www.bittorrent.org/beps/bep_0005.html
(一)概述
BitTorrent使用分布式哈希表(DHT)来存储无tracker记录的torrent文件中的peer信息。这种协议机制使得每个peer都同时成为了一个tracker。它基于Kademlia[i]并使用UDP实现。
请注意不要混淆peer和node,本文将多次使用这两个术语。Peer指实现BitTorrent协议中客户端和服务端功能,通过TCP通信的网络成员。Node指实现DHT协议中客户端和服务端功能,通过UDP通信的网络节点。DHT网络由大量的node构成,每个node中存储了多个peer的地址信息。BitTorrent客户端[1]在运行时也会作为一个node加入DHT网络,来获取使用BitTorrent协议下载时需要连接到的peers地址。
每个node都包含一个唯一标示符”node ID”,它的值从BitTorrent的infohash的160位空间中随机选取[2]。距离(distance metric)用来比较两个node ID之间或者是一个node ID与一个infohash之间的接近程度。每一个node必须维护自己的路由表,其中包含其他node的ID和联系信息。路由表会保存更多距离接近自己node ID的node信息,而却只保留少数几个远离自己node ID的node信息。
Kademlia使用node ID异或(XOR)后产生的无符号整数来度量node的接近程度。Distance(A, B) = |A xor B| 异或后得到的值越小则表示两个node之间的距离越近。
node在寻找正在下载某个torrent的peer时,会计算torrent的infohash与路由表中node之间的距离,然后向距离最短的若干个node请求获取infohash对应的peer信息。如果被请求的node存储了被请求的infohash对应的peer信息,则直接把peer信息返回给发送请求的node,查找peer完成。如果被请求的节点没有保存被请求的infohash信息,则返回若干个最接近infohash的node给发送请求的node。发送请求的node收到返回的node后,从中选取最接近infohash的若干个节点再次发送查询请求。每次查询如果没有直接找到peer,就会找到更接近infohash的node,这个操作会一直不断重复一直持续到不能找到更接近infohash的node为之。在查询结束后,发起查询的node会向刚才返回peer或node的node发起请求,把自己的peer信息加入到查询的infohash对应的peer列表中。
(二)路由表
每一个node会维护一个保存已知有效节点的路由表。当node向DHT发起请求时,路由表中的nodes会作为起始节点。这些nodes是在向其他node请求过程中,由被请求的node返回的。
我们发现的nodes并非都能起作用,有的node是有效的,而有的node是无效的。许多使用DHT协议的node都可以在向某个node发送请求后接收被请求node的回复,但是不能回复未主动请求的node发送的请求[3]。对于每一个node的路由表,只存储有效nodes非常重要。有效的node是指在过去的15分钟以内,对node发送的某一个请求给出过回复的node;或者对我们的发出的请求给出过回复(未必在15分钟以内),并且在过去的15分钟给自己node发送过请求。上面两种情况都可以判定node为有效node。超过15分钟没有任何活动的node将被视为可疑node。当nodes不能回复自己node的多次请求时,这个节点将被视为无效节点。比起状态未知的nodes,状态为有效的节点会被赋予更高的优先权。
路由表覆盖整个从0到2160的node ID空间。路由表又被细分为buckets(桶),每一个bucket覆盖一部分node D空间。一个空的路由表建立时只有一个bucket,它覆盖ID范围是从min=0到max=2160。当一个node ID为“N”的node插入到表中时,它会被添加到ID范围在min<= N<=max的bucket中。一个空路由表只有一个bucket,所以所有的node都会加入这个bucket中。每一个bucket最多只能容纳K[4]个nodes。当一个bucket充满了有效的nodes之后,不再接受加入新node,除非自己的node ID在这个bucket的范围内。在这种情况下,这个bucket会分裂为2个新的buckets,每一个新桶覆盖原来旧桶一半的范围。旧桶中的nodes会重新分配到这两个新的buckets中。对于只有一个bucket的新路由表,覆盖整个范围的bucket会分裂为2个新的buckets,第一个覆盖0到2159的范围空间,第二个覆盖2159..2160的范围空间。
Bucket充满有效的 nodes时,新发现的node将被丢弃。如果bucket中某一个node失效,应当使用新的node来代替。如果bucket中存在超过15分钟未联系的node,它的状态会转换为可疑状态,这时应当向最久没有联系的可疑节点发送ping。如果目标node回复了ping,那么将可疑状态转换为有效状态,继续向下一个可疑的node发送ping,并不断这样循环下去,直到发现一个未响应ping的node,或者确认bucket中的所有nodes都是有效的。如果直到发现一个未响应ping的node,应当再尝试ping一次。如果仍然收不到回复,丢弃这个node或替换为新node。通过这些机制,路由表会充满长期稳定在线的nodes。
第一个node插入路由表后,应当不断的发布find_node消息,获取新的nodes,再向离自己更近的nodes发送find_node消息,不断循环,找到离自身更近的node。当不能找到更近的节点时,查找完成。路由表应当BitTorrent客户端软件保存起来供下次启动时初始化时使用。
(三)KRPC协议
1.概述
KRPC协议通过发送和接收B编码的UDP包实现了简单的远程过程调用机制。进行KPRC调用时,发送一个查询包并等待接收到一个回复包,如果失败也不会重传。KPRC有三种消息类型:query,response和error。针对DHT协议,query具体有四种:ping,find_node,get_peers,announce_peer。
每个KRPC消息都由一个B编码的独立的字典构成,其中有2个关键字是所有消息都包含的,消息类型决定剩余的附加关键字。每一个消息都包含关键字t,它是一个字符串类型的transaction ID。transaction ID由发送请求的node产生,并且在回复中必须包含该字段,来区分同一个node回复的多个请求。transaction ID应当编码为一个很短的二进制串,通常使用2字节,可以对应2^16个不同的请求。另一个每个KRPC消息都包含的关键字是y,长度为一个字节,标识这个消息的类型。y的取值有三种:q代表请求,r代表回复,e代表错误。
2.联系信息
peer的联系信息编码为6字节长的字符串,也称作”Compact IP-address/port info”。其中前4个字节是网络字节序(大端序)的IP地址,后2个字节是网络字节序的端口号。
node的联系信息编码为26字节长的字符串,也称作”Compact node info”。其中前20字节是网络字节序的node ID,后面6个字节是peer的”Compact IP-address/port info”。
3.请求
请求,对应于KPRC消息字典中的“y”关键字的值是“q”,它包含2个附加的关键字“q”和“a”。关键字“q”是一个字符串类型,包含了请求的方法名字。关键字“a”一个字典类型包含了请求所附加的参数。
4.回复
回复,对应于KPRC消息字典中的“y”关键字的值是“r”,包含了一个附加的关键字r。关键字“r”是一个字典类型,包含了返回的值。发送回复消息是在正确解析了请求消息的基础上完成的。
5.错误
错误,对应于KPRC消息字典中的y关键字的值是“e”,包含一个附加的关键字e。关键字“e”是一个列表类型。第一个元素是一个数字类型,表明了错误码。第二个元素是一个字符串类型,表明了错误信息。当一个请求不能解析或出错时,错误包将被发送。下表描述了可能出现的错误码:
错误码 | 错误描述 |
201 | 一般错误 |
202 | 服务错误 |
203 | 协议错误,如不规范的包,无效的参数,或者错误的token |
204 | 未知方法 |
错误包例子:
generic error = {“t”:”bb”, “y”:”e”, “e”:[201, “A Generic Error Ocurred”]}
bencoded = d1:eli201e23:A Generic Error Ocurrede1:t2:bb1:y1:ee[ii]
(四)DHT请求
所有请求的关键字id是请求节点的node ID,所有的回复的关键字id是回复节点的node ID。
1. ping
最基础的请求就是ping,它的关键字q值为ping。ping请求包含参数id,它是一个20字节长的字符串,值为发送者网络字节序的node ID。对应的ping回复也包含参数id,值为回复者的node ID。
1 |
arguments: {"id" : "<querying nodes id>"} |
1 |
response: {"id" : "<queried nodes id>"} |
报文包例子
1 |
arguments = {"t":"bb", "y":"q", "q":"ping", "a":{"id":"abcdefghij0123456789"}} |
1 |
bencoded = d1:ad2:id20:abcdefghij0123456789e1:q4:ping1:t2:bb1:y1:qe |
1 |
response = {"t":"bb", "y":"r", "r": {"id":"mnopqrstuvwxyz123456"}} |
1 |
bencoded = d1:rd2:id20:mnopqrstuvwxyz123456e1:t2:bb1:y1:re |
2. find_node
find_node用来查找node ID的联系信息,请求包中关键字q值为find_node。find_node请求包含2个参数,第一个参数是自己的node ID,第二个参数是需要查找的node ID。当node接收到了find_node的请求,会给出对应的回复,回复中有2个关键字id和nodes,nodes为字符串类型,包含被请求节点的路由表中K个距离最近的nodes的联系信息。
1 |
arguments: {"id" : "<querying nodes id>", "target" : "<id of target node>"} |
1 |
response: {"id" : "<queried nodes id>", "nodes" : "<compact node info>"} |
报文包例子
1 |
find_node Query = {"t":"aa", "y":"q", "q":"find_node", "a": {"id":"abcdefghij0123456789", "target":"mnopqrstuvwxyz123456"}} |
1 |
bencoded = d1:ad2:id20:abcdefghij01234567896:target20:mnopqrstuvwxyz123456e1:q9:find_node1:t2:aa1:y1:qe |
1 |
Response = {"t":"aa", "y":"r", "r": {"id":"0123456789abcdefghij", "nodes": "def456..."}} |
1 |
bencoded = d1:rd2:id20:0123456789abcdefghij5:nodes9:def456...e1:t2:aa1:y1:re |
3. get_peers
get_peers操作可以获取torrent文件对应的info_hash的peers。请求包中关键字q的值为get_peers。get_peers请求包含2个参数,第一个参数id值为发送请求node ID。第二个参数info_hash值为torrent文件的infohash。如果接收到请求的node保存了对应info_hash的peers,它会返回一个关键字values,值为列表类型的字符串。列表中每一个字符串包含了”Compact IP-address/port info”格式的peer联系信息。如果收到请求的节点没有保存对应infohash的peers,那么他将返回关键字nodes,值为被请求节点的路由表中离info_hash最近的K个nodes,每个node使用”Compact node info”格式回复。每一种情况中回复包都必须包含请求方用于annouce_peer关键字token,值为二进制字符串。
1 |
arguments: {"id" : "<querying nodes id>", "info_hash" : "<20-byte infohash of target torrent>"} |
1 |
response: {"id" : "<queried nodes id>", "token" :"<opaque write token>", "values" : ["<peer 1 info string>", "<peer 2 info string>"]} |
1 |
or: {"id" : "<queried nodes id>", "token" :"<opaque write token>", "nodes" : "<compact node info>"} |
报文包例子
1 |
get_peers Query = {"t":"aa", "y":"q", "q":"get_peers", "a": {"id":"abcdefghij0123456789", "info_hash":"mnopqrstuvwxyz123456"}} |
1 |
bencoded = d1:ad2:id20:abcdefghij01234567899:info_hash20:mnopqrstuvwxyz123456e1:q9:get_peers1:t2:aa1:y1:qe |
1 |
Response with peers = {"t":"aa", "y":"r", "r": {"id":"abcdefghij0123456789", "token":"aoeusnth", "values": ["axje.u", "idhtnm"]}} |
1 |
bencoded = d1:rd2:id20:abcdefghij01234567895:token8:aoeusnth6:valuesl6:axje.u6:idhtnmee1:t2:aa1:y1:re |
1 |
Response with closest nodes = {"t":"aa", "y":"r", "r": {"id":"abcdefghij0123456789", "token":"aoeusnth", "nodes": "def456..."}} |
1 |
bencoded = d1:rd2:id20:abcdefghij01234567895:nodes9:def456...5:token8:aoeusnthe1:t2:aa1:y1:re |
4. announce_peer
announce_peer请求用来向宣告自己正在某个端口下载某个torrent文件。announce_peer包含4个参数,第一个参数id值为node ID;第二个参数info_hash值为正在下载的torrent文件的infohash;第三个参数是port值为自己peer的整型的端口号,第四个参数数token值为之前get_peers请求中收到的回复中包含的token值。接收到announce_peer请求的node必须检查请求中的token值与之前get_peers时回复给请求节点的token是否相同。如果相同,那么收到请求的节点会根据发送announce_peer节点的IP和请求中包含的port端口号生成一个”Compact IP-address/port info”,并保存在对应的infohash下。
1 |
arguments: {"id" : "<querying nodes id>", |
1 |
"implied_port": <0 or 1>, |
1 |
"info_hash" : "<20-byte infohash of target torrent>", |
1 |
"port" : <port number>, |
1 |
"token" : "<opaque token>"} |
1 |
response: {"id" : "<queried nodes id>"} |
报文包例子
1 |
announce_peers Query = {"t":"aa", "y":"q", "q":"announce_peer", "a": {"id":"abcdefghij0123456789", "implied_port": 1, "info_hash":"mnopqrstuvwxyz123456", "port": 6881, "token": "aoeusnth"}} |
1 |
bencoded = d1:ad2:id20:abcdefghij01234567899:info_hash20:<br /> |
1 |
mnopqrstuvwxyz1234564:porti6881e5:token8:aoeusnthe1:q13:announce_peer1:t2:aa1:y1:qe |
1 |
Response = {"t":"aa", "y":"r", "r": {"id":"mnopqrstuvwxyz123456"}} |
1 |
bencoded = d1:rd2:id20:mnopqrstuvwxyz123456e1:t2:aa1:y1:re |