【转】P2P-BT对端管理协议

对端管理
指的是远端peer集合的管理(虽然自身client也可以视为一个peer,但对端管理不包括自身peer)

一个客户端(client)必须维持与每一个远程peer连接的状态信息,即1V1关系(本端对某个远端peer) 
在本代码中PcPeer指这种1V1关系,而不是仅指远程peer 
对于每个连接连接来说,每一端的peer应该是4种状态之一:一端是interested或者not interested,则另一端是choking或者unchoking。反之亦然。Choking表示直到unchoking发生之前,都不会有任何数据传送给连接另一端的peer 
代码如下:

#define CPE_STAT_NONE                ((char)0x00)
#define CPE_STAT_LC_I                ((char)0x01)                        //B 是否拥有 A 感兴趣的分片
#define CPE_STAT_RM_I                ((char)0x02)                        //A 是否拥有 B 感兴趣的分片
#define CPE_STAT_LC_C                ((char)0x04)                        //A 能否向 B 请求分片数据
#define CPE_STAT_RM_C                ((char)0x08)                        //B 能否向 A 请求分片数据
1
2
3
4
5
这些信息由如下关键字来描述:

choke(拒绝/阻塞)、unchoke(接受)两种状态 
如果远程peer对本client是choke,远程peer将不会接收来自本client的请求 
而本client在接收到来自远程peer的choke消息后,就不会再试图向远程peer发送数据请求,因为本客户端会认为所有发给远程peer的请求都已被丢弃

interest(兴趣) 
远程peer是否对本client提供的数据感兴趣,这由远程peer通知本client,当本client不choke它时,远程peer就开始请求块(block),这也意味着 
本代码中PcPeer需要记录本client是否对远程peer感兴趣,以及它是否choke/unchoke远程peer

客户端连接开始时的初始状态为choke和not interstered(不感兴趣)

当本client对远程peer感兴趣,并且该远程peer没有choke时,那么本client就可以从远程peer下载块(block) 
当本client没有choke远程peer,而且该远程peer对本client感兴趣时,本client就会上传块(block) 
这也就意味着,在建立连接后,本client必须不断的通知远程peer,告诉远程peer,是否对它还感兴趣 
本和远程peer的状态信息必须保持最新,即使本client被choke,这样才能保证,当远程peer不再choke本client时,本client可以开始下载(反之亦然)

对端通信协议
对端之间要进行通信,需要使用P2P通信协议 
本代码位于

bool CPcPeer::pack(char *pack, int &len, HOSTHANDLE& addr)
bool CPcPeer::unpack(char *pack, const int len)
1
2
P2P协议的消息类型如下:

// 下述为0x0f下的
#define P2P_CONTROL_INV  ((char)0x01)    //连接请求
#define P2P_CONTROL_PAV  ((char)0x02)    //连接响应
#define P2P_CONTROL_CMD  ((char)0x03)    //命令
#define P2P_CONTROL_DAT  ((char)0x04)    //数据
#define P2P_CONTROL_LIV  ((char)0x05)    //心跳
// 下述为0xf0下的
#define P2P_MASK_CHK     ((char)0x80)        //CHOKE标记掩码
#define P2P_MASK_REQ     ((char)0x40)        //REQUEST标记掩码
#define P2P_MASK_HAV     ((char)0x20)        //HAVE标记掩码

PcPeer的step状态为

enum {ST_NONE, ST_PEND, ST_LINK, ST_TRAN};
1
初始本client和远程peer均为ST_NONE状态 
step状态的转换图如下: 

stepç¶æç转æ¢å¾
 
1.本client发送连接请求报文后,本client状态更新为ST_PEND 
远程peer收到连接请求协议后,远程peer的状态更新为ST_LINK 
2.远程peer发送连接请求回复协议,远程peer的状态更新为ST_TRAN 
3.本client收到远端peer发回的连接回复协议,本client状态由ST_PEND更新为ST_TRAN 
4.此后本client和远端peer状态均为ST_TRAN

协议流程图如下: 
 è¿éåå¾çæè¿°

è¿éåå¾çæè¿°

è¿éåå¾çæè¿°


任务文件
http:serve的地址列表
info_hash:任务文件中info部分的sha校验码,即发布的种子数据
peer_id:自身的标识
port:提供上传的端口号
ip:你的ip地址,没有的话serve会自己找到
uploaded、dowloaded:你上传和下载了多少,服务器可以用它做流量分析
left:还要下载多少字节
event:状态,告诉服务端你是准备开始下载,还是停止,或下载完成了 
BT在打开一个任务文件后,先选择文件保存在哪里,然后判断文件不存在的话就建立新文件,存在的话就用sha-1去校验文件,如果校验错误,说明是未下载的文件,就可以实现续传
服务器
服务端会使用info_hash查找列表,同时,根据ip和port进行反向连接,以测试用户是内网用户还是公网用户(如果是内网用户,是无法连通的)
服务端的返回信息中,有一个时间参数,用来告诉客户端用户隔多少时间一次服务端,因为BT的动态性非常高,1秒内,可能有成千的peer加入或退出,所以需要一个参数设置查询频率
分片选择算法
严格的优先级 
第一个策略是,一旦请求了某个分片的子片段,然后该分片余下的子片段会优先被请求,这样,可以尽可能获得一个完整的分片

最少优先 
对一个下载者来说,选择下一个被下载分片时,通常选择它的peer们所拥有的最少的那个分片,即最少优先,这种技术,确保了每个peer都拥有其他peer们最希望得到的那些片段,从而一旦有需要,上传就可以开始

随机的第一个片段 
最少优先的一个例外是下载开始时,此时,下载者没有任何分片可供上传,所以需尽快获得一个完整的分片,所以,第一个分片是随机选择的,直到第一个分片下载完成,才切换回最少优先的策略

最后阶段模式 
有时候,peer可能从一个速率很慢的peer那里请求一个分片,为了防止这种情况,在最后阶段,peer向所有的peer都发送某分片的子片段请求,一旦某些子片段到了,就向其他的peers发送取消消息,取消对这些子片段的请求,以避免带宽浪费

请求的并发发送
在BT中,很重要的一点是同时发送多个请求,以避免单个请求的两个片段发送之间的延迟

peer发送到serve的请求
info_hash: 即种子hash
peer_id:使用URL编码的20字节串,用于标识客户端的唯一ID,由客户端启动时生成。这个ID可以是任意值,甚至可能是二进制数据。目前没有生成该ID的标准准则,尽管如此,实现者必须保证该ID对于本地主机是唯一的,因此可以通过包含进程ID和启动时记录的时间戳(timestamp)来达到这个目的
port:客户端正在监听的端口号。为BitTorrent协议保留的端口号是6881-6889
uploaded:peer已上传的总量(从start事件开始)
downloaded:peer已下载的总量(从start事件开始)
left:客户端还未下载的字节数,以十进制表示
compact:设置为1,表示客户端接收压缩的响应包,这时peers列表将被peers串替换,peers串中每个peer占6个字节(前4表示ip,后2表示端口)
numwant(可选):客户端希望从serve接受的peers数,如未设置,默认为50
ip(可选):客户端的IP地址,只有当请求的IP地址不是客户端的IP地址时,才需要此参数(客户端通过代理的方式交互)
key:当客户端ip改变时,可用该标识来证明自己的身份
event:如果指定,必须是started/completed/stoped和空之中的一个,如果一个请求不指定event,表示它只是每隔一段时间发送的请求,event取值如下
started:第一个发送到serve的请求,其event必须为此值
stopped:如果正常关闭客户端,必须发该事件到serve
completed: 如果下载完成,必须发送该事件到serve,如果客户端启动前,就已完成下载,则没必要发送,serve仅仅基于此事件增加已完成的下载数 
event指定了值,但值为空,那么它和event不指定值的效果一样(定期发送)
serve回复peer的请求
failure reason:如果包含这个键(key),那么其他的键(key)就不会出现,该键(key)对应的值是一个可读的错误消息,它告诉用户的请求为什么会失败
interval:指定peer间隔发送请求的时间
min interval(可选):最小的请求间隔,表示peer不能在这个时间间隔内向track重发请求
complete:完成整个文件下载的peers数(做种数)
incomplete:未完成文件下载的peers数(非做种数)
peers:peers列表 
peer列表数一般控制在30-55之间,当一个分片下载完成后,客户端需要将have消息发送给大部分活跃的peers,结果是广播通信的代价和peers数目成正比,当这个数高于25以后,新加入的peers已不太可能提升下载速度
Peer端交互
一个客户端(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(不感兴趣)。换句话就是: 
l am_choking = 1 
l am_interested = 0 
l peer_choking = 1 
l peer_interested = 0 
当一个客户端对一个远程peer感兴趣并且那个远程peer没有choke这个客户端,那么这个客户端就可以从远程peer下载块(block)。当一个客户端没有choke一个peer,并且那个peer对这个客户端这个感兴趣时,这个客户端就会上传块(block)。 
客户端必须不断通知它的peers,它是否对它们感兴趣,这一点是很重要的。客户端和每个端的状态信息必须保持最新,即使本客户端被choke。这允许所有的peer知道,当它们unchoke该客户端后,该客户端是否开始下载(反之亦然)。
握手
握手是一个必需的报文,并且必须是客户端发送的第一个报文。该握手报文的长度是(49+len(pstr))字节 
握手的消息格式:handshake: <pstrlen><pstr><reserved><info_hash><peer_id>

pstrlen: 的字符串长度,单个字节。
pstr: 协议的标识符,字符串类型。
reserved: 8个保留字节。当前的所有实现都使用全0.这些字节里面的每一个字节都可以用来改变协议的行为。
info_hash: 种子hash
peer_id: 用于唯一标识客户端的20字节字符串。 
连接的发起者应该立即发送握手报文,一旦接收方看到握手报文中的info_hash部分,接收方必须尽快响应 
如果一个客户端接收到一个握手报文,并且该客户端没有服务这个报文的info_hash,那么该客户端必须丢弃该连接。 
如果一个连接发起者接收到一个握手报文,并且该报文中peer_id与期望的peer_id不匹配,那么连接发起者应该丢弃该连接
报文
接下来协议的所有报文采用如下的结构:<length prefix><message ID><payload>。length prefix(长度前缀)是一个4字节的大端(big-endian)值。message ID是单个十进制值。playload与消息相关。

keep-alive
格式 :<len=0000> 
keep-alive消息是一个0字节的消息,将length prefix设置成0。没有message ID和payload。如果peers在一个固定时间段内没有收到任何报文(keep-alive或其他任何报文),那么peers应该关掉这个连接,因此如果在一个给定的时间内没有发出任何命令的话,peers必须发送一个keep-alive报文保持这个连接激活。通常情况下,这个时间是2分钟

choke
格式 :<len=0001><id=0> 
choke报文长度固定,并且没有payload。

unchoke
格式 :<len=0001><id=1> 
unchoke报文长度固定,并且没有payload。

interested
格式 :<len=0001><id=2> 
interested报文长度固定,并且没有payload。

not interested
格式 :<len=0001><id=3> 
not interested报文长度固定,并且没有payload。

have
格式 :<len=0005><id=4><piece index> 
have报文长度固定。payload是piece(片)的从零开始的索引,该片已经成功下载并且通过hash校验。

实现者注意:实际上,一些客户端必须严格实现该定义。因为peers不太可能下载他们已经拥有的piece(片),一个peer不应该通知另一个peer它拥有一个piece(片),如果另一个peer拥有这个piece(片)。最低限度”HAVE suppresion”会使用have报文数量减半,总的来说,大致减少25-35%的HAVE报文。同时,给一个拥有piece(片)的peer发送HAVE报文是值得的,因为这有助于决定哪个piece是稀缺的。 
一个恶意的peer可能向其他的peer广播它们不可能下载的piece(片)。

bitfield
格式 : <len=0001+X><id=5><bitfield> 
bitfield报文可能仅在握手序列发送之后,其他消息发送之前立即发送。它是可选的,如果一个客户端没有piece(片),就不需要发送该报文。 
bitfield报文长度可变,其中x是bitfield的长度。payload是一个bitfield,该bitfield表示已经成功下载的piece(片)。第一个字节的高位相当于piece索引0。设置为0的位表示一个没有的piece,设置为1的位表示有效的和可用的piece。末尾的冗余位设置为0。 
长度不对的bitfield将被认为是一个错误。如果客户端接收到长度不对的bitfield或者bitfield有任一冗余位集,它应该丢弃这个连接。

request
格式 : <len=0013><id=6><index><begin><length> 
request报文长度固定,用于请求一个块(block)。payload包含如下信息:

index: 整数,指定从零开始的piece索引。
begin: 整数,指定piece中从零开始的字节偏移。
length: 整数,指定请求的长度。
piece
格式 :<len=0009+X><id=7><index><begin><block> 
piece报文长度可变,其中x是块的长度。payload包含如下信息: 
1. index: 整数,指定从零开始的piece索引。 
2. begin: 整数,指定piece中从零开始的字节偏移。 
3. block: 数据块,它是由索引指定的piece的子集。

cancel
格式 :<len=0013><id<=8><index><begin><length> 
cancel报文长度固定,用于取消块请求。playload与request报文的playload相同。一般情况下用于结束下载。

UDP打洞 
NAT(Network Address Translator)有三种: 
a)Full Cone NAT(完全圆锥型):从同一私网地址端口192.168.0.8:4000发至公网的所有请求都映射成同一个公网地址端口1.2.3.4:62000 ,192.168.0.8可以收到任意外部主机发到1.2.3.4:62000的数据报。 
b)Address Restricted Cone NAT (地址限制圆锥型):从同一私网地址端口192.168.0.8:4000发至公网的所有请求都映射成同一个公网地址端口1.2.3.4:62000,只有当内部主机192.168.0.8先给服务器C 6.7.8.9发送一个数据报后,192.168.0.8才能收到6.7.8.9发送到1.2.3.4:62000的数据报。 
c)Port Restricted Cone NAT(端口限制圆锥型):从同一私网地址端口192.168.0.8:4000发至公网的所有请求都映射成同一个公网地址端口1.2.3.4:62000,只有当内部主机192.168.0.8先向外部主机地址端口6.7.8.9:8000发送一个数据报后,192.168.0.8才能收到6.7.8.9:8000发送到1.2.3.4:62000的数据报。 
三者有个共同点,发送数据时,映射的端口是固定的,然后可以通过固定端口和Peer通讯

UDP打洞的过程大致如此:

1、双方都通过UDP与服务器通讯后,网关默认就是做了一个外网IP和端口号 与你内网IP与端口号的映射,这个无需设置的,服务器也不需要知道客户的真正内网IP

2、用户A先通过服务器知道用户B的外网地址与端口

3、用户A向用户B的外网地址与端口发送消息,

4、在这一次发送中,用户B的网关会拒收这条消息,因为它的映射中并没有这条规则。

5、但是用户A的网关就会增加了一条允许规则,允许接收从B发送过来的消息

6、服务器要求用户B发送一个消息到用户A的外网IP与端口号

7、用户B发送一条消息,这时用户A就可以接收到B的消息,而且网关B也增加了允许规则,允许接收从A发送过来的消息

8、之后,由于网关A与网关B都增加了允许规则,所以A与B都可以向对方的外网IP和端口号发送消息。 

è¿éåå¾çæè¿°


BT协议1.0
BitTorrent Protocol Specifications v1.0 翻译 
原文(原始版本):http://www.bittorrent.org/protocol.html 
更详细的版本:http://wiki.theory.org/BitTorrentSpecification 
注: 
1) 本文是原始版本的翻译,如果有晦涩不清的地方,请参考上面的第二个link(更详细的版本)。 
2) 因为没有任何实践,完全是基于文档的自己的理解,所以可能会有翻译和理解错误的地方。 
3) 因为是老版本的协议原文,所述内容和现在实际的BT客户端肯定会有出入。 
4) 协议扩展和DHT相关介绍在官网(http://www.bittorrent.org)上都有,需要请参考。

BitTorrent协议规范1.0 
BitTorrent是一种用来传输文件的协议。它通过URL来识别被传输的文件并且被设计成能够无缝的集成到网络里。 它相对于单纯的HTTP协议的优势是当多个下载者同时下载同一个文件时,下载者之间可以互相传输文件内容(注:提高了下载速度),这使得被下载源(注:也就是指通常http下载时的http server)能够在只增加适量(少量/合理)负载的情况下支持非常大数量的下载者。

一个BitTorrent文件传输系统由以下几部分构成: 
* 一个普通的web服务器 
* 一个静态的元信息文件(注:也就是.torrent文件) 
* 一个BitTorrent的Tracker(注:也就是Tracker服务器:用来追踪当前所有下载者信息的服务器。) 
* 一个原始的下载者(注:即种子。或者叫原始上传者。在BT的世界里,下载者也是上传者。) 
* 终端用户的网页浏览器(注:用来下载torrent文件) 
* 终端用户下载者(注:可以理解为BT的Client端) 
理想的情况下应该有很多的终端用户在下载同一个文件。(注:实际情况也正是如此!)

在网络上提供(发布)一个BT文件以供下载,需要进行以下的步骤: 
1. 启动一个Tracker服务器(通常情况下,已经有一个Tracker服务器正在运行)。 
2. 启动一个普通的web服务器,比如apache,通常应该已经有一个web服务器在运行。 
3. 在web服务器上将扩展名为.torrent文件的mimetype关联为application/x-bittorrent(注:使客户端下载后知道用什么应用程序打开.torrent文件)。 
4. 根据源文件(注:将要下载的文件)和Tranker服务器的URL生成元信息文件(.torrent文件)。 
5. 将生成的元信息文件(.torrent文件)放到web服务器上。 
6. 在某个网页上增加该元信息文件(.torrent文件)的下载link。 
7. 启动一个拥有完整文件的下载者(即源)(注:这时我们有了第一个上传源,并且它有完整的文件,也就是通常所说有一个种子)。

要开始通过BT下载文件,需要进行以下的步骤: 
1. 安装BitTorrent(或者已经装好了) 
2. 浏览网页(注:寻找你下载的文件的.torrent文件) 
3. 点击.torrent文件的link(注:这是你的BT程序应该会自动运行) 
4. 选择下载文件的保存路径,或者继续下载以前未下载完成的文件。 
5. 等待下载完成。 
6. 结束BitTorrent程序(它将继续上传直到你手动停止它)。(注:结束前你就是一个种子)

传输的内容按如下方式编码(注:该编码方式称为bencode): 
* 字符串编码为:其长度的10进制数加冒号加字符串本身。例如:4:span表示字符串’spam’ 
* 数字编码为’i’加10进制数加’e’。例如:i3e表示数字3,而i-3e表示数字-3。数字没有长度限制。i-0e是非法的。所有以0开头的编码,比如i03e,都是非法的,除了i0e,很自然的它表示数字0。 
* 列表(List)编码为字母’l’加列表的元素(元素同样也是bencoded过的)加’e’。例如l4:spam4:eggs表示列表[‘spam’, ‘eggs’]。 
* 字典(Dictionary)编码为字母’d’加key加value加’e’。例如d3:cow3:moo:4spam4:eggse表示{‘cow’:’moo’, ‘spam’:’eggs’},而d4:spaml1:a1:bee表示{‘spam’: [‘a’, ‘b’]}。key必须是字符串,并且必须按照排序后的顺序出现(按照key的原字串排序,不是字母顺序)。(注:sorted as raw strings应该是指基于binary compare的排序,而不是按照某种语言,比如英文,法文,字面意思/逻辑上的顺序)

元信息文件(.torrent文件)是一个以bencoded方式编码过的字典,该字典包含以下的key(及key对应的value): 
announce 
Tracker的URL。 
info 
该key的value是一个map,包含以下的key(及其value): 
name:其值是一个string,建议了被下载文件(或者文件夹)用什么名字保存。它仅仅是一个建议值。 
piece length:其值是一个数字,定义了每个文件块的大小,单位是byte。 
为了数据传输的方便,文件被分成固定大小的块(piece),除了最后一个文件块,因为很可能除不尽。 
piece length通常是2的幂,最常用的是2的18幂=256KB(3.2以前的版本用的是2的20次幂=1MB作为缺省值)。 
pieces:其值是一个长度是20的倍数的string,每一段string(长度为20)内容是其对应顺序的文件块的SH1的哈希值。 
还有两个key是length和files,这两个key不能同时出现,也不能都不出现(也就是说必须也只能出现一个)。 
如果length出现则表示下载的是单个文件,否则就表示下载的是多个文件:以文件夹的形式。 
单个文件的情况下,length就表示该文件的大小,单位是byte。 
考虑到其他key的目的,多文件也作为一个文件处理;按照它们在文件夹里出现的顺序连接成一个文件。 
这种情况下,files的值是一个字典的列表(译者注:每一个字典代表一个文件),每个字典包括以下的key(及其value): 
length:其值是数字,定义文件的长度,单位是byte。 
path:其值是string的列表,每个string表示子文件夹的名字,最后一个string表示文件的名字(长度为零的列表是错误的)。 
单文件的情况下,name就表示这个文件的名字。多文件时,name表示文件夹的名字。

向Tracker服务器以Get方式发送的请求中应包含以下的Key: 
info_hash 
.torrent文件里info字段(bencoded编码后的info字段)的SHA1哈希值,长度为20字节。 
注意info字段只是.torrent文件的一个子串(注:.torrent文件本身就是一个字符串)。 
这20字节的哈希值必须要进行URL编码。 
peer_id 
长度为20的字符串,用作标识下载端的ID。 
由每一个下载端在开始下载的时候随机生成(注:可理解为每次下载不同文件时都生成一个peer_id)。 
该值也必须进行URL编码(注:另一份更加详细的文档中则注明该值不能进行URL编码)。 
ip 
可选项。 
表明当前peer所在的IP地址(或者DNS名)(注:应该是指DNS解析前的主机名)。 
通常用于第一个种子,如果它和Tracker服务器在同一台机器上。 
port 
当前peer所侦听的端口号。 
默认的行为是依次尝试侦听端口6881到6889,如果所有这些端口都被占用,则放弃侦听(注:也就是放弃下载)。 
uploaded 
总上传大小,用十进制ASCII字符表示。(注:从发送”started”事件开始计算,单位应该是字节。) 
downloaded 
总下载大小,用十进制ASCII字符表示。(注:从发送”started”事件开始计算,单位应该是字节。) 
left 
未下载大小(注:还需要下载的内容),用十进制ASCII字符表示(注:单位应该是字节)。 
注意这个数值不可以用文件总字节数和已经下载字节数之差来计算。 
某些已经下载但未完成文件块会因为不能通过一致性检查而被重新下载。 
(注:应该是计算某文件块的哈希值,如果和已知的,即下载完成的该块的哈希值不一样,则一致性检查失败。 
从而认为该文件块没有下载完毕,需要重新下载。) 
event 
可选项。 
其值可以是started,completed,stopped(或者是empty,值是empty和没有该key是一样的)。 
如果没有该key,则本次request被认为是普通的定期request中的一次。 
当下载开始时,第一个request必须包括该key并且其值必须是started。 
当下载完成时,必须发送一个request包含该key并且值为completed。 
但是在文件下载完成后再开始该任务时(注:其实就是做种子),不能发送包含completed的request。 
当下载停止时,必须发送包含stopped的request给Tracker服务器。 
Tracker服务器的response也是以bencoded方式编码过的字典。如果response中包含有名为failure reason的key,那么该key的值是一个人可以理解的字符串(注:含有语义的字符串)用来通知发出请求的BT客户端该请求失败的原因,同时此时该response中不需要有其它任何key。否则,response中必须包含有两个key:interval,值为数字,单位是秒。用来告知BT客户端通常的定期发出的request之间的最小间隔时间。peers,值为字典的列表。其中每个字典包含以下的key:peer id, ip和port。他们的值依次是BT客户端的ID,IP地址或者DNS名,端口号。注意当有事件发生或者需要知道更多的peer时,BT客户端可以向Tracker发送非定期的request。

如果你需要对元信息文件(.torrent文件)或者发送给Tracker服务器的request的内容作任何扩充,请与Bram Cohen协商以确保所有的扩展是兼容的。

BitTorrent的peer协议构建在TCP协议之上。它运作效率很高同时不需要做任何socket的设置。

peer之间的连接时对等的。从peer连接的两个方向上看,发送的消息时一样的,同时数据可以向连接的任何一个方向上传送。

peer协议是以元信息文件(.torrent文件)里描述的排序的文件块(file piece)这一概念为基础的,排序的序号从0开始。当某个peer下载完某个文件块,并验证其hash值正确后,该peer就向与它连接着的其它peer宣布它拥有该文件块。

对于每个连接连接来说,每一端的peer应该是4种状态之一(可用2bit来定义这4种状态):一端是interested或者not interested,则另一端是choking或者unchoking。反之亦然。Choking表示直到unchoking发生之前,都不会有任何数据传送给连接另一端的peer。关于Choke的原因和相关使用技巧将在本文的后面解释。

注:为了便于后面的理解,有必要先解释一下choke和interest。举例:文件被分为0,1,2,3,4这5个文件块。现在有两个peer建立了连接,分别叫A和B。以下的解释是站在A的立场上进行的。 
interested - 如果A没有块1,而B有块1,就是说B有A需要的块。则A的状态就是interested。 
not interested - 如果A已经有块1,而B只有块1,就是说B没有A需要的快。那么A的状态就是not interested。 
choking - 如果因为某种原因,B将在某段时间内(惩罚期)不给A发送任何数据,那么在这段时间,B的状态就是choking(choked)。 
unchoking - 惩罚期过后,choking状态解除,B的状态就变为unchoking(unchoked)。

peer之间的连接上要发生数据传输,那么peer的状态就必须是一端interested,另一端unchoked。interested/not interested状态必须时时进行更新。当某个peer需要下载某个文件块时,它应该向状态时unchoked的peer明确表示它需要的文件块,同时忽视那些状态时choked的peer。要正确地实现这个需要一些技巧(注:不是那么简单),但是一旦正确地实现了,就可以让peer知道哪些与它相连的状态是unchoked的peer会立刻开始给它传送它需要的数据。

连接刚建立时,默认的状态是自己是not interested,对方是choked。

当数据被传输时,BT客户端需要维持在同一时间内始终有多个对不同文件块的request在排队,以保证能够取得最好的TCP(网络)性能(这被称作pipelining:管道排队)。另一方面,排队的request不应该立刻被写入TCP的缓冲区里,该request队列应该由BT客户端在自己的内存中保存,而不是保存到应该用程序级别的网络缓冲区里(注:也就是说应该BT客户端自己维护这个队列,而不是交给TCP协议去维护)。这样做事为了当choke发生时,这些未发送同时又已经没必要发送的request可以及时被抛弃掉。

peer wire协议由握手消息开始,接着便是没有结束标记的带有长度前缀的消息(注:”length-prefixed messages”应该就是指按bencoded方式编码过的字串。而”never-ending”应该是说如果需要结束,就直接断开连接)。握手以”19”这个字串开始,后面紧接着是”BitTorrent protocol”这个字串。开头的第一个字串是定长的(”19”这个字串的长度为2),这样做希望其它新的协议也遵守这一规则以便新协议在连接后的一开始就能够和当前协议区分开。(注:”BitTorrent protocol”这个字串的长度就是19,所以程序处理上应该首先固定读取2个长度的字符串,然后将其转换为数字,接着读取该数字表示的长度的字符串,这个字符串就是协议名称。那么通过连接后的第一个消息,就可以知道对方使用的是什么协议了。比如新的BT协议应该发送如下的消息:”22BitTorrent protocol v2”。)

所有后续发送的数字应该使用4 bytes的big-endian编码方式。(注:big-endian即高位字节在后的编码方式。比如这种编码方式下,0x0010转换为十进制时应该这样:0x0110 -> 0x1001 -> 9)

固定的消息头之后是8个字节长的保留字节,目前的实现里是全0。如果你希望利用这些保留自己进行功能扩充,请与Bram Cohen协商以确保所有的扩展是兼容的。

接着便是元信息文件里面info的SHA1哈希值,长度为20个字节。(这和发送给Tracker服务器的request里面的info_hash基本是一样的,唯一的区别是这里就是SHA1哈希值,而info_hash还需要进行escape处理)(注:因为发送Tracker服务器的request是用HTTP的GET方式传送的,所以必须进行escape处理。而这里是直接TCP连接,用的是BT自己的协议,所以不需要escape处理)。如果双方发送的值不一样,则切断连接(注:如果hash值不一样,则表示下载的不是同一个文件,也就没必要继续了,所以这里应该是要断开连接的)。有一个例外就是如果某个BT客户端希望通过一个端口下载多个文件,那么它可以先等对方发送info的hash值,接收后,如果自己的下载列表里包括该文件,那么发送给对方同样的info的hash值就可以了(注:如果没有,则等同于双方hash值不一样,可以直接断开连接)。

info的hash值传输完之后是20byte的peer id。每个下载端的peer id在向Tracker服务器发送request时被Tracker服务器记录下来,同时包含在Tracker服务器发送给BT客户端的response里面的peer id列表里。所以如果接受方回应的peer id和期待的peer id不一样,就切断连接。(注:具体解释下:开始下载时,会向Tracker服务器按照前面所述各式发送request,接着从Tracker服务器的response里,可以获得一系列[ip,peer id],那么通过ip地址和对方连接后,按照本协议,对方会发送它的peer id过来,如果它发来的peer id和从Tracker服务器上获得的不一样,就切断连接。)

以上就是握手的过程,接下来就是带有长度前缀的消息流(注:以bencoded方式编码的消息流)。长度为0的消息时用作保持连接(注:类似于心跳帧),不会做进一步的处理。心跳消息通常每2分钟发送一次(注:也就是说,当没有有用的数据传输时,超时时间大约是2分钟),但是当发送数据消息时,超时可能会发生的更快(注:叶就是说超时时间可能会短的多)。

所有的非心跳消息以一个单字节开头,该字节用来定义消息类型。 
可选的消息类型如下: 
0 - choke 
1 - unchoke 
2 - interested 
3 - not interested 
4 - have 
5 - bitfield 
6 - request 
7 - piece 
8 - cancel

‘choke’, ‘unchoke’, ‘interested’, 和 ‘not interested’没有负载(注:也就是说没有具体的消息内容,因为消息类型就已经表达了所有的内容)。

‘bitfield’必须是握手之后的第一个消息,如果需要发送的话(注:因为该消息是可选的)。它的内容表明了发送方peer已经拥有了哪些文件块:拥有的文件块的序号所对应的bit设置为1,没有的设置为0。如果没有任何文件块,则可以跳过该消息(注:也就是不发送)’。bitfield’的第一个字节从高位到低位依次代表文件块0到7,第二个字节代表8到15,其余依次类推。结尾多余的bit设置为0。(注:举例来说如果一个文件被分为20块,某个peer开始下载时已经有了第1,4,7,18块,那么它发送的’bitfield’消息的内容就应该是010010010000000000100000)。

‘have’消息的内容是一个数字:刚刚下载完成并且正确的通过hash值验证的文件块的序号(注:该消息用于通知连接那一头的peer)。

‘request’消的内容包含一个序号,开始位置和长度。位置和长度的单位是字节。长度通常是2的幂,除非因为到达文件末尾。当前的实现使用的值是2的15次幂(注:即32KB。这是最早的值,现行版本的BT用的值请参考文章开头注明的另外一遍更详细的文档),如果对方peer要求的值大于2的17次幂(注:即128KB)则断开连接。 
(注:文件块并不是在一次消息中完全被传输,实际上文件块又被细分为更小的块,为了说明方便,这里称之为cell。所以通常下载完一个文件块需要多个request消息,每个request消息用来要求对方传输某个指定的cell。request消息中的序号知名是哪个文件块,开始位置可以理解为cell的序号,长度是固定值,可以理解为cell的大小)。

‘cancel’ 消息的内容和request消息的内容一样。’cancel’ 消息通常只在文件下载到最后的时候才有可能被发送,这一时期消息发送/处理方式被称为”游戏结束模式”。当文件下载将近尾声时,通常都有这样的趋势:最后的少量数据会从某个单通道的modem线路上获得(注:”off a single hosed modem line”不知道如何翻译好),并且速度会非常的慢。所以为了尽快取得最后的这些数据块,一旦要求这些数据块的requests准备好了,就会把这些requests发给所有正在向我发送数据的peer(要求他们发送我需要的数据块)。为了避免数据重发而导致的效率低下(注:这里的数据重发指的是不同的peer都会向我发送相同的数据),一旦我得到了某个数据块,我就向其他所有peer发送cancel消息,告诉它们我已经得到了某个数据块,不需要再向我发送了。

‘piece’消息的内容包含序号,开始和具体的数据。虽然消息的内容里没有明确说明,但该消息实际上是和request消息相关的(注:可以理解为该消息就是request消息的response消息,要来传输request消息中要求的文件数据)。需要注意的是有时候会收到非期望的’piece’消息,比如choke和unchoke这两个消息交替发生的太快,或者消息传输的太慢。 
文件块的下载顺序通常是随机的,这样可以避免某个peer拥有的文件块是和它相连的peer的超集或者子集,从而使下载合理并有效。

Choking之所以存在有几个理由:首先是因为向许多连接同时发送数据的时候,TCP的堵塞控制做的很差(所以BT客户端需要自己做Choking,也就是堵塞,目的说白了就是要提高下载效率);另外choking也让每个peer采用一种轮回的算法以保证它们能够达到一致的下载速度(注:其实就是通过choking控制,避免和自己相连的某些peer下载速度快,而其它peer则一直很慢,简而言之,就是为了公平)。

下面描述的choking算法是当前已经应用的一个。很重要的一点是:所有新的算法既要能在全部由新算法构成的BT网络里工作,也要能在大多数BT客户端使用的是老算法的网络里工作。

一个好的算法应该达到以下几个标准。它应当能够限制一定数量的上传连接以获得好的TCP性能。它应该避免频繁的在choking和unchoking之间切换,这被叫做”纤维性颤动”。它应当报答那些让它下载的peer(注:也就是不要choking下载源)。最后,它应当时不时地尝试那些未曾unchoking过的peer(注:因为初始状态是choking。),以便能够找到比当前已经连接的效果更好的peer,这被称作乐观地unchoking。

当前实际应用的算法通过延迟10秒去改变某个peer的choking状态,以避免”纤维性颤动”。通过unchoking四个下载速度最好,并且是自己interested的peer,来报答这些peer和限制上传连接。同时,上传速度好,但不是interested的peer会被unchoking,但是如果它们变成interested状态,那么上传速度最差的peer会被choking。如果自己拥有完整的文件(注:自己是种子),那么就用上传速度来决定谁应该被unchoke。

最佳畅通是指:一直都会有一个peer被unchoking而不管它的上传速度(如果它是自己interested,那么就作为4个被允许的下载者之一)。最佳畅通每30秒循环一次。为了让他们有机会下载一个完整的文件块(注:指某些最佳畅通的peer下载了某个文件块的一部分之后被choking了,为了能让他们下载完这个文件块),下次和它的建立连接的可能性将会是循环中其它普通的畅通的三倍(注:即30秒后,要再次进行最佳畅通时,该peer被选中的可能性将会3倍于其它peer。如果这样的peer多于一个,那么它们之间的竞争则应该是平等的)。
 

相关推荐
©️2020 CSDN 皮肤主题: Age of Ai 设计师:meimeiellie 返回首页