全部博文(24)
分类: 系统运维
2009-06-26 14:44:19
此文档旨在由 BitTorrent 开发社区维护和使用。欢迎大家为它做贡献,其中的内容仅代表当前协议,但它已经被目前许多存在的客户端实现所采用。
(译者注:在本文档翻译过程中,如果遇到没有对应标准翻译的术语,一律不予翻译,例如torrent,peer,tracker等)
(译者注:peer一般译成“端”,所以p2p应该翻译成端对端,但这并没有一个标准的译法,因此在本文中不作翻译,同时译者应该将peer to peer和数据链路层的点对点协议(也缩写成p2p)区分开)
字节串按如下方式编码:<以十进制ASCII编码的串长度>:<串数据>
注意:字节串编码没有开始和结束分隔符。
例:4:spam表示字节串“spam”
整数按如下方式编码:i<以十进制ASCII编码的整数>e
开始的“i”与结尾的“e”分别是开始和结束分隔符。可以使用如“i-3e”之类的负数。但是你不能把“0”放到数字的前面,如“i04e”。另外,“i0e”是有效的。
lists按如下方式编码:le
开始的“l”(l是小写的L,而不是大写的i)与结尾的“e”分别是开始和结束分隔符。lists可以包含任何B编码的类型,包括整数、串、dictionaries和其他的lists。
dictionaries按如下方式编码:de
开始的“d”与结尾的“e”分别是开始和结束分隔符。注意键(key)必须被B编码为串。值可以是任何B编码的类型,包括整数、串、lists和其他的dictionaries。键(key)必须是串,并且以排序的顺序出现(以原始串排列,而不是以字母数字顺序)。串采用二进制比较方式,而不是特定于某种文化的自然比较(即既不是按照中文的比较方式,也不是按照英文的排序方式)。
例2:d4:spaml1:a1:bee 表示dictionary { "spam" => ["a", "b"] }
例3:d9:publisher3:bob17:publisher-webpage15:e表示dictionary { "publisher" => "bob", "publisher-webpage" => "", "publisher.location" => "home" }
(译者注:对于string和interger,目前已经存在官方的翻译,但list和dictionary并不存在一个统一的译法)
元信息文件(就是平常咱们经常接触到的以.torrent为后缀的文件)的内容是一个B编码的dictionary,包含下面列出的键(key),其中字符串类型的值均以UTF-8编码。
encoding:(可选),由上文可知,.torrent元文件中包含一个info dictionary,当该dictionary过大时,就需要对其分片(piece),该编码就是用来生成分片的。(字符串类型)
这一节主要是讲述两种模式(单文件模式和多文件)所共有的键(key)
private: (可选),这个键(key)所对应值是整数类型,如果设置为1。客户端必须广播自己的存在,然后通过在元信息文件中显式描述的trackers得到其他的 peers,如果设置为0或者不设置,则表明客户端可以通过其他的方式来得到其他的peers,例如:PEX peer exchange技术,DHT技术等。“private”可以解释为没有外部的peer源(如果客户端不提供PEX peer exchange技术、DHT技术等,那么BitTorrent客户端必须通过tracker来得到其他的peers)。(整数类型)
对应于单文件模式,info dictionary包含如下的结构:
md5sum:(可选),相当于文件MD5和的32个字符的16进制串,BitTorrent根本就不使用这个键(key),但是有些程序为了更大的兼容性而包含它。(字符串类型)
对应于多文件模式,info dictionary包含如下的结构:
path:包含单个或多个元素的list,这些元素合成在一起表示文件路径和文件名。list中的每一个元素对应于一个目录名或者文件名(当是最后一个元素时对应文件名). 例如:文件"dir1/dir2/file.ext"由三个字符串元素组成:"dir1"、"dir2"和"file.ext"。这三个元素会被编码成B 编码的字符串list:l4:dir14:dir28:file.exte
注意:URL中的所有二进制数据(特别是info_hash和peer_id)必须适当地进行转义。这意味着不在集合{0-9, a-z, A-Z, ‘.’, ‘-’, ‘_’, ‘~’}中的字节必须以’%nn’方式编码,其中nn是这个字节的十六进制值。
对于一个20字节的散列值 “\x12\x34\x56\x78\x9a\xbc\xde\xf1\x23\x45\x67\x89\xab\xcd\xef\
x12\x34\x56\x78\x9a”,其正确的编码形式是“%124Vx%9A%BC%DE%F1%23Eg%89%AB%CD%EF
%124Vx%9A“。
从客户端发送到Tracker的请求包含如下参数:
started:第一个发送到Tracker的请求其event值必须是该值。
stopped:如果正常关闭客户端,必须发送改事件到Tracker。
completed:如果下载完毕,必须发送改事件到Tracker。如果客户端启动之前,已经下载完成的话,则没有必要发送该事件。Tracker仅仅基于该事件增加已经完成的下载数。
Tracker以”text/plain”文本响应客户端的请求,这个响应文本由B编码的dictionary组成,而这个dictionary则包含如下的键(key):
peer id:peer自己选择的用来标识自己的ID,上文在描述Tracker请求时已经说明。 (字符串类型)
ip:peer的IP地址,可以是Ipv6/Ipv4/DNS name。(字符串类型)
port:peer的端口号。(整数类型)
综上所述,默认情况下peers列表的长度是50,如果torrent中的peers比较少,那么这个列表长度会更短。否则如果torrent中的peers比较多,那么tracker就得从这些peers中随机选择一些放入响应中。当响应请求时,Tracker需要使用一个智能的机制来进行peer选择。例如:向做种者报告种子就得避免(意思说如果没有好的机制,就有可能出现这种情况)。
如果有一个事件发生(即stopped或者completed)或者客户端需要获得更多的peers时,客户端发送请求的频率肯定高于指定的间隔。尽管如此,不断地向Tracker发送请求以获得更多的peers绝对不是一个好主意。如果一个客户端想在其响应中包含一个更长的peer列表,那么它应该通过指定numwant参数来达到目的。
BitTorrent实现者注意:30个peers已经是一个很大的数目了,官方的客户端版本3在peers少于30的情况,会积极地向Tracker建立连接已获得更多的peers,但是如果客户端已经有55个peers的情况下,客户端将拒绝和Tracker建立连接。这个值的选择对于性能非常重要。当一个片(piece)已经下载完成后,客户端需要将HAVE消息(参考下文)发送给大多数活跃的peers。结果是广播通信的代价与peers数量成正比。这个数高于25之后,新加入的peers不太可能提升下载速度。强烈建议UI设计者使这个数模糊和很难修改,因为这样做没有任何作用。
按照惯例:大部分Tracker支持另一种形式的请求,这种方式查询Tracker正在管理的给定torrent(或者所有的torrent)。这种方式通常被称为”scrape page”,因为这种方式会自动进行另一个单调乏味的流程:”screen scraping” Tracker的统计页。
类似于上面描述的URL,scrape URL也是一个HTTP GET方法。尽管如此,它们的Base URL是不相同的。想要得到scrape URL,请使用如下步骤:从announce URL开始,找到该announce URL中的最后一个’/’,如果该’/’之后的内容不是’announce’,那么说明这个Tracker不支持scrape。如果这个Tracker支持scrape,那么使用’scrape’代替’announce’就可以得到scrape页。
例:(announce URL -> scrape URL)
~ -> ~
~ -> ~
~.php -> ~.php
~ -> (scrape not supported)
~?x2%0644 -> ~?x2%0644
~?x=2/4 -> (scrape not supported)
~%064announce -> (scrape not supported)
可用info_hash(上文描述的一个20字节值)作为scrape URL的补充。这样就限制了Tracker发送某个特定(info_hash限定)的torrent的数据。否则tracker正在管理的所有torrent的统计数据将会返回。强烈建议软件编写者尽可能使用info_hash参数,这样就能够减少Tracker的负载和带宽。
你也可以指定多个info_hash参数给Tracker (得支持多个info_hash参数)。这不是官方规范的一部分,但是已经成为了实际标准,例如:
.php?info_hash=aaaaaaaaaaaaaaaaaaaa&info_hash=bbbbbbbbbbbbbbbbbbbb&info_hash=cccccccccccccccccccc
这个HTTP GET方法的响应是一个’text/plain’或者有时候是用gzip压缩的文本,这个文本由一个B编码的dictionary组成,这个dictionary包含如下的键(key):
complete:完成文件下载的peer数,即做种者的数量。(整数类型)
downloaded:已向tracker注册的下载完成的总次数("event=complete",即一个客户端完成了下载) 。(整数类型)
incomplete:非做种的peers数(还没有完成该文件下载的peers数),即“占他人便宜者”。 (整数类型)
name:(可选的),torrent的内部名,由.torrent文件中info键所对应值中的name指定。
注意这个响应有三层dictionary嵌套。例如:
d5:filesd20:....................d8:completei5e10:downloadedi50e10:incompletei10eeee
表示....................是一个20字节的info_hash,有5个做种者,5个正在下载者以及50个已经完成的下载。
下面的响应键是非官方的。因为它们都是非官方的,因此都是可选的。
peer(端)协议使片(piece)的交换变得容易,片的描述请参考元信息文件。
注意:原来的规范在描述peer协议时,也使用术语piece“(片)”,但是这不同于元信息文件里面的术语“piece(片)”,由于这个原因,在本规范中,将使用术语“块(block)”来描述peers(端)之间交换的数据。
一个客户端(client)必须维持其与每一个远程peer(端)连接的状态信息:
注意这也意味着本客户端需要记录它是否对远程peer(端)感兴趣,以及它是否choke/unchoke远程peer。因此真正的列表看起来像这样:
客户端连接开始时状态是choke和not interested(不感兴趣)。换句话就是:
当一个客户端对一个远程peer感兴趣并且那个远程peer没有choke这个客户端,那么这个客户端就可以从远程peer下载块(block)。当一个客户端没有choke一个peer,并且那个peer对这个客户端这个感兴趣时,这个客户端就会上传块(block)。
客户端必须不断通知它的peers,它是否对它们感兴趣,这一点是很重要的。客户端和每个端的状态信息必须保持最新,即使本客户端被choke。这允许所有的peer知道,当它们unchoke该客户端后,该客户端是否开始下载(反之亦然)。
如果没有用其他的方法指定,在peer wire协议中的所有整数都会编码为4个字节的大端(big-endian)值。这也包括在握手之后,所有报文(Message)的长度前缀。
(译者注:因为ICMP-Internet控制报文协议中的Message翻译成报文,同时IP/TCP层中传输的数据都翻译为数据报,应用层传输的数据都翻译成报文,因此在这里Message翻译成报文)
peer wire协议由一个初始的握手组成。握手之后,peers通过以长度为前缀消息的交换进行通信。长度前缀就是上面描述的整数。
握手是一个必需的报文,并且必须是客户端发送的第一个报文。该握手报文的长度是(49+len(pstr))字节。
握手:handshake:
在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。
你可以在找到关于shadow编码风格(包含关于版本字符串后的三个字符用法的习惯)的详细说明。
例如:用于Shadow 5.8.11的’S58B-----‘...
使用这种编码风格的知名客户端是:
Bram的客户端现在使用这种风格:'M3-4-2--' or 'M4-20-8-'。
使用不同的编码风格。它的peer_id由4个ASCII字符’exbc’组成,接着是2个字节的x和y,最后是随机字符。版本号中的x在小数点前面,y是版本号后的两个数字。使用相同的方案,但是在版本号后面添加’LORD’。BitComet的一个非正式补丁曾经使用’FUTB’代替’exbc’。自版本0.59开始,BitComet peer id的编码使用Azureus风格。
也使用其特有的风格。它的peer_id由三个大写字母’XBT’以及紧随其后的代表版本号的三个ASCII数字组成。如果客户端是debug版本,第七个字节是小写字符’d’,否则就是’-‘。接着就是’-‘,然后是随机数,大写和小写字母。例如:peer_id的开始部分为'XBT054d-'表明该客户端是版本号为0.5.4的debug版本。
使用以下的peer_id方案:开始的两个字符是’OP’,后面的四个数字是开发代号。接着的字符是随机的小写十六进制数字。
使用如下的peer_id方案:开始的字符是’-ML’,后面跟着点式版本,然后就是一个’-’,最后跟着随机字符串。例如:'-ML2.7.2-kgjjfkd'。
使用模式'-BOWxxx-yyyyyyyyyyyy',其中y是随机的(大写字母),x依赖于版本。如果版本为1.0.6,那么xxx = AOC。
使用Bram的新风格:'Q1-0-0--' or 'Q1-10-0-'之后紧随着随机字节。
是Azureus的一个分支,在它的1.1版本,其peer id使用'AZ2500BT' + 随机字节的方式。
版本1.90自称是或源自于Mainline 3.4.6。它的peer ID以'346------'开始。
有几种编码peer ID的方式。一种模式是读取它的peer ID然后使用开始的八个字节作为它peer ID的基础来重新连接。它的实际ID使用'\0\3BS'(c 标记法)作为版本3.x的前四个字节,使用'\0\2BS'作为版本2.x的前四个字节。所有方式都使用'UDP0'作为结尾。
使用它的十进制ASCII版本值作为开始的两个字节。第三个和第四个字节是'RS'。紧随其后的是用户的昵称和一些随机字节。
的peer ID以’-G3’开始,然后追加多达9个表示用户昵称的字符。
使用Azureus风格,但是前面字符是’FG’,没有’-’。版本 1.82.1002 仍然使用版本数字 '0180'。
源自于BitTornado,但是试着模仿Azureus风格。结果是它的peer ID以’-NE’开始,接着是四个数字的版本号,最后就是以shadow peer id风格描述客户端类型的三个字符。
takes the sha1 hash of a user dependent string(这个不好翻译,待译),使用"AP" + version string + "-"代替开始的一些字符。
的id以四个字母"QVOD"开始,接着是4个十进制数字的开发代号(目前是” 0054”)。最后的12个字符是随机的大写十六进制数字。中国有一个修改版,该版本以随机字节代替前四个字符。
许多客户端全部使用随机数或者随机数后面跟12个全0(像Bram客户端的老版本)。
接下来协议的所有报文采用如下的结构:
keep-alive消息是一个0字节的消息,将length prefix设置成0。没有message ID和payload。如果peers在一个固定时间段内没有收到任何报文(keep-alive或其他任何报文),那么peers应该关掉这个连接,因此如果在一个给定的时间内没有发出任何命令的话,peers必须发送一个keep-alive报文保持这个连接激活。通常情况下,这个时间是2分钟。
choke报文长度固定,并且没有payload。
unchoke报文长度固定,并且没有payload。
interested报文长度固定,并且没有payload。
not interested报文长度固定,并且没有payload。
have报文长度固定。payload是piece(片)的从零开始的索引,该片已经成功下载并且通过hash校验。
实现者注意:实际上,一些客户端必须严格实现该定义。因为peers不太可能下载他们已经拥有的piece(片),一个peer不应该通知另一个peer它拥有一个piece(片),如果另一个peer拥有这个piece(片)。最低限度”HAVE suppresion”会使用have报文数量减半,总的来说,大致减少25-35%的HAVE报文。同时,给一个拥有piece(片)的peer发送HAVE报文是值得的,因为这有助于决定哪个piece是稀缺的。
一个恶意的peer可能向其他的peer广播它们不可能下载的piece(片)。Due to this attempting to model peers using this information is a bad idea.
bitfield报文可能仅在握手序列发送之后,其他消息发送之前立即发送。它是可选的,如果一个客户端没有piece(片),就不需要发送该报文。
bitfield报文长度可变,其中x是bitfield的长度。payload是一个bitfield,该bitfield表示已经成功下载的piece(片)。第一个字节的高位相当于piece索引0。设置为0的位表示一个没有的piece,设置为1的位表示有效的和可用的piece。末尾的冗余位设置为0。
长度不对的bitfield将被认为是一个错误。如果客户端接收到长度不对的bitfield或者bitfield有任一冗余位集,它应该丢弃这个连接。
request报文长度固定,用于请求一个块(block)。payload包含如下信息:
根据官方规范有关主要版本3,“所有当前执行应使用 2^15(32 KB),请求数量大于 2^17(128 KB)时应断开连接。”在主要版本4中,此反应修改到了 2^14(16 KB),超过该值的用户会强迫拒绝。注意到块请求小于片断大小(>=2^18 字节),所以为下载一个完整片断需要多次请求。
由于新版本将限制定在 16 KB,尝试使用 32 KB 的块就好比用 4 发子弹来玩俄式轮盘——会遇到困难。更小的请求会导致更大的系统时间和空间开销,因为要跟踪很多请求。结果应使用所有客户端都允许的 16 KB。
请求块大小的限制执行的选择没有减少一部分清楚。在主要版本 4 中,强制使用 16 KB 的请求,许多客户端会使用该值,只有一个严格客户端组不会使用。大多数旧客户端使用 32 KB 请求,不允许明显减少可能用户的批次。同时 16 KB 是现在部分官方的限制(“部分”是因为官方协议文档没有更新),所以强制使用没有错。另外,允许更大的请求增大了可能用户的批次,除在非常低的带宽连接(小于 256 kbps)中,多个块会在一个阻塞周期内完成下载,从而强迫使用旧的限制仅会降低很少的性能。因此,推荐仅在旧的 128 KB 下才强行限制。
piece报文长度可变,其中x是块的长度。payload包含如下信息:
cancel报文长度固定,用于取消块请求。playload与request报文的playload相同。一般情况下用于结束下载。
port报文由新版本的Mainline发送,新版本Mainline实现了一个DHT tracker。该监听端口是peer的DHT节点正在监听的端口。这个peer应该插入本地路由表(如果支持DHT tracker的话)。
通常建议用户在每个连接上保持一些未完成的请求。因为从一块的下载到开始下载另一块需要一个完全的往返程(往返程在片断消息和下一个请求消息之间)。一旦与高 BDP (Bandwidth Delay Product,高延迟或带宽)相连,会降低很多性能。官方客户端未完成请求的默认值是 5。
用户注意:这是最严格的性能条款。一个 5 请求的静态队列对于具有 50 ms 延迟 5 Mbps 中的 32 KB 块是合理的。连接更大的带宽越来越常见,所以用户界面设计者被催促使其对于改变更适用。特别地,线缆调制解调器以调整通信量和增加其可能缓和部分由此导致的问题,这是众所周知的。
自动调整:调整此参数的一个合理的方法是连续测量单个连接的带宽。如果该用户增加带宽而队列不够,则尝试增加队列长度。使用相同标志如果减少队列长度没有减少带宽和延迟,可能是队列长度太大了。
(该项不是原始规范的一部分)
在 S-5.5 中的超级种子特性是一种新的做种算法,用来帮助只有有限带宽的做种者发布很大的文件,减少为了产生新的种子而上传的数据总量。
当做种者使用“超级种子模式”时,它不会作为标准种子,而是伪装成一个没有数据的普通客户端。当客户端连接时,它会通知它们自己收到一块从未发送或源很少的片断。这将促使客户端仅尝试下载那个片断。
当客户端完成下载该片断时,做种者看到它以前发送的片断在其他用户中至少有一个拥有后,才会继续发送另外的片断。在那之前,客户端下载不到做种者的其他片断,这样不会浪费做种者的带宽。
这种方法会有更高的效率,同时促使用户只下载源最少的数据,降低了多余数据的发送,限制了没有为该群传输数据的用户而发送的数据量。在这之前,做种者可能需要上传文件总大小的 1.5 到 2 倍,其他用户才可能成为种子。但是,使用超级种子模式的单个客户端发布大的文件只需上传文件大小的 1.05 倍就能成为种子。这是标准做种效率的 1.5 到 2 倍。
不推荐一般用户采用超级种子模式。虽然它有助于稀少数据的扩散,但是它限制了客户端下载片断的选择,同时限制了客户端下载自己已得到部分的片断。所以,超级种子模式只推荐原始做种者使用。
综上,“原始做种者模式”或“发布者模式”是更恰当的名称。
客户端可以随机顺序下载片断。
更好的方法是首先下载源最少的片断。客户端可以从每个其他用户保存的原始位字段来决定,通过拥有消息来更新。然后,客户端可以下载出现在其他用户位字段中频率最低的片断。
下载接近完成时,最后几块的速度有变慢的趋势。为了加速,客户端向其他所有拥有自己缺少块的用户发送请求。为防止变成无效,客户端在每个块完成后就向其他用户发送一个取消的消息。
没有已定的界限,推荐百分比或能用来作为指导的块计数。
何时进入最后阶段模式有待讨论。一些客户端以请求了所有块来进入最后阶段。其他的等到剩余块的数目少于传输中块的数目或不超过 20 来进入该阶段。保持很少的等待块(1 或 2 块)来将允许值减到最少,如果随机选择块的请求,可能会重复下载到已有的块。更多的协议说明见:
阻塞有几种原因。TCP 拥塞控制在同时发出许多连接时表现很差。同时,阻塞使每个用户使用 tit-for-tat-ish 算法来确定自己得到一致的下载速度。
下面描述的阻塞算法是现在使用的。所有新算法同时在整个包括它们的网络中工作正常,这是很重要的。
一个好的阻塞算法应有几个标准。它应为更高的 TCP 性能改进并发上传数。它应避免被叫做“原纤化作用”的快速阻塞和未阻塞。它应互换到让自己下载的用户。最后,它应偶尔测试未使用连接来发现是否比当前使用的更好,这叫做最佳畅通。
当前使用的阻塞算法通过每 10 秒钟改变被阻塞用户来避免原纤化作用。
互换和上传数目的改进是由拥有最佳上传速度和感兴趣的 4 个未阻塞用户来控制的。这将使客户端的下载速度变得最大。这 4 个用户被称为下载者,因为它们对从客户端的下载感兴趣。
与下载者相比,具有较高上传速度的用户对未阻塞不感兴趣。如果它们感兴趣,具有最低上传速度的下载者将被阻塞。如果客户端拥有完成的文件,它使用上传速度而不是下载速度来决定哪一个用户未阻塞。
对于最佳畅通,在任何时候只有一个未阻塞用户,而不管它的上传速度(如果感兴趣,它会成为 4 个允许的下载者之一)。最佳畅通的用户 30 秒循环一次。最新连接的用户有 3 次可能作为循环中当前的最佳畅通。这给它们得到一个完成块就上传的机会。
(扩展不在官方协议中)
偶尔一个 BitTorrent 用户会被其他用户冷落,它先前从那些用户中下载了数据。在这种情况下,它通常得到很低的下载速度,直到最佳畅通找到更好的用户。为了缓和此问题,当过了 1 分钟而没有从某个用户得到一个片断,BitTorrent 认为它被那个用户“冷落”了,不上传给那个用户(除了最佳畅通以外)。这会频繁导致超过 1 个的用户同时变成最佳畅通(一个例外是最佳畅通,规则如上所述),它会使波动的下载速度回复得更快。