Peer wire protocol (TCP)
概述
peer(端)协议使片(piece)的交换变得容易,片的描述请参考元信息文件。
注意:原来的规范在描述peer协议时,也使用术语piece“(片)”,但是这不同于元信息文件里面的术语“piece(片)”,由于这个原因,在本规范中,将使用术语“块(block)”来描述peers(端)之间交换的数据。
一个客户端(client)必须维持其与每一个远程peer(端)连接的状态信息:
l choked: 远程peer(端)是否已经choke本客户端。当一个peer(端) choke本客户端后,它是在通知本客户端,除非它unchoke本客户端,否则它不会应答该客户端所发出的任何请求。本客户端也不应该试图向远程peer发送数据请求,并且应该认为所有没有应答的请求已经被远程peer丢弃。
l interested: 远程peer(端)是否对本客户端提供的数据感兴趣。这是远程peer在通知本客户端,当本客户端unchoke他们时,远程客户端将开始请求块(block)。
注意这也意味着本客户端需要记录它是否对远程peer(端)感兴趣,以及它是否choke/unchoke远程peer。因此真正的列表看起来像这样:
l am_choking: 本客户端正在choke远程peer。
l am_interested: 本客户端对远程peer感兴趣。
l peer_choking: 远程peer正choke本客户端。
l 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该客户端后,该客户端是否开始下载(反之亦然)。
数据类型
如果没有用其他的方法指定,在peer wire协议中的所有整数都会编码为4个字节的大端(big-endian)值。这也包括在握手之后,所有报文(Message)的长度前缀。
报文流(Message flow)
(译者注:因为ICMP-Internet控制报文协议中的Message翻译成报文,同时IP/TCP层中传输的数据都翻译为数据报,应用层传输的数据都翻译成报文,因此在这里Message翻译成报文)
peer wire协议由一个初始的握手组成。握手之后,peers通过以长度为前缀消息的交换进行通信。长度前缀就是上面描述的整数。
握手(HandShake)
握手是一个必需的报文,并且必须是客户端发送的第一个报文。该握手报文的长度是(49+len(pstr))字节。
握手:handshake: <pstrlen><pstr><reserved><info_hash><peer_id>
l pstrlen: <pstr>的字符串长度,单个字节。
l pstr: 协议的标识符,字符串类型。
l reserved: 8个保留字节。当前的所有实现都使用全0.这些字节里面的每一个字节都可以用来改变协议的行为。来自Bram的邮件建议应该首先使用后面的位,以便可以使用前面的位来改变后面位的意义。
l info_hash: 元信息文件中info键(key)对应值的20字节SHA1哈希。这个info_hash和在tracker请求中info_hash是同一个。
l 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
peer_id长20个字节。至于怎么将客户端和客户端版本信息编码成peer_id,现在主要有两种惯例:Azureus风格和Shadow风格。
Azureus风格使用如下编码方式:’-’, 紧接着是2个字符的client id,再接着是4个数字的版本号,’-’,后面跟着随机数。
例如:'-AZ2060-'...
使用这种编码风格的知名客户端是:
l 'AG' - Ares
l 'A~' - Ares
l 'AR' - Arctic
l 'AT' - Artemis
l 'AX' - BitPump
l 'AZ' - Azureus
l 'BB' - BitBuddy
l 'BC' - BitComet
l 'BF' - Bitflu
l 'BG' - BTG (uses Rasterbar libtorrent)
l 'BP' - BitTorrent Pro (Azureus + spyware)
l 'BR' - BitRocket
l 'BS' - BTSlave
l 'BW' - BitWombat
l 'BX' - ~Bittorrent X
l 'CD' - Enhanced CTorrent
l 'CT' - CTorrent
l 'DE' - DelugeTorrent
l 'DP' - Propagate Data Client
l 'EB' - EBit
l 'ES' - electric sheep
l 'FC' - FileCroc
l 'FT' - FoxTorrent
l 'GS' - GSTorrent
l 'HL' - Halite
l 'HN' - Hydranode
l 'KG' - KGet
l 'KT' - KTorrent
l 'LC' - LeechCraft
l 'LH' - LH-ABC
l 'LP' - Lphant
l 'LT' - libtorrent
l 'lt' - libTorrent
l 'LW' - LimeWire
l 'MO' - MonoTorrent
l 'MP' - MooPolice
l 'MR' - Miro
l 'MT' - MoonlightTorrent
l 'NX' - Net Transport
l 'OT' - OmegaTorrent
l 'PD' - Pando
l 'qB' - qBittorrent
l 'QD' - QQDownload
l 'QT' - Qt 4 Torrent example
l 'RT' - Retriever
l 'RZ' - RezTorrent
l 'S~' - Shareaza alpha/beta
l 'SB' - ~Swiftbit
l 'SS' - SwarmScope
l 'ST' - SymTorrent
l 'st' - sharktorrent
l 'SZ' - Shareaza
l 'TN' - TorrentDotNET
l 'TR' - Transmission
l 'TS' - Torrentstorm
l 'TT' - TuoTu
l 'UL' - uLeecher!
l 'UM' - µTorrent for Mac
l 'UT' - µTorrent
l 'VG' - Vagaa
l 'WT' - BitLet
l 'WY' - FireTorrent
l 'XL' - Xunlei
l 'XT' - XanTorrent
l 'XX' - Xtorrent
l 'ZT' - ZipTorrent
另外还需要识别的客户端有:
l 'BD' (例如: -BD0300-)
l 'NP' (例如: -NP0201-)
l 'SD' (例如: -SD0100-)
l 'wF' (例如: -wF2200-)
l 'hk' (例如: -hk0010-) 中国IP地址,IP address, unrequestedly sends info dict in message 0xA, reconnects immediately after being disconnected, reserved bytes = 01,01,01,01,00,00,02,01
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-----‘...
使用这种编码风格的知名客户端是:
l 'A' - ABC
l 'O' - Osprey Permaseed
l 'Q' - BTQueue
l 'R' - Tribler
l 'S' - Shadow's client
l 'T' - BitTornado
l 'U' - UPnP NAT Bit Torrent
Bram的客户端现在使用这种风格:'M3-4-2--' or 'M4-20-8-'。
BitComet使用不同的编码风格。它的peer_id由4个ASCII字符’exbc’组成,接着是2个字节的x和y,最后是随机字符。版本号中的x在小数点前面,y是版本号后的两个数字。BitLord使用相同的方案,但是在版本号后面添加’LORD’。BitComet的一个非正式补丁曾经使用’FUTB’代替’exbc’。自版本0.59开始,BitComet peer id的编码使用Azureus风格。
XBT客户端也使用其特有的风格。它的peer_id由三个大写字母’XBT’以及紧随其后的代表版本号的三个ASCII数字组成。如果客户端是debug版本,第七个字节是小写字符’d’,否则就是’-‘。接着就是’-‘,然后是随机数,大写和小写字母。例如:peer_id的开始部分为'XBT054d-'表明该客户端是版本号为0.5.4的debug版本。
Opera 8预览版和Opera 9.x发行版使用以下的peer_id方案:开始的两个字符是’OP’,后面的四个数字是开发代号。接着的字符是随机的小写十六进制数字。
MLdonkey使用如下的peer_id方案:开始的字符是’-ML’,后面跟着点式版本,然后就是一个’-’,最后跟着随机字符串。例如:'-ML2.7.2-kgjjfkd'。
Bit on Wheels使用模式'-BOWxxx-yyyyyyyyyyyy',其中y是随机的(大写字母),x依赖于版本。如果版本为1.0.6,那么xxx = AOC。
Queen Bee使用Bram的新风格:'Q1-0-0--' or 'Q1-10-0-'之后紧随着随机字节。
BitTyrant是Azureus的一个分支,在它的1.1版本,其peer id使用'AZ2500BT' + 随机字节的方式。
TorrenTopia版本1.90自称是或源自于Mainline 3.4.6。它的peer ID以'346------'开始。
BitSpirit有几种编码peer ID的方式。一种模式是读取它的peer ID然后使用开始的八个字节作为它peer ID的基础来重新连接。它的实际ID使用'\0\3BS'(c 标记法)作为版本3.x的前四个字节,使用'\0\2BS'作为版本2.x的前四个字节。所有方式都使用'UDP0'作为结尾。
Rufus使用它的十进制ASCII版本值作为开始的两个字节。第三个和第四个字节是'RS'。紧随其后的是用户的昵称和一些随机字节。
C3 Torrent的peer ID以’-G3’开始,然后追加多达9个表示用户昵称的字符。
FlashGet使用Azureus风格,但是前面字符是’FG’,没有’-’。版本 1.82.1002 仍然使用版本数字 '0180'。
BT Next Evolution源自于BitTornado,但是试着模仿Azureus风格。结果是它的peer ID以’-NE’开始,接着是四个数字的版本号,最后就是以shadow peer id风格描述客户端类型的三个字符。
AllPeers takes the sha1 hash of a user dependent string(这个不好翻译,待译),使用"AP" + version string + "-"代替开始的一些字符。
Qvod的id以四个字母"QVOD"开始,接着是4个十进制数字的开发代号(目前是” 0054”)。最后的12个字符是随机的大写十六进制数字。中国有一个修改版,该版本以随机字节代替前四个字符。
许多客户端全部使用随机数或者随机数后面跟12个全0(像Bram客户端的老版本)。
报文(Messages)
接下来协议的所有报文采用如下的结构:<length prefix><message ID><payload>。length prefix(长度前缀)是一个4字节的大端(big-endian)值。message ID是单个十进制值。playload与消息相关。
l keep-alive: <len=0000>
keep-alive消息是一个0字节的消息,将length prefix设置成0。没有message ID和payload。如果peers在一个固定时间段内没有收到任何报文(keep-alive或其他任何报文),那么peers应该关掉这个连接,因此如果在一个给定的时间内没有发出任何命令的话,peers必须发送一个keep-alive报文保持这个连接激活。通常情况下,这个时间是2分钟。
l choke: <len=0001><id=0>
choke报文长度固定,并且没有payload。
l unchoke: <len=0001><id=1>
unchoke报文长度固定,并且没有payload。
l interested: <len=0001><id=2>
interested报文长度固定,并且没有payload。
l not interested: <len=0001><id=3>
not interested报文长度固定,并且没有payload。
l 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(片)。Due to this attempting to model peers using this information is a bad idea.
l 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有任一冗余位集,它应该丢弃这个连接。
l request: <len=0013><id=6><index><begin><length>
request报文长度固定,用于请求一个块(block)。payload包含如下信息:
n index: 整数,指定从零开始的piece索引。
n begin: 整数,指定piece中从零开始的字节偏移。
n length: 整数,指定请求的长度。
l piece: <len=0009+X><id=7><index><begin><block>
piece报文长度可变,其中x是块的长度。payload包含如下信息:
n index: 整数,指定从零开始的piece索引。
n begin: 整数,指定piece中从零开始的字节偏移。
n block: 数据块,它是由索引指定的piece的子集。
l cancel: <len=0013><id<=8><index><begin><length>
cancel报文长度固定,用于取消块请求。playload与request报文的playload相同。一般情况下用于结束下载。
l port: <len=0003><id=9><listen-port>
port报文由新版本的Mainline发送,新版本Mainline实现了一个DHT tracker。该监听端口是peer的DHT节点正在监听的端口。这个peer应该插入本地路由表(如果支持DHT tracker的话)。