# 深入理解BitTorrent协议

大部分内容直接翻译自http://bittorrent.org/beps/bep_0003.html,机翻后校对了一下。原理说的很详细了。这里做一个总结。

# BitTorrent协议

BitTorrent 是一种用于分发文件的协议。它通过 URL 识别内容,旨在与 Web 无缝集成。与普通 HTTP 相比,它的优势在于,当同一文件的多个下载同时发生时,下载器会相互上传,使得文件源可以支持非常大量的下载器,而其负载仅适度增加。

# BitTorrent协议的元素

  • 普通的网络服务器
  • 静态“元信息”文件(metainfo)
  • BitTorrent 跟踪器(tracker)
  • 一个“原始”下载器
  • 最终用户网络浏览器
  • 最终用户下载者

# 服务器启动流程

  1. 开始运行跟踪器(或者,更有可能已经运行了一个)。
  2. 开始运行一个普通的 Web 服务器,例如 apache,或者已经有一个。
  3. 将扩展名 .torrent 与他们 Web 服务器上的 mimetype application/x-bittorrent 相关联(或者已经这样做了)。
  4. 使用要提供的完整文件和跟踪器的 URL 生成元信息 (.torrent) 文件。
  5. 将元信息文件放在 Web 服务器上。
  6. 从其他网页链接到元信息 (.torrent) 文件。
  7. 启动一个已经有完整文件('origin')的下载器。

# 客户端下载流程

  1. 安装 BitTorrent(或已经安装)。
  2. 浏览网页。
  3. 单击指向 .torrent 文件的链接。
  4. 选择在本地保存文件的位置,或选择部分下载以恢复。
  5. 等待下载完成。
  6. 告诉下载器退出(它会一直上传,直到发生这种情况)。

# bencoding

  • 字符串是以长度为前缀的以十为基数,后跟一个冒号和字符串。例如 4:spam 对应于“垃圾邮件”。
  • 整数由“i”表示,后跟以 10 为底的数字,后跟“e”。例如,i3e 对应于 3,而 i-3e 对应于 -3。整数没有大小限制。 i-0e 无效。所有带前导零的编码,例如 i03e,都是无效的,除了 i0e,它当然对应于 0。
  • 列表被编码为“l”,后跟其元素(也被编码),后跟“e”。例如 l4:spam4:eggse 对应于 ['spam', 'eggs']。
  • 字典被编码为一个“d”,后跟一个交替键列表和它们的对应值,后跟一个“e”。例如,d3:cow3:moo4:spam4:eggse 对应于 {'cow': 'moo', 'spam': 'eggs'} 和 d4:spaml1:a1:bee 对应于 {'spam': ['a' , 'b']}。键必须是字符串并按排序顺序显示(按原始字符串排序,而不是字母数字)。

# 元数据文件(种子文件)metainfo files(.torrent)

元信息文件(也称为 .torrent 文件)是具有以下键的编码字典:

  • announce: 跟踪器的 URL。

  • info: 这映射到一个字典,键如下所述。

name key映射到 UTF-8 编码的字符串,这是保存文件(或目录)的建议名称。这纯粹是建议性的。

片段(pieces)长度映射到文件被分成的每个片段中的字节数。为了传输的目的,文件被分成固定大小的片段,除了可能被截断的最后一个片段之外,这些片段的长度都相同。片段长度几乎总是 2 的幂,最常见的是 2^18 = 256 K(3.2 版之前的 BitTorrent 使用 2^20 = 1 M 作为默认值)。 片段映射到一个长度为 20 的倍数的字符串。它将被细分为长度为 20 的字符串,每个字符串都是该片段在相应索引处的 SHA1 哈希。

还有一个key length或一组key files,但不是两者都有或两者都没有。如果存在key length,则下载代表单个文件,否则代表目录结构中的一组文件。

在单个文件的情况下,key length映射到文件的长度(以字节为单位)。

对于其他keys,多文件情况被视为只有一个文件,此文件含义为将文件按照它们在文件列表中出现的顺序连接起来。文件列表是文件映射到的值,并且是包含以下键的字典列表:

length - 文件的长度,以字节为单位。

path - 与子目录名称相对应的 UTF-8 编码字符串列表,最后一个是实际文件名(零长度列表是错误情况)。

在单文件情况下,名称键是文件名,在多文件情况下,它是目录名。

.torrent 文件中包含文本的所有字符串都必须采用 UTF-8 编码。

# 追踪器trackers

Tracker是一个注册服务,用来协调BT协议的文件分发。当客户端开始执行下载任务时,通过Tracker GET请求获取到当前需要下载的同一资源的peer节点信息。

Tracker GET 请求具有以下键:

# info_hash

元信息文件中信息值的编码形式的 20 字节 sha1 散列。这个值几乎肯定会被转义。

请注意,这是元信息文件的子字符串。 info-hash 必须是在 .torrent 文件中找到的编码形式的hash,这与对元信息文件进行 bdecoding 相同,当且仅当 bdecoder 完全验证输入时(例如密钥排序),提取信息字典并对其进行编码,没有前导零)。相反,这意味着客户端必须要么拒绝无效的元信息文件,要么直接提取子字符串。他们不得对无效数据执行解码-编码往返。

# peer_id

此下载器使用长度为 20 的字符串作为其 id。每个下载器在新下载开始时随机生成自己的 id。这个值也几乎肯定必须被转义。

# ip

一个可选参数,给出此对等点所在的 IP(或 dns 名称)。一般因为GET请求本身通过tcp是可以获取IP地址的,如果出现通常是因为Proxy地址和请求地址隔离开时,可以返回tracker机器上的真实ip。

# port

此对等方正在侦听的端口号。常见的行为是下载器尝试侦听端口 6881,如果该端口被占用,则尝试 6882,然后是 6883,依此类推,然后在 6889 之后放弃。

# uploaded

到目前为止上传的总量,以十进制 ascii 编码。

# downloaded

到目前为止下载的总量,以十进制 ascii 编码。

# left

此对等点仍需下载的字节数,以十进制 ascii 编码。请注意,这不能从下载的文件长度和文件长度中计算出来,因为可能会有数据恢复操作,并且有可能一些下载的数据未能通过完整性检查,必须重新下载。

# event

这是一个可选键,映射到已启动、已完成或已停止(或为空,与不存在相同)。如果不存在,这是定期发布的公告之一。在第一次开始下载时发送一条 started 的通知,在下载完成时发送一条 completed。如果文件在启动时已经完成,则不会发送 completed。下载者在停止下载时使用stopped 发送通知。

Tracker response是经过bencoded编码的字典。如果跟踪器响应有key失败原因,那么它会映射到一个可读的字符串,解释查询失败的原因,并且不需要其他key。否则,它必须有两个键:interval,映射到下载器在常规重新请求之间应该等待的秒数,以及对等点。 peers映射到peer对应的字典列表,每个字典都包含键peer id、ip和port,它们分别映射到peer的自选ID、IP地址或dns名称作为字符串和端口号。请注意,如果事件发生或他们需要更多对等点,下载者可能会在非预定时间重新请求。

通过 UDP 的tracker协议也很常见。

# peer protocol(对等节点协议)

BitTorrent 的对等协议通过 TCP 或 uTP 运行。

对等连接是对称的。在两个方向上发送的消息看起来是一样的,并且数据可以在任一方向上流动。

对等协议按照元信息文件中描述的索引引用文件的片段,从零开始。当一个对等点完成下载一个片段并检查哈希是否匹配时,它会向所有对等点宣布它拥有该片段。

连接在两端包含两种状态:阻塞与否(choked),以及是否感兴趣(interested)。 Choking 是一个通知,在发生 unchoking 之前不会发送任何数据。

每当一侧感兴趣并且另一侧没有阻塞时,就会发生数据传输。兴趣状态必须始终保持更新 - 每当下载者没有东西,他们会向当前在未阻塞的情况下的对等方请求数据,尽管被阻塞,他们必须表示不感兴趣。正确实现这一点很棘手,但可以让下载者知道哪些对等点在未阻塞的情况下将立即开始下载。

连接开始的状态为阻塞并且不感兴趣

当数据正在传输时,下载者应该同时保持多个片段请求排队以获得良好的 TCP 性能(这称为“流水线”。)另一方面,不能立即写入 TCP 缓冲区的请求应该在内存中排队而不是保存在应用程序级别的网络缓冲区中,因此当发生阻塞时它们都可以被丢弃。

对等线协议由一个握手组成,然后是一个永无止境的以长度为前缀的消息流。握手以字符 19(十进制)开头,后跟字符串“BitTorrent 协议”。前导字符是一个长度前缀,放置在那里是希望其他新协议可以做同样的事情,因此可以很容易地相互区分。

协议中发送的所有后续整数都被编码为 4 个字节的 big-endian。

在固定头之后是八个保留字节,在所有当前实现中都为零。如果您希望使用这些字节扩展协议,请与 Bram Cohen 协调以确保所有扩展都兼容地完成。

接下来是元信息文件中信息值的编码形式的 20 字节 sha1 散列。 (这与作为 info_hash 向tracker宣布的值相同,只是在这里它是原始的,而不是在这里引用)。如果双方不发送相同的值,他们将切断连接。一个可能的例外是,如果下载者想通过单个端口进行多次下载,他们可能会等待传入的连接首先给出下载哈希,如果它在他们的列表中,则使用相同的响应。

在下载散列之后是 20 字节的对等 id,它在跟踪器请求中报告并包含在跟踪器响应的对等列表中。如果接收方的对等 id 与发起方期望的不匹配,它将切断连接。

这就是握手,接下来是长度前缀和消息的交替流。长度为零的消息是keepalive,并被忽略。 Keepalive 通常每两分钟发送一次,但请注意,当预期有数据时,超时可以更快地完成。

# peer messages(对等协议信息)

所有non-keepalive信息都以一个字节开始,该字节给出了它们的类型。

可能的值是:

  1. 阻塞(choke)- 0
  2. 非阻塞(unchoke)- 1
  3. 感兴趣(interested)- 2
  4. 不感兴趣(not interested)- 3
  5. 有(have)- 4
  6. 位域(bitfield)- 5
  7. 请求(request)- 6
  8. 片(piece)- 7
  9. 取消(cancel)- 8

'choke'、'unchoke'、'interested' 和 'notinterested' 没有负载。

'bitfield' 仅作为第一条消息发送。它的负载是一个位域,下载器发送的每个索引都设置为 1,其余的设置为 0。还没有任何东西的下载者可能会跳过“位域”消息。位域的第一个字节分别对应于从高位到低位的索引 0 - 7。下一个 8-15 等。最后的备用位设置为零。

“have”消息的有效负载是一个数字,即下载器刚刚完成并检查其哈希值的索引。

“request”消息包含索引、开始和长度。最后两个是字节偏移量。长度通常是 2 的幂,除非它在文件末尾被截断。所有当前的实现都使用 2^14 (16 kiB),并关闭请求大于该数量的连接。

“cancel”消息与请求消息具有相同的有效负载。它们通常仅在下载结束时发送,即所谓的“残局模式”。当下载几乎完成时,最后几部分往往会从一条软管调制解调器线路上全部下载下来,这需要很长时间。为了确保最后几部分快速进入,一旦对给定下载器的所有部分的请求当前尚未挂起,它就会向正在下载的每个人发送对所有部分的请求。为了避免这种情况变得非常低效,它会在每次到达时向其他所有人发送取消消息。

'piece' 消息包含索引、开始和片段。请注意,它们与请求消息隐式相关。如果 choke 和 uncoke 消息连续快速发送和/或传输速度非常缓慢,则可能会有意外的消息到达。

下载器通常以随机顺序下载片段,这可以很好地防止它们拥有任何对等体片段的严格子集或超集。

Choke的发生有几个原因。当一次通过多个连接发送时,TCP 拥塞控制的行为非常糟糕。此外,choking 让每个对等方使用以牙还牙(tit-for-tat-ish)的算法来确保他们获得一致的下载速率。

下面描述的阻塞算法是当前部署的算法。非常重要的是,所有新算法在完全由它们自己组成的网络和主要由这个组成的网络中都能很好地工作。

一个好的阻塞算法应该满足几个标准。它应该限制同时上传的数量以获得良好的 TCP 性能。它应该避免快速choke和unchoke,称为“纤颤(fibrillation)”。它应该回报允许它下载的peer对等方。最后,它应该不时尝试未使用的连接,以确定它们是否可能比当前使用的连接更好,这被称为乐观解除阻塞(optimistic unchoking)。

当前部署的choke算法通过每十秒更改一次阻塞者来避免颤动。它通过unchoke它具有最佳下载率和感兴趣的四个对等方来实现互惠和上传数量上限。具有更好上传率但不感兴趣的对等节点不会被阻塞,如果他们感兴趣,最差的上传者会被阻塞。如果下载器有一个完整的文件,它会使用其上传速率而不是其下载速率来决定谁来解锁。

对于乐观解除阻塞,在任何时候都有一个未阻塞的对等点,无论其上传速率如何(如果感兴趣,它会被视为四个允许的下载器之一。)乐观解除阻塞的对等点每 30 秒轮换一次。为了给他们一个不错的机会来上传完整的作品,新连接开始的可能性是当前乐观解锁的可能性是轮换中其他任何地方的三倍。

# 参考

  • http://bittorrent.org/beps/bep_0003.html