bt协议有早起的tracker版本(俗称bt1.0)和现在常用的基于DHT的版本(俗称bt2.0),下文为整理的协议相关资料。
tracker版本
bittorrent是一个文件分发协议,它使用url来定位文件而且跟web服务无缝集成。当有多个人同时下载同一个文件时,下载者之间可以互相上传自己已有的那部分文件,让一个文件支持很多人同时下载却只增加小量的带宽负担变成可能,这就是bt协议相比http协议的优势。
1. bt文件分享由下列内容组成:
传统的文件服务器
种子文件(.torrent文件)
bt tracker服务器
文件分享者
web浏览器
web浏览器用户(多个)
2. 一个服务器按照下面的步骤开始文件分享过程
启动一个bt tracker服务器
启动一个普通的web服务器,如apache
在web服务器上配置多媒体类型‘application/x-bittorrent’关联到.torrent文件
生成一个.torrent文件,在文件中添加bt tracker服务器的地址
上传torrent文件到web服务器
发布torrent文件下载页面
等待用户下载
3. 一个用户按照下面的步骤开始文件下载
安装bt客户端
浏览web页面
下载torrent文件
保存torrent文件到本地
使用bt客户端打开torrent文件,开始下载
等待文件下载完成
4. bencoding编码
strings(字符串)编码为:<字符串长度>:<字符串> 例如: 4:test 表示为字符串"test",4:例子 表示为字符串“例子”,字符串长度单位为字节,没开始或结束标记
integers(整数)编码为:i<整数>e,开始标记i,结束标记为e,例如:i1234e 表示为整数1234,i-1234e 表示为整数-1234,整数没有大小限制,i0e 表示为整数0,i-0e 为非法,以0开头的为非法如: i01234e 为非法
lists(列表)编码为:le,开始标记为l,结束标记为e,列表里可以包含任何bencoding编码类型,包括整数,字符串,列表,字典。例如: l4:test5abcdee 表示为二个字符串[“test”,“abcde”]
dictionaries(字典)编码为de,开始标记为d,结束标记为e,关键字必须为bencoding字符串,值可以为任何bencoding编码类型,例如: d3:agei20ee 表示为{“age”=20},d4:path3:C:\8:filename8:test.txte 表示为{“path”=“C:”,“filename”=“test.txt”}
5. metainfo files
metainfo files(俗称torrent文件、bt种子文件),是使用bencoding进行编码的一个dictionaries数据类型,包括以下key
- info(dictionary): 必选, 表示该bt种子文件的文件信息
- 文件信息包括文件的公共部分
1.1) piece length(integer): 必选, 每一数据块的长度
1.2) pieces(string): 必选, 所有数据块的 SHA1 校验值
1.3) publisher(string): 可选, 发布者
1.4) publisher.utf-8(string): 可选, 发布者的 UTF-8 编码
1.5) publisher-url(string): 可选, 发布者的 URL
1.6) publisher-url.utf-8(string): 可选, 发布者的 URL 的 UTF-8 编码 - 如果 bt 种子包含的是单个文件,包含以下内容
2.1) name(string): 必选, 推荐的文件名称
2.2) name.utf-8(string): 可选, 推荐的文件名称的 UTF-8 编码
2.3) length(int): 必选,文件的长度单位是字节 - 如果是多文件,则包含以下部分:
3.1) name(string): 必选, 推荐的文件夹名称
3.2) name.utf-8(string): 可选, 推荐的文件名称的 UTF-8 编码
3.3) files(list): 必选, 文件列表,每个文件列表下面是包括每一个文件的信息,文件信息是个字典 - 文件字典
4.1) length(int): 必选,文件的长度单位是字节
4.2) path(string): 必选,文件名称,包含文件夹在内
4.3) path.utf-8(string): 必选,文件名称 UTF-8 表示,包含文件夹在内
4.4) filehas(string): 可选,文件hash
4.5) ed2k(string): 可选, ed2k 信息
- 文件信息包括文件的公共部分
- announce(string): 必选, tracker 服务器的地址
- announce-list(list): 可选, 可选的 tracker 服务器地址
- creation date(interger): 必选, 文件创建时间
- comment(string): 可选, bt 文件注释
- created by(string): 可选,文件创建者
6. trackers
tracker服务器接收get请求,一个get请求由下列字段组成
• info_hash 20字节的sha1哈希值,是bencoding编码之后的torrent文件内容的hash。
• peer_id: 长度为20的字符串,代表下载者的id,每一个下载者开始下载之前会随机生成自己的id。
• ip : 可选参数,表示文件下载者的id
• port: 文件下载者监听的端口,默认从6881开始,最大的6889
• uploaded: 十进制表示的上传字节总数
• downloaded: 十进制表示的下载字节总数
• left: 十进制表示的剩余字节总数,注意这个值不能通过downloaded和length进行算数计算得到,因为当一些下载文件块的数据的完整性校验失败的,这些文件块必须被重新下载。
• event : 可选参数,有四个可能的至 started,completed,stopped,empty。
tracker返回的内容是一个bencoded dictionaries数据类型,如果返回的内容包含failure reason字段,表示请求失败,failure reason包含失败的理由。如果没有failure reason字段,则返回内容必须包含interval和peers字段。interval代表客户端发起下一次请求的间隔,peers包含一个peer列表。一个peer由peer、id、ip、port组成。
7. peer protocol
bt peer协议是基于tcp或者utp协议。peer连接是对称的,双方可以同时发送数据,而且数据的形式是一样的。一个peer代表一个bt下载用户。
peer协议会使用torrent文件中的pieces块,下标从0开始。当一个peer下载完一个文件块,检查文件块的hash值匹配正确的时候,它就发送一个announce请求给拥有该piece块的peer列表,声明自己拥有该piece块,这样其它的peer就可以向这个peer发送下载该piece的请求。
peer链接两端包含2位的状态:choked,unchoked和interested,uninterested。Choking表示不会发送数据知道一个unchoking动作发生。后面将会解释为什么会存在choking这个状态。
当peer连接的双方有一个是interested状态,而且另外一个不是choking状态,这个连接就可以进行数据传输。无论何时当一个下载者向一个unchoked状态的peer发送下载请求时,interest状态必须每次更新。这个属性的实现比较困难,但是这让下载者知道当它是unchoked的状态时哪些peers将会立刻开始下载成为可能。
peer连接一开始是choked和uninterested状态。当数据开始传输时下载者必须把下载块放进请求队列来获取更高的tcp性能(这就是所谓的管道技术)。另一方面,不能马上写入tcp缓存的请求必须放进队列,而不是放在应用层的网络缓冲区,这样当choke动作发生时,他们才能被丢掉。
peer连线协议由握手和紧跟着的无穷的用长度做前缀的字符串流。握手由19(十进制字符串)跟着字符串’BitTorrent protocol’开始。开始的字符串是长度。后面所有的整数都是使用4字节big-endian(高字节序)固定的头部之后跟着是8个保留字节,在目实现的协议版本中值都是0。如果你想扩展协议,可以使用这8个字节,请和Bram Cohen(bt协议的作者)联系确保所有扩展的兼容性。再接下来是20个字节的sha1哈希值,来自torrent文件中的info字段。如果peer连接的两端发送的哈希值不一致,则连接被关闭。一个例外的情形是当一个下载者想在一个端口开始多线程下载,他们等待进来的连接发送hash,如果这个hash在它的维护的列表里面,就把这个hash返回给另一端。20字节hash的后面是20字节的peer id,这个id会发送给bt tracker服务器,而且会出现在bt tracker返回的peer列表里面。
上面就是一次握手所有的内容。接下来是可选一系类的长度做前缀的消息。长度为0表示keepalives(保持连接),被忽略。保持连接消息每两分钟发送一次。
8. peer messages
所有的非保持连接的消息由一个表示类型的字节开始。
类型列表如下:
0 - choke
1 - unchoke
2 - interested
3 - not interested
4 - have
5 - bitfield
6 - reques5t
7 - piece
8 - cancel
‘choke’, ‘unchoke’, ‘interested’, 和 ‘not interested’ 这四个消息没有消息体。bitfield消息只作为第一个消息发送一次。它的消息体是一个bitfield数据类型(参考c语言),下载者发送过的块的下标对应的位置1,其余的置0。下载者没有任何内容的时候可以跳过bitfield消息。bitfield的第一个字节对应下标0 - 7,第二个对应 8 - 15,等等。多余的位置0。
have消息的消息体是一个数字,表示下载者最近下载完成和检验正确的文件块的下标。
request消息包含index、begin、length三个字段。最后两个是字节偏移。length通常是2的指数除非是文件的最后一块。当前所有bt协议的实现版本中length的值是16kiB,关闭连接的request中length字段的值要大于16kiB。
cancel消息和request有一样的消息体。用来取消下载某一个文件块。
piece消息包含index、begin、piece字段。它们的值和request消息是相关的。下载者通常用随机的顺序下载文件块,这样能提高效率。
choking发生的原因有几个。一个是当一次发起过多的连接时tcp拥塞控制表现极差。另外choking可以让peer连接的双方使用 tit-for-tat-ish算法来保证下载速度的一致。
一个好的choking算法必须具备几个好的特性。它应该能控制并发数量来获取更高的tcp性能。它应该避免过快的choking和unchoking。最后它应该每过一段时间就尝试空闲连接来找到更好的连接,这就是所谓的unchoking优化。
基于DHT的bt协议
由于很多国家、地区通过封禁tracker的方式试图封停bt下载以保护版权、控制网络传输等,因此tracker式的bt受到了极大的打击。而基于DHT的去中心化网络则慢慢兴起。
基于DHT的bt下载流程包括:借助于DHT和KRPC完成Node节点寻址、资源对应的Peer获取,底层uTP以及Peer Wire握手,获取到目标资源的"种子信息(infohash/filename/pieces分块sha1)",完成下载。
1. DHT算法
DHT常见算法主要有Kademlia、chord、Pastry等。其中Kademlia最为流行,BT及BT的衍生派(Mainline, Btspilits, Btcomet, uTorrent…),eMule及eMule各类Mods(verycd, easy emules, xtreme…)等P2P文件分享软件都是基于该算法来实现DHT网络的。
关于Kademlia算法的详细内容在此不做介绍,因为篇幅较长。核心思想可以用如下的例子来表明:
常见的tracker好比是学校图书馆,里面存满了书,学生需要统一去图书馆进行登记借阅。而分布式则是,拆掉图书馆(不设立中心化的服务器),将图书馆里所有的书都分发到每位学生手上(所有的文件分散存储在各个节点上)。即是所有的学生,共同组成了一个分布式的图书馆。为了方便查找,Kademlia作了下面这种安排:
假设某本书的书名的hash值是 00010000,那么这本书就会被要求存在学号为00010000的同学手上。但还得考虑到会有同学缺勤。算法要求这本书不能只存在一个同学手上,而是被要求同时存储在学号最接近00010000的k位同学手上,即00010001、00010010、00010011…等同学手上都会有这本书。
同样地,当你需要找这本书时,将书名hash一下,得到 00010000,这个便是索书号,你就知道该找哪(几)位同学了。剩下的问题,就是找到这(几)位同学的手机号。
一个可行的思路就是在你的通讯录里找到一位拥有目标同学的联系方式的同学。前面提到,每位同学手上的通讯录都是按距离分层的。算法的设计是,如果一个同学离你越近,你手上的通讯录里存有ta的手机号码的概率越大。而算法的核心的思路就可以是:当你知道目标同学Z与你之间的距离,你可以在你的通讯录上先找到一个你认为与同学Z最相近的同学B,请同学B再进一步去查找同学Z的手机号。
以0000110为基础节点,如果一个节点的ID,前面所有位数都与它相同,只有最后1位不同,这样的节点只有1个——0000111,与基础节点的异或值为0000001,即距离为1;对于0000110而言,这样的节点归为“k-bucket 1”;
如果一个节点的ID,前面所有位数相同,从倒数第2位开始不同,这样的节点只有2个:0000101、0000100,与基础节点的异或值为0000011和0000010,即距离范围为3和2;对于0000110而言,这样的节点归为“k-bucket 2”;
……
如果一个节点的ID,前面所有位数相同,从倒数第n位开始不同,这样的节点只有2(i-1)个,与基础节点的距离范围为[2(i-1), 2i);对于0000110而言,这样的节点归为“k-bucket i”;
我们现在来阐述一个完整的索书流程。
A同学(学号00000110)想找《分布式算法》,A首先需要计算书名的哈希值,hash(《分布式算法》) = 00010000。那么A就知道ta需要找到00010000号同学(命名为Z同学)或学号与Z邻近的同学。
Z的学号00010000与自己的异或距离为 00010110,距离范围在[24, 25),所以这个Z同学可能在k-bucket 5中(或者说,Z同学的学号与A同学的学号从第5位开始不同,所以Z同学可能在k-bucket 5中)。
然后A同学看看自己的k-bucket 5有没有Z同学:
如果有,那就直接联系Z同学要书;
如果没有,在k-bucket 5里随便找一个B同学(注意任意B同学,它的学号第5位肯定与Z相同,即它与Z同学的距离会小于24,相当于比Z、A之间的距离缩短了一半以上),请求B同学在它自己的通讯录里按同样的查找方式找一下Z同学:
– 如果B知道Z同学,那就把Z同学的手机号(IP Address)告诉A;
– 如果B也不知道Z同学,那B按同样的搜索方法,可以在自己的通讯录里找到一个离Z更近的C同学(Z、C之间距离小于23),把C同学推荐给A;A同学请求C同学进行下一步查找。
由此,可以将节点的定位变成类似二叉搜索的方式进行。保证对于任意n个学生,最多只需要查询log2(n)次,即可找到获得目标同学的联系方式
2. peer和node
一个peer是一个实现了bt协议并且开启了TCP监听端口的bt客户端或者服务器。一个node是一个实现了DHT协议并且开启了UDP监听端口的bt客户端或者服务器,这两者非常容易混淆。
DHT由很多node以及这些node保存的peer地址信息组成,一个bt客户端包括了一个DHT node节点,通过这些node节点来和DHT网络中的其它节点通信来获取peer的信息,然后再通过bt协议从peer下载文件。流程如下。
- 当node要为 torrent(种子文件) 寻找 peer(保存了目标资源的IP) 时,它将自己路由表中的node ID 和 torrent 的 infohash(资源HASH) 进行"距离对比"(node和目标文件的距离),然后向路由表中离 infohash 最近的node发送请求,问它们正在下载这个 torrent 的 peer 的联系信息
- 因为资源HASH和node HASH都共用一套20bytes的命名空间,所以DHT node节点充当了peer的"代理"的工作,我们不能直接向peer发起资源获取请求(即使这个peer确实存储了我们的目标资源),因为peer本身不具备处理P2P request/response能力的,我们需要借助DHT的能力,让DHT告诉我们哪个peer保存了我们想要的资源或者哪个DHT node可能知道从而递归地继续去问那个DHT网络
- 如果一个被联系的node知道下载这个 torrent 的 peer 信息,那个 peer 的联系信息将被回复给当前node。否则,那个被联系的node则必须回复在它的路由表中离该 torrent 的 infohash 最近的node的联系信息,
- 最初的node重复地请求比目标 infohash 更近的node,直到不能再找到更近的node为止
- 查询完了之后,客户端把自己作为一个 peer 插入到所有回复node中离种子最近的那个node中,这一步背后的含义是: 我之前是请求这个资源的人,我们现在获取到资源了,我在下载这个文件的同时,我也要充当一个新的peer来向其他的客户端贡献自己的文件共享,这样,当另外的其他客户端在发起新的请求的时候,DHT节点就有可能把当前客户端对应的peer返回给新的请求方,这样不断发展下去,这个资源的热度就越来越热,下载速度也越来越快
- 请求 peer 的返回值包含一个不透明的值,称之为"令牌(token)"
- 如果一个node宣布它所控制的 peer 正在下载一个种子(即该node拥有该文件资源),它必须在回复请求node的同时,附加上对方向我们发送的最近的"令牌(token)"。这样当一个node试图"宣布"正在下载一个种子时,被请求的node核对令牌和发出请求的node的 IP 地址。这是为了防止恶意的主机登记其它主机的种子。由于令牌仅仅由请求node返回给收到令牌的同一个node,所以没有规定他的具体实现。但是令牌必须在一个规定的时间内被接受,超时后令牌则失效。在 BitTorrent 的实现中,token 是在 IP 地址后面连接一个 secret(通常是一个随机数),这个 secret 每五分钟改变一次,其中 token 在十分钟以内是可接受的。
3. 路由表
- 每个node节点维护一个路由表保存已知的好节点。路由表中的节点是用来作为在 DHT 中请求的起始点。路由表中的节点是在不断的向其他节点请求过程中,对方节点回复的。即DHT中的K桶中的节点,当我们请求一个目标资源的时候,我们根据HASH XOR从自己的K桶中选择最有可能知道该资源的节点发起请求,而被请求的节点也不一定知道目标资源所在的peer,这个时候被请求方会返回一个新的"它认为可能知道这个peer的节点",请求方收到这个新的节点后,会把这个节点保存进自己的K桶内,然后继续发起请求,直到找到目标资源所在的peer为止
- 并不是我们在请求过程中收到的节点都是平等的,有的节点是好的,而另一些则不是。许多使用 DHT 协议的节点都可以发送请求并接收回复,但是不能主动回复其他节点的请求,这种节点被称之为"坏节点"
- 节点的路由表只包含已知的好节点,这很重要。好节点是指在过去的 15 分钟以内,曾经对我们的某一个请求给出过回复的节点(存活好节点),或者曾经对我们的请求给出过一个回复(不用在15分钟以内),并且在过去的 15 分钟给我们发送过请求。上述两种情况都可将节点视为好节点。在 15 分钟之后,对方没有上述 2 种情况发生,这个节点将变为可疑的。当节点不能给我们的一系列请求给出回复时,这个节点将变为坏的。相比那些未知状态的节点,已知的好节点会被给于更高的优先级。因此如果我们要做DHT嗅探,我们的嗅探器除了要能够发出FIND_NODE请求及接收返回之外,还需要能够响应其他节点发来的请求(get_peers/announce_peer),这样才不会被其他节点列入"可疑"甚至"坏节点"列表中。
- 路由表覆盖从 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 和 2159…2160,以log2N的方式不断分裂,类似于Kademlia中的K桶机制
- 当桶装满了好节点,新的节点会被丢弃。一旦桶中的某个节点变为了坏的节点,那么我们就用新的节点来替换这个坏的节点。如果桶中有在 15 分钟内都没有活跃过的节点,我们将这样的节点视为可疑的节点,这时我们向最久没有联系的节点发送 ping。如果被 ping 的节点给出了回复,那么我们向下一个可疑的节点发送 ping,不断这样循环下去,直到有某一个节点没有给出 ping 的回复,或者当前桶中的所有节点都是好的(也就是所有节点都不是可疑节点,他们在过去 15 分钟内都有活动)。如果桶中的某个节点没有对我们的 ping 给出回复,我们最好再试一次(再发送一次 ping,因为这个节点也许仍然是活跃的,但由于网络拥塞,所以发生了丢包现象,注意 DHT 的包都是 UDP 的),而不是立即丢弃这个节点或者直接用新节点来替代它。这样,我们得路由表将充满稳定的长时间在线的节点
- 每个桶都应该维持一个 lastchange 字段来表明桶中节点的"新鲜"度。当桶中的节点被 ping 并给出了回复,或者一个节点被加入到了桶,或者一个节点被新的节点所替代,桶的 lastchange 字段都应当被更新。如果一个桶的 lastchange 在过去的 15 分钟内都没有变化,那么我们将更新它。这个更新桶操作是这样完成的
- 从这个桶所覆盖的范围中随机选择一个 ID,并对这个 ID 执行 find_nodes 查找操作。常常收到请求的节点通常不需要常常更新自己的桶
- 反之,不常常收到请求的节点常常需要周期性的执行更新所有桶的操作,这样才能保证当我们用到 DHT 的时候,里面有足够多的好的节点
- 在插入第一个节点到路由表并启动服务后,这个节点应试着查找 DHT 中离自己更近的节点,这个查找工作是通过不断的发出 find_node 消息给越来越近的节点来完成的,当不能找到更近的节点时,这个扩散工作就结束了
- 路由表应当被启动工作和客户端软件保存(也就是启动的时候从客户端中读取路由表信息,结束的时候客户端软件记录到文件中)
4. bt协议扩展
BitTorrent 协议已经被扩展为可以在通过 tracker 得到的 peer 之间互相交换节点的 UDP 端口号(也就是告诉对方我们的 DHT 服务端口号),在这样的方式下,客户端可以通过下载普通的种子文件来自动扩展 DHT 路由表(我直接知道某个节点有某一个资源)。新安装的客户端第一次试着下载一个无 tracker 的种子时,它的路由表中将没有任何节点,这是它需要在 torrent 文件中找到联系信息
- peers 如果支持 DHT 协议就将 BitTorrent 协议握手消息的保留位的第 8 字节的最后一位置为 1
- 这时如果 peer 收到一个 handshake 表明对方支持 DHT 协议,就应该发送 PORT 消息。它由字节 0x09 开始,payload 的长度是 2 个字节,包含了这个 peer 的 DHT 服务使用的网络字节序的 UDP 端口号
- 当 peer 收到这样的消息时应当向对方的 IP 和消息中指定的端口号的节点发送 ping
- 如果收到了 ping 的回复,那么应当使用上述的方法将新节点的联系信息加入到路由表中
5. torrent文件扩展
一个无 tracker 的 torrent 文件字典不包含 announce 关键字,而使用 nodes 关键字来替代。这个关键字对应的内容应该设置为 torrent 创建者的路由表中 K 个最接近的节点(可供选择的),这个关键字也可以设置为一个已知的可用节点(这意味着接收到这个种子文件的客户端能够向这些节点发出解析请求,询问资源的所在位置),比如这个 torrent 文件的创建者
不要自动加入 router.bittorrent.com 到 torrent 文件中或者自动加入这个节点到客户端路由表中。这么做还有另一个好处,这个对等网络可以保持无中心化,对于外部新加入的新节点来说,它可以不用通过"中心引导节点"来加入网络,隐藏了"中心引导节点"的存在,增强了对等网络的隐蔽性。
nodes= [[“”, ], [“”,], …]
nodes= [[“127.0.0.1”, 6881], [“your.router.node”,4804]]
6. KRPC协议
KRPC是BitTorrent在Kademlia理论基础之上定义的一个通信消息格式协议,主要用来支持peer节点的获取(get_peer)和peer节点的声明(announce_peer),以及判活心跳(ping)、节点寻址(find_node),它在find_node的原理上和DHT是一样的,同时增加了get_peer / announce_peer / ping协议的支持。KRPC协议是由B编码组成的一个简单的RPC结构,有4种请求:ping、find_node、get_peers 和 announce_peer。
Peers的联系信息被编码为6字节的字符串。又被称为"CompactIP-address/port info",其中前4个字节是网络字节序的IP地址,后2个字节是网络字节序的端口。
Nodes的联系信息被编码为26字节的字符串。又被称为"Compactnode info",其中前20字节是网络字节序的nodeID,后面6个字节是peers的"CompactIP-address/port info"。
KRPC协议框架如下:
- t关键字: 每条消息都包含 t 关键字,它是一个代表了 transaction ID 的字符串。transaction ID 由请求节点产生,并且回复中要包含回显该字段(挑战-响应模型),所以回复可能对应一个节点的多个请求。transaction ID 应当被编码为一个短的二进制字符串,比如 2 个字节,这样就可以对应 2^16 个请求
- y关键字: 它由一个字节组成,表明这个消息的类型。y 对应的值有三种情况
- q 表示请求(请求Queries): q类型的消息它包含 2 个附加的关键字 q 和 a
1.1) 关键字 q: 是字符串类型,包含了请求的方法名字(get_peers/announce_peer/ping/find_node)
1.2) 关键字 a: 一个字典类型包含了请求所附加的参数(info_hash/id…) - r 表示回复(回复 Responses): 包含了返回的值。发送回复消息是在正确解析了请求消息的基础上完成的,包含了一个附加的关键字 r。关键字 r 是字典类型
2.1) id: peer节点id号或者下一跳DHT节点
2.2) nodes": “”
2.3) token: token - e 表示错误(错误 Errors): 包含一个附加的关键字 e,关键字 e 是列表类型
3.1) 第一个元素是数字类型,表明了错误码,当一个请求不能解析或出错时,错误包将被发送。下表描述了可能出现的错误码
201: 一般错误
202: 服务错误
203: 协议错误,比如不规范的包,无效的参数,或者错误的 toke
204: 未知方法
3.2) 第二个元素是字符串类型,表明了错误信息
错误包例子:
一般错误={“t”:“aa”, “y”:“e”, “e”:[201,“A Generic Error Ocurred”]}
B编码=d1:eli201e23:AGenericErrorOcurrede1:t2:aa1:y1:ee
- q 表示请求(请求Queries): q类型的消息它包含 2 个附加的关键字 q 和 a
7. DHT请求
所有的请求都包含一个关键字id,它包含了请求节点的nodeID。所有的回复也包含关键字id,它包含了回复节点的nodeID。
(1) ping
最基础的请求就是ping,用于检测节点是否可达。这时KPRC协议中的“q”=“ping”。Ping请求包含一个参数id,它是一个20字节的字符串包含了发送者网络字节序的nodeID。对应的ping回复也包含一个参数id,包含了回复者的nodeID。
参数: {“id” : “”}
回复:{“id” : “”}
报文包例子
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
(2) 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” : “”, “target” : “”}
回复: {“id” : “”, “nodes” : “”}
报文包例子
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”:“def456…”}}
B编码
=d1:rd2:id20:0123456789abcdefghij5:nodes9:def456…e1:t2:aa1:y1:re
(3) 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” : “”,“info_hash” : “<20-byte infohash of targettorrent>”}
回复:{“id” : “”,“token” :“”,“values” : [“<peer 1 info string>”,“<peer 2 info string>”]}
or:{“id” : “”,“token” :“”,“nodes” : “”}
报文包例子
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:t2: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”: “def456…”}}
B编码
=d1:rd2:id20:abcdefghij01234567895:nodes9:def456…5:token8:aoeusnthe1:t2:aa1:y1:re
(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”: “”, “info_hash” :“<20-byte infohash of target torrent>”, “port”: , “token” : “”}
回复: {“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: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
8. utp
uTP协议是一个基于UDP的开放的BT点对点文件共享协议。在uTP协议出现之前,BT下载会占用网络中大量的链接,直接导致其它网络应用服务质量下载和网络的拥堵,因此有很多ISP都开始限制BT的下载。uTP减轻了网络延迟并解决了传统的基于TCP的BT协议所遇到的拥塞控制问题,提供可靠的有序的传送。
9. Peer Wire协议
在BitTorrent中,节点的寻址是通过DHT实现的,而实际的资源共享和传输则需要通过uTP以及Peer Wire协议来配合完成
(1)握手
Peer Wire协议是Peer之间的通信协议,通常由一个握手消息开始。握手是一个必需的报文,并且必须是客户端发送的第一个报文。该握手报文的长度是(49+len(pstr))字节。握手消息的格式如下:
<pstrlen><pstr><reserved><info_hash><peer_id>
pstrlen: 字符串长度,单个字节。
pstr: 协议的标识符,字符串类型。
reserved: 8个保留字节。当前的所有实现都使用0.这些字节里面的每一个字节都可以用来改变协议的行为。来自Bram的邮件建议应该首先使用后面的位,以便可以使用前面的位来改变后面位的意义。
info_hash: 元信息文件中info键(key)对应值的20字节SHA1哈希。这个info_hash和在tracker请求中info_hash是同一个。
peer_id: 用于唯一标识客户端的20字节字符串。这个peer_id通常跟在tracker请求中传送的peer_id相同(但也不尽然,例如在Azureus,就有一个匿名选项)。
在BitTorrent协议1.0版本,pstrlen = 19, pstr = “BitTorrent protocol”。
连接的发起者应该立即发送握手报文。如果接收方能够同时地服务多个torrent,它会等待发起者的握手报文(torrent由infohash唯一标识)。尽管如此,一旦接收方看到握手报文中的info_hash部分,接收方必须尽快响应。tracker的NAT-checking特性不会发送握手报文的peer_id字段。
如果一个客户端接收到一个握手报文,并且该客户端没有服务这个报文的info_hash,那么该客户端必须丢弃该连接。
如果一个连接发起者接收到一个握手报文,并且该报文中peer_id与期望的peer_id不匹配,那么连接发起者应该丢弃该连接。注意发起者可能接收来自tracker的peer信息,该信息包含peer注册的peer_id。来自于tracker的peer_id需要匹配握手报文中的peer_id。
peer_id长20个字节。至于怎么将客户端和客户端版本信息编码成peer_id,现在主要有两种惯例:Azureus风格和Shadow风格。
Azureus风格使用如下编码方式:’-’, 紧接着是2个字符的client id,再接着是4个数字的版本号,’-’,后面跟着随机数。
例如:‘-AZ2060-’…
Shadow风格使用如下编码方式:一个用于客户端标识的ASCII字母数字,多达五个字符的版本号(如果少于5个,则以’-’填充),紧接着是3个字符(通常是’—’,但也不总是这样),最后跟着随机数。版本字符串中的每一个字符表示一个0到63的数字。‘0’=0, …, ‘9’=9, ‘A’=10, …, ‘Z’=35, ‘a’=36, …, ‘z’=61, ‘.’=62, ‘-’=63。
(2)状态信息
一个客户端(client)必须维持其与每一个远程peer(端)连接的状态信息:
choked: 远程peer(端)是否已经choke本客户端。当一个peer(端) choke本客户端后,它是在通知本客户端,除非它unchoke本客户端,否则它不会应答该客户端所发出的任何请求。本客户端也不应该试图向远程peer发送数据请求,并且应该认为所有没有应答的请求已经被远程peer丢弃。
interested: 远程peer(端)是否对本客户端提供的数据感兴趣。这是远程peer在通知本客户端,当本客户端unchoke他们时,远程客户端将开始请求块(block)。注意这也意味着本客户端需要记录它是否对远程 peer(端)感兴趣,以及它是否choke/unchoke远程peer。因此真正的列表看起来像这样:
am_choking: 本客户端正在choke远程peer。
am_interested: 本客户端对远程peer感兴趣。
peer_choking: 远程peer正choke本客户端。
peer_interested: 远程peer对本客户端感兴趣。
客户端连接开始时状态是choke和not interested(不感兴趣)。换句话就是:
am_choking = 1
am_interested = 0
peer_choking = 1
peer_interested = 0
当一个客户端对一个远程peer感兴趣并且那个远程peer没有choke这个客户端,那么这个客户端就可以从远程peer下载块(block)。当一个客户端没有choke一个peer,并且那个peer对这个客户端这个感兴趣时,这个客户端就会上传块(block)。
客户端必须不断通知它的peers,它是否对它们感兴趣,这一点是很重要的。客户端和每个端的状态信息必须保持最新,即使本客户端被choke。这允许所有的peer知道,当它们unchoke该客户端后,该客户端是否开始下载(反之亦然)。
10. 获取资源完成下载
磁力链是为了简化BT种子文件的分发,封装了一个简化版的magnet url,客户端解析这个magnet磁力链之后,需要在DHT网络中寻找infohash对应的peer节点,获取节点成功后,向目标peer节点获取真正的BitTorrent种子(torrent文件)信息(包含了完整的pieces SHA1杂凑信息),另一个渠道就是传统的Bt种子论坛会分发BT种子文件。
magnet URL的格式如下:
magnet:?xt=urn:btih:&dn=&tr=
- : Infohash的16进制编码,共40字符。为了与其它的编码兼容,客户端应当也支持32字符的infohash base32编码
- Xt是唯一强制的参数
- dn是在等待metadata时可能供客户端显示的名字
- 如果只有一个字段,Tr是tracker的url,如果有很多的tracker,那么多个tr字段会被包含进去
#dn和tr都是可选的
为了获取目标资源的种子信息(infohash/filename/pieces分块sha1),需要在Peer Wire握手之后发出扩展支持交互消息。这是一个B编码的字典,包括三类不同的消息。
1 request(请求):
请求消息并不在字典中附加任何关键字,这个消息的回复应当来自支持这个扩展的peer,是一个reject或者data消息,回复必须和请求所指出的片相同
Peer必须保证它所发送的每个片都通过了infohash的检测。即直到peer获得了整个metadata并通过了infohash的验证,才能够发送片(即一个peer应该保证自己已经完整从其他peer中拷贝了一份相同的资源文件后,才能继续响应其他节点的拷贝请求)。Peers没有获得整个metadata时,对收到的所有metadata请求都必须直接回复reject消息
#exampel
{'msg_type': 0, 'piece': 0}
d8:msg_typei0e5:piecei0ee
#这代表请求消息在请求metadata的第一片
2 data
这个data消息需要在字典中添加一个新的字段,“total_size”.这个关键字段和extension头的"metadata_size"有相同的含义,这是一个整型
Metadata片被添加到bencode字典后面,他不是字典的一部分,但是是消息的一部分(必须包括长度前缀)。
如果这个片是metadata的最后一个片,他可能小于16KB。如果它不是metadata的最后一片,那大小必须是16KB
# example
{'msg_type': 1, 'piece': 0, 'total_size': 3425}
d8:msg_typei1e5:piecei0e10:total_sizei34256eexxxxxxxx...
# x表示二进制数据(metadata)
3 reject
Reject消息没有附件的关键字。它的意思是peer没有请求的这个metadata片信息
在客户端收到收到一定数目的消息后,可以通过拒绝请求消息来进行洪泛攻击保护。尤其在metadata的数目乘上一个因子时
#
{'msg_type': 2, 'piece': 0}
d8:msg_typei1e5:piecei0ee
4 request请求获取信息的格式如下:
{
e: 0,
ipv4: xxx,
ipv6: xxx,
complete_ago: 1,
m:
{
upload_only: 3,
lt_donthave: 7,
ut_holepunch: 4,
ut_metadata: 2,
ut_pex: 1,
ut_comment: 6
},
matadata_size: 45377,
p: 33733,
reqq: 255,
v: BitTorrent 7.9.3
yp: 19616,
yourip: xxx
}
m: 是一个字典,表示客户端支持的所有扩展以及每个扩展的编号
1) ut_pex: 表示该客户端支持PEX(Peer Exchange)
2) ut_metadata表示支持BEP-009(也就是交换种子文件的metadata)
总结
本文包含了BT下载相关知识的介绍,可能还有很多遗漏,后续再继续补充。
欢迎关注本人公众号,公众号会更新一些不一样的内容,相信一定会有所收获。