BitTorrent 是一个用于文件分发的协议。它通过 URL 来标识内容,其设计使其可以与 Web 无缝集成。BitTorrent 相对于 HTTP 的优势在于,当相同文件的多个下载并行进行时,下载者之间还可以互传数据,这就使得文件源在仅增加少量负载的情况下支持数量众多的下载成为可能。
BitTorrent 文件分发由以下部分组成
- 一个普通的 Web 服务器
- 一个静态的 'metainfo' 文件
- 一个 BitTorrent tracker
- 一个 '原始的' 下载者
- 终端用户 Web browsers
- 终端用户下载者
理想情况下,单个文件有多个终端用户在下载。
要提供 BitTorrent 下载服务,主机需要执行如下步骤
- 启动运行一个 Tracker。
- 启动运行一个普通的 Web 服务器,比如 apache。
- 在他们的 Web 服务器上将扩展名 .torrent 和 mimetype application/x-bittorrent 关联起来。
- 使用要发布的完整文件和 Tracker 的 URL 生成一个 metainfo(.torrent)文件。
- 将 metainfo 文件放到 Web 服务器上。
- 将 metainfo(.torrent)文件的链接放到其它的 Web 页面里。
- 启动一个已经有了完整文件的下载器('origin')。
要启动下载,用户需执行如下步骤
- 安装 BitTorrent。
- 浏览 Web。
- 点击 .torrent 文件的链接。
- 选择保存文件的本地路径,或者选择一个已经部分下载的文件恢复下载。
- 等待下载完成。
- 退出下载器(它会持续上传数据直到退出)。
B编码
- 字符串表示为,十进制的长度前缀,后跟冒号和字符串本身。比 4:spam 表示 spam。
- 整数表示为,i 后跟十进制数字,然后是一个 e 字符表示结束。比如 i3e 表示 3,i-3e 表示 -3。整数没有大小限制。i-0e 是无效的。所有以 0 为前导的编码,比如 i03e,都是无效的,除了 i0e,而它当然表示 0。
- 列表被编码为,l 后跟它们的元素(也是 B 编码的),然后是 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 文件
Metainfo 文件(即 .torrent 文件)是 B 编码的字典,它具有如下的键:
announce
- Tracker 的 URL。
info
- 它映射到一个字典,下面会描述它的键。
一个 .torrent 文件中所有包含文本的字符串必须以 UTF-8 编码。
info 字典
name
键映射为一个 UTF-8 编码的字符串,它建议的保存文件(或字典)时所采用的名字。这纯粹是建议性的。
piece length
映射为文件分割后每个片的字节数。为了便于数据传输,文件被分割为固定大小的片,它们具有相同的长度,除了最后的文件片可能由于截断而不同外。piece length
几乎总是 2 的幂,最常见的是 2^18 = 256K(3.2 版之前的 BitTorrent 版本使用 2^20 = 1M 作为默认值)。
pieces
映射为长度是 20 的整数倍的字符串。它将被细分为长度为 20 的字符串,其中每个都是对应 index 处的片的 SHA1 hash 值。
还有一个键 length
或键 files
,但不会都有或都没有。如果是 length
,表示下载是单个文件,否则表示下载是目录结构中的一系列文件。
在单个文件的情况中,length
映射为文件的字节长度。
出于其他键的目的,多文件的情况仅被看作将 files
列表中出现的文件顺序连接形成的单个文件。文件列表是 files
映射的值,它是包含如下键的字典的列表:
length
- 文件的长度,以字节为单位。
path
- UTF-8 编码的字符串的列表,对应于子目录的名字,它的最后部分是实际的文件名(长度为 0 的列表是错误情况)。
在单个文件的情况中,name
键是文件名,在多文件的情况中,它是目录名。
注:
用 JSON 来表示 .torrent
文件使我们可以对这种文件格式的结构有更清晰的认识。单个文件的 .torrent
文件结构如下:
{
"announce":"xxxx",
"info":{
"name":"xxxx",
"piece length":"xxxx",
"pieces":"xxxx",
"length":"xxxx"
}
}
多个文件的 .torrent
文件结构如下:
{
"announce":"xxxx",
"info":{
"name":"xxxx",
"piece length":"xxxx",
"pieces":"xxxx",
"files":[
{
"length":"xxxx",
"path":"xxxx"
},
{
"length":"xxxx",
"path":"xxxx"
},
{
"length":"xxxx",
"path":"xxxx"
}
]
}
}
trackers
Tracker GET 请求具有如下的键:
info_hash
是
metainfo
文件中info
值的 B 编码形式的 20 字节 SHA1 哈希。这个值几乎肯定必须被转义。注意,这是
metainfo
文件的一个子串。info-hash
必须是.torrent
文件中找到的编码形式的哈希,这与对metainfo
文件进行 B 解码,提取info
字典并对其进行编码相同,当且仅当 B 解码器完全验证输入之后(比如键的顺序,没有前导 0)。相反,这意味着客户端必须拒绝无效的metainfo
文件或直接提取子串。它们不得对无效数据执行解码-编码循环。
peer_id
- 一个下载器用作其 ID 的长度为 20 的字符串。每个下载器在新下载开始时随机生成它自己的 ID。这个值也几乎肯定必须被转义。
ip
- 一个可选的参数,它给出了这个 Peer 的 IP 地址(或域名)。如果它与 tracker 位于相同机器的话,通常被用作源。
port
- 当前 Peer 监听的端口号。下载器的常见行为是,尝试监听端口 6881,但如果那个端口被占用则尝试端口 6882,然后是 6883,等等,直到 6889 之后放弃。
uploaded
- 到目前为止上传的数据量,以十进制 ascii 编码。
downloaded
- 到目前为止下载的数据量,以十进制 ascii 编码。
left
- 当前 Peer 仍然需要下载的字节数,以十进制 ascii 编码。注意,这个值不能用文件的长度和已经下载的数据量来计算,因为它可能是恢复的下载,而且,也有可能下载回来的一些数据在完整性检查时失败,而不得不重新下载。
event
- 这是一个可选的键,它映射为
started
,completed
,或stopped
(或empty
,与没有这个键一样)。如果不存在,则这是定期进行的公告请求之一。使用started
的公告在下载首次启动时发送,使用completed
的将在下载完成时发送。如果启动时文件已经完成则不会发送completed
。当停止下载时下载器发送一个使用stopped
的公告。
注:看了半天也没看懂,到底如何计算 info_hash
。ip
字段的用作源到底是什么意思?是用作数据源么?
由此可见,BitTorrent 客户端向 Tracker 服务器发送的请求数据,如果以 JSON 格式表示的话,看起来可能像下面这样:
{
"info_hash":"xxx",
"peer_id":"xxx",
"ip":"xxx",
"port":1234,
"uploaded":5678,
"downloaded":6789,
"left":12345,
"event":"xxx"
}
但具体是什么样的结构化数据表示格式,目前还看不出来。
Tracker 的响应也是 B 编码的字典。如果 Tracker 的响应包含 failure reason
键,则它是解释查询失败的原因的可读字符串,且不会再有其它键。否则,它必须包含两个键:interval
,它是下载器在两次正常的重新请求之间应该等待的秒数,和 peers
。peers
是字典的列表,即网络中的 Peer 信息,列表中的每一项都包含键 peer id
,ip
和 port
,它们分别是 Peer 为自己选择的 ID,字符串形式的 IP 地址或域名,和端口号。注意,如果事件发生或它们需要更多 Peers,则下载器可能在非调度时间进行重请求。
更常见的是,Trackers 返回 Peer 列表的兼容表示,参见 BEP 23。
注:
Tracker 服务器的响应数据的格式,说明的还是比较清晰的,即 B 编码的字典。响应数据的格式分为两种,分别是请求失败时的响应,和请求成功时的响应。请求失败时的响应格式,如果用 JSON 格式来表示的话,大概看起来像这样:
{
"failure reason":"xxx"
}
请求成功时,响应中包含了正在下载相同资源的其它节点的信息,如果用 JSON 格式来表示这种响应的话,大概看起来像这样:
{
"interval":12345,
"peers":[
{
"peer id":"xxxxxx",
"ip":"xxxxxx",
"port":1234
},
{
"peer id":"xxxxxx",
"ip":"xxxxxx",
"port":1234
},
{
"peer id":"xxxxxx",
"ip":"xxxxxx",
"port":1234
}
]
}
如果你想对 metainfo
文件或 Tracker 查询做扩展,请与 Bram Cohen 合作来确认所有扩展的兼容性。
通过 UDP Tracker 协议通告也很常见。
注:
由上面的内容不难理解,Tracker 服务器 BitTorrent 网络中的中心化服务器,主要用于管理网络中的节点。
Peer 协议
BitTorrent 的 Peer 协议操作基于 TCP 或 uTP。
Peer 连接是对称的。消息在两个方向上的发送看起来是一样的,数据可在任一方向上流动。
Peer 协议通过 metainfo
文件中所描述的索引引用文件的片段,索引从零开始。当 Peer 完成一个片的下载,并检查了哈希值的匹配性,它将向它所有的 Peers 通告它具有该片。
连接在任一端包含两个状态位:阻塞或不阻塞,以及是否感兴趣。(注:即与对端的数据通道是否畅通,以及对端是否具有自己需要的数据。)阻塞是一种通知,在阻塞解除之前将不会发送任何数据。阻塞背后的原因和常见技术将在本文后面解释。
数据传输发生在一端需要数据而另一端没有阻塞的时候。感兴趣状态必须随时保持更新 - 当下载者无需在对端处于非阻塞状态的话,就向对端请求数据的时候,它们必须表达缺乏兴趣,尽管正在被阻塞。正确实现这一点很棘手,但是下载者可以知道哪些 Peers 会在解除阻塞时立即开始下载。
连接开始时处于阻塞且不感兴趣状态。
当传输数据时,下载者应该一次在队列中保持多个片的请求以获得较好的 TCP 性能(这被称为 'pipelining')。另一方面,无法立即写入 TCP 缓冲区的请求应该放进内存队列中,而不是应用级的网络缓冲区中,以使它们在阻塞发生时可以被扔掉。(注:内存缓冲区和应用级缓冲区到底分别指什么?)
Peer 线协议由握手消息及其后永不停止的长度前缀消息流组成。握手消息由字符 9 (十进制)及其后面的字符串 BitTorrent protocol
构成。前导字符是长度前缀,希望其他新协议可以做同样的事情,因此可以在很小的程度上相互区分。
协议中后续发送的整数都以四字节大尾端编码。
固定头部之后是 8 个保留字节,在当前所有的实现中它们都是 0。如果你想使用这些字节扩展协议,请与 Bram Cohen 合作,以确保所有扩展可以兼容地完成。
接下来是 metainfo
文件中 info
值的 B 编码形式的 20 字节 SHA1 哈希。(这个值与向 Tracker 服务器公告的 info_hash
相同,只是在这里它是原始的而不是在这里引用的)。如果两端没有发送相同的值,则切断连接。一个可能的例外是,如果下载者想要通过单个端口执行多个下载,则他们可能会等待传入连接首先提供下载哈希,并且如果它在列表中的话则以相同的哈希响应。
下载哈希后面的是 20 字节的 Peer ID,该 Peer ID 由 Tracker 请求报告,并包含在 Tracker 响应的 Peer 列表中。如果接收端的 Peer ID 与发起端的期待不匹配,则它切断连接。
这就是握手消息,接下来是长度前缀和消息的交替流。长度为 0 的消息是 keepalives
,它们会被忽略。Keepalives
通常每 2 分钟发送一次,但是注意,在预期数据时可以更快地完成超时。
Peer 消息
所有的非 keepalive
消息以一个标识消息类型的字节开始。
可能的值为:
- 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' 只作为第一条消息发送。它的载荷是一个位域,其中下载器已经发送的每个索引设置为 1,其它设置为 0。没有任何东西的下载器可以跳过 bitfield
消息。位域的第一个字节从高位到低位分别对应于索引 0 - 7。接下来的一个是 8 - 15,等等。最后一个字节中多余的位设置为 0。
have
消息的载荷是一个数字,是下载器刚刚完成并检查了哈希值的索引。
request
消息包含索引,起始位置,和长度。后两个是字节偏移量。长度通常是 2 的幂,除非它被文件尾截断。所有当前的实现使用 2^14 (16 kiB),当请求比这个值更大数量的数据时,连接关闭。
cancel
消息具有与 request
消息相同的载荷。它们通常仅在下载结束时发送,称为“结束游戏模式”。当下载快结束时,最后一些数据片倾向于从单个调制解调器线上下载,这需要很长时间。为了确保最后的数据片快速地到达,一旦特定下载器还没有下载到的所有数据片的请求都处于挂起状态,则它向当前正从其下载的每个 Peer 请求所有的数据。为了防止这种情况变得特别低效,每次数据片到达时它都向其它 Peer 发送 cancel
。
piece
消息包含索引,起始位置,和数据片。注意,它们隐式地与请求消息相关联。如果 choke
和 unchoke
消息快速连续地发送和/或传输速度非常慢的话,可能会有未预期的数据片到达。
下载器通常以随机顺序下载数据片,这样可以很好地防止它们拥有任何 Peers 的片的严格子集或超集。
Choking
有几个原因。当一次性通过多个连接发送时,TCP 拥塞控制的表现很差。而且,choking
让每个 Peer 使用 tit-for-tat-ish 算法以确保它们获得一致的下载速率。
下面描述的 choking
算法正是当前部署的。所有新算法在完全由他们自己组成的网络中,以及在主要由这个算法组成的网络中,都能很好地工作是非常重要的。
一个好的 choking
算法应该满足一些标准。它应该限制并发上传的数量以获得良好的 TCP 性能。它应该避免快速地 choking
和 unchoking
,这被称为 抖动
。它应该回应让它下载的 Peer。最后,它应该偶尔尝试使用未使用的连接,以确定它们是否可能比当前使用的更好,这称为乐观的 unchoking
。
当前部署的 choking
算法通过仅改变每隔 10 秒钟 choked
一次的 Peer 避免了抖动。它通过解除对其具有最佳下载速率并且感兴趣的四个 peers 来实现往复和上传数量的限制。拥有更高上传率但不感兴趣的 peer 会被 unchoked,如果它们变得被感兴趣则最糟糕的上传者被 choked。如果下载者拥有一个完整的文件,它使用它的上传速率而不是它的下载速率来决定谁 unchoke。
对于乐观的 unchoking,在任何时间都有一个单独的 peer,它处于 unchoked 状态而无论它的上传速率是多少(如果感兴趣,它被作为四个允许的下载者中的一个)。哪个 peer 是乐观的 unchoked 每隔 30 秒轮转一次。为了给他们一个很好的机会获得一个完整的片段上传,新的连接的开始时间是当前乐观的unchoke的三倍,与旋转中的其他任何地方一样。