跳转至

bep 3 BitTorrent 协议规范

bep 3 原始链接

BitTorrent 是一种分发文件的协议。它通过 URL 识别内容,旨在与网络无缝集成。 与普通 HTTP 相比,它的优势在于,当多个下载者同时下载同一文件时, 下载者会相互上传,从而使文件源能够支持大量下载者,而只需适度增加其负载。

分发 BitTorrent 文件需要以下的组件

  1. 一个普通的文件服务器
  2. 一个静态种子文件
  3. 一个 BitTorrent tracker
  4. 一个“原始”下载者
  5. 终端用户浏览器
  6. 终端用户下载器

为了分发文件,需要进行以下流程

  1. 启动一个 tracker
  2. 运行一个普通的 web 服务器,比如 apache。
  3. .torrent 扩展名跟 mime type application/x-bittorrent 进行关联。
  4. 从要分发的文件和 tracker 地址生成种子文件。
  5. 通过 web 服务器等方式分发种子文件。
  6. 以完整的文件和种子文件启动下载器,俗称做种。

为了进行下载,下载者需要如下的操作

  1. 安装支持 BitTorrent 协议的一个下载器
  2. 下载种子文件
  3. 将种子文件添加到下载器
  4. 选择下载路径,或者从之前未完成的下载恢复下载
  5. 等待下载完成
  6. 关闭客户端,或者保持客户端启动状态进行做种

bencoding,种子文件的编码格式

bencoding 包含四种数据类型

  • 字符串(二进制字符串)。编码为 长度+ : + 字符串内容。 例如 4:spam 代表 'spam'
  • 整数。 以为 i 开头,e结尾,中间为整数的十进制格式。比如 i3e3, i-3e-3, 整数没有大小限制。 不允许使用i-0e, 只有 i0 合法。 除了 0 的编码 i0e 以外不可以有前导 0i03e 不合法。
  • 列表。 以 l 开头, e 结束,中间顺序穿插 bencode 格式的元素内容。比如 ['spam', 'eggs'] 编码为 l4:spam4:eggse
  • 字典。字典是以 d 开头, e 结尾的键值对。 按照键-值的顺序编码。 {'cow': 'moo', 'spam': 'eggs'} 编码为 d3:cow3:moo4:spam4:eggse{'spam': ['a', 'b']} 编码为 d4:spaml1:a1:bee。 字典中的键必须唯一。 字典的键必须为字符串,而且必须按顺序出现。(sorted as raw strings, not alphanumerics)

种子文件

种子文件 (.torrent 文件) 是一个 bencode 编码的字典,并且包含以下的 key:

announce tracker url 地址

info 一个字典,并且需要包含以下的 key:

info dictionary

name: 一个 utf-8 编码的 bencode 字符串,建议用户保存文件或者文件夹的路径。

piece length:区块长度。为了进行传输,文件会被分为固定大小的区块。 最后一个区块除外,由于文件长度原因,最后一个区块可能会被截断。 比较常见的区块长度是二的幂(2^n),非二的幂也是允许的值。常见值有 2^18 = 256K 等。

pieces:区块哈希,长度为 20 的正整数倍。每 20 字节为对应区块的 sha1 哈希值。 比如 pieces[0:20] 为第一个哈希,对应第一个区块的 sha1 哈希值。 pieces[20:40] 为第二个哈希值,为第二个区块的 sha1 哈希。

对于单文件种子,还必须包含 length 键,表示种子锁对应的单个文件的大小。

对于多文件种子,还必须包含 files 键,files 是一个字典组成的列表,每个字典必需包含以下的 keys:

length: 当前文件的长度。

path:一个 list[str], 由 UTF-8 字符串组成的列表,最后一个元素为文件名,前面的元素为文件夹层级。

单文件种子的 name 为种子所对应的文件,多文件种子的 name 为对应文件夹的名称。

Trackers

客户端使用 GET 请求像 tracker 请求其他节点,请求必需包含以下的参数。

( 关于 HTTP 请求和 GET 请求的参数,可以查看维基百科

info_hash

20 字节长度的 sha1 哈希,对种子文件的 info 部分进行哈希生成。在 HTTP query 中需要进行转义

由于 bencode 的特,可以先解析整个种子文件,对 info 部分进行 bencode 编码, 然后再计算对应 sha1 哈希。 info_hash 只应在种子文件的 bencode 正确的情况下进行计算,如果种子文件的 bencode 编码错误,解析器必需拒绝对应的种子文件。

Tip

这里的 info_hash 不是常见的 40 字节长度的 info_hash 字符串,而是原始的二进制数据。

常见的 40 字节长度的 info_hash 实际上是原始 info_hash 的 hex 编码结果。

ubuntu-24.04-live-server-amd64 为例, 这个种子的 info_hash 的原始二进制值为 [129, 175, 7, 73, 25, 21, 65, 93, 173, 69, 248, 124, 12, 42, 229, 47, 174, 146, 192, 107]

但是由于二进制内容不一定能够对应可显示的内容,所以一般以 hex 编码为 81af07491915415dad45f87c0c2ae52fae92c06b

import hashlib
from urllib.parse import quote

print(quote(bytes.fromhex("81af07491915415dad45f87c0c2ae52fae92c06b")))

# 或者

print(quote(hashlib.sha1(...).digest()))

使用 python 从种子中计算 info_hash 的例子:

import hashlib
from bencode2 import bdecode, bencode

with open('ubuntu-24.04-live-server-amd64.torrent', 'rb') as f:
    data = bdecode(f.read())

info_hash = hashlib.sha1(bencode(data[b'info'])).digest()

(需要使用 pip 安装 bencode2)

peer_id

一个 20 字节长度的字符串,代表客户端的 ID。每个客户端应该为每个下载都生成一个随机 ID 。 在 URL 中需要进行转义。

Tip

常见客户端的 peer id 会遵循一定的规范,以方便 tracker 判断请求所对应的客户端。

ip

可选参数,用于向 tracker 汇报客户端所在的 ip 地址。

port

客户端的 bt 协议监听窗口。

Tip

出于安全考虑,许多 tracker 禁止客户端使用特权端口和一些其他常见协议的端口。建议使用高位随机端口。

uploaded

客户端目前为止的上传量,以十进制字符串格式编码。

Tip

虽然原始规范没有定义,但一般来说会发送 started 事件后的上传量,以字节数计算。

如果你暂停了一个有1G累积上传量的种子,然后重新开始做种,那么客户端应该汇报一个 started 事件并且汇报 0 的上传量。 如果随后又进行了2G的上传,此时应该汇报2G的上传量。

如果此时添加一个新的tracker,QB(libtorrent 2.0.10 )会汇报 started 事件,并且汇报 2G 的上传量。其他实现的行为未测试。

downloaded

客户端目前为止的下载量,以十进制字符串格式编码。

Tip

虽然原始规范没有定义,但一般来说会发送 started 事件后的下载量,以字节数计算。

如果你暂停了一个有1G累积下载量的种子,然后重新恢复下载,那么客户端应该汇报一个 started 事件并且汇报 0 的下载量。 如果随后又进行了2G的下载,此时应该汇报2G的下载量。

如果此时添加一个新的tracker,QB(libtorrent 2.0.10 )会汇报 started 事件并且汇报 2G 的下载量。其他实现的行为位置

left

peer 仍然想要下载的字节大小,以十进制字符串格式编码。 这一项不能通过种子对应的文件大小和当前下载量进行计算, 因为 peer 可能在下载前就拥有部分文件, 或者从别的节点接收到了损坏的数据导致某些区块进行了重新下载。

event

可选参数,started, completed, 或者 stopped。或者为空字符串,跟不传递此参数效果相同。

如果未传递此参数,则表示这是在定期汇报。

客户端在下载开始时汇报 started, 在下载完成时汇报 completed。 在对应的下载停止时汇报 stopped

如果本地文件已经下载完成,不需要汇报 started 事件。

响应

tracker 的响应为 bencode 编码的字典。

如果汇报失败,应该包含一个 key failure reason,对应的值应为字符串,包含一个人类可读的错误描述。

如果汇报成功,应该包含两个 key intervalpeers

interval 的值为整数,表示客户端下一次请求前应该等待的秒数。

peers 的值为一个字典组成的列表(list[dict]),每个字典应该包含 keys peer idip, port

ip 为字符串,表示其他 peer 的 ip 地址或者 dns name。port 则为数字。peer id 见前文。

peer 也有可能请求紧凑(compact)的响应,在bep 0023 中进行定义。

peers 也有可能通过 UDP 协议进行汇报,在bep 15中进行定义。

peer protocol

peer 协议可能跑在 TCP 或者 uTP 上。

peer 协议是对称的全双工协议,两个peer发送的消息格式是相同的。

协议中发送的所有整数都被编码为四字节大端字节序(uint32_t)。

peer 协议由两部分组成,握手和 peer 消息

握手

握手为固定长度的 40+8+20+20 字节消息

  1. 固定字符串 b"\x13BitTorrent protocol"
  2. 8 字节的 extension message
  3. 20 bytes info_hash,info_hash 的计算见前文。
  4. 20 bytes peer id

消息流

握手完成后,接下来是事件流。每个消息以接下来的消息长度作为前缀,长度为 4 字节的无符号整数。

如果长度为0,说明是 keep alive 消息,事件可以忽略。

如果长度不为0,接下来的1个字节为消息id,消息id有以下的几种情况:

  • 0 - choke
  • 1 - unchoke
  • 2 - interested
  • 3 - not interested
  • 4 - have
  • 5 - bitfield
  • 6 - request
  • 7 - piece
  • 8 - cancel

(bep 0006 中还定义了 fast extension,对消息 id 进行了扩展)