BitTorrent 是一种用于分发文件的协议。它通过 URL 标识内容,并且旨在与 Web 网络无缝集成。相对于简单的 HTTP 下载,它的优势在于当多个用户同时下载同一个文件时,这些用户之间可以互相上传文件块,多个用户共同参与文件传输,而非依赖于单一的文件服务器。使得文件源能够支持大量用户同时下载而只对其自身的负载产生较小的影响。
BitTorrent 文件分发由以下实体组成:#
- 一个普通的 Web 服务器
- 一个静态的 metainfo 文件
- 一个 BitTorrent tracker
- 一个原始下载器
- 网络浏览器
- BT 下载客户端
- 一个文件在最理想情况下能够被许多最终用户共享下载。
要开始服务,主机要执行以下步骤:#
- 启动一个 tracker(或者已经运行一个)。
- 启动一个普通的 Web 服务器,比如 Apache(或者已经有一个运行中)。
- 在他们的 Web 服务器上将扩展名 .torrent 与 MIME 类型 application/x-bittorrent 相关联(或者已经这样做了)。
- 使用要提供服务的完整文件和 tracker 的 URL 生成 metainfo (.torrent) 文件。
- 将 metainfo 文件放在 Web 服务器上。
- 从其他网页链接到 metainfo (.torrent) 文件。
- 启动一个已经拥有完整源文件的下载器。
要开始下载,用户执行以下操作:#
- 安装 BitTorrent 客户端。
- 上网冲浪。
- 打开 .torrent 文件的链接。
- 选择在本地保存文件的位置,或选择要继续的部分下载。
- 等待下载完成。
- 退出下载客户端(它会一直上传直到收到退出指令)。
Bencode 编码#
Bencode 编码用于将任意数据类型的值序列化(也称为 “串行化”)为字节流,以便在网络上传输或存储在磁盘上。 Bencode 编码支持字符串、整数、列表和字典四种基本数据类型。
- 对于字符串类型,采用长度前缀加冒号的方式表示,例如”spam” 会被编码为 4 。
- 对于整数类型,以字符”i” 开始,后面跟着十进制表示的整数和字符”e”,例如 3 被编码为 i3e,-3 被编码为 i-3e 。
- 对于列表类型,以字符”l” 开始,后面跟着列表中各个元素的 Bencode 编码,最后以字符”e” 结束,例如 [‘spam’, ‘eggs’] 被编码为 l4:spam4 。
- 对于字典类型,以字符”d” 开始,后面跟着按照 key 的 ASCII 码排序的键值对的 B 编码,最后以字符”e” 结束,例如 {‘cow’: ‘moo’, ‘spam’: ‘eggs’} 被编码为 d3:cow3:moo4:spam4 。需要注意的是,所有字典类型的键必须是字符串,并且按照原始字符串的排序方式进行排序。
Bencode 编码提供了一种简单、紧凑、可读性较强的数据序列化方案,适用于各种数据类型的序列化和反序列化操作。
Metainfo 文件#
MetaInfo 文件(也称为 .torrent 文件)是使用 bencoding 编码的字典,包含以下键:
announce
Tracker 的 URL 地址。
info
这个键映射到一个字典中,其中包含下面描述的键。
在包含文本的 .torrent 文件中的所有字符串都必须使用 UTF-8 编码。
info dictionary#
BitTorrent 下载文件时所使用的元数据信息。它包含了多个键值对,其中每个键都代表了一个特定的属性,例如建议保存的文件名、文件块的大小和哈希值等。这些属性可以帮助客户端正确地下载并组装文件。
其中一些重要的属性包括:
- name:建议保存的文件名,它纯粹是为了提供建议而不具有强制性。
- piece length:文件被分成固定大小的块,该属性指定每个块的大小(通常是 2 的幂次方)。
- pieces:每个块对应的哈希值,用于验证文件的完整性。
- length:文件长度(只适用于单个文件的情况),或者所有文件拼接后的总长度。
如果下载的是单个文件,则只需要使用 name 和 length 属性。如果是多个文件,则需要使用 files 列表来表示文件的目录结构,其中每个文件都由一个字典表示,包含 length 和 path 属性。
trackers#
Tracker GET 请求包含以下关键字:
info_hash:元信息文件中 info 值的 bencoded 形式的 20 字节 SHA1 哈希值。该值几乎肯定需要进行转义。请注意,这是元信息文件的子字符串。 info-hash 必须是 .torrent 文件中编码形式的哈希值,这与对元信息文件进行 bencoded 解码、提取 info 字典并编码它完全相同,如果 bencoded 编码完全验证了输入(例如,键排序、前导零的缺失),则必须这样做。反之,这意味着客户端必须拒绝无效的元信息文件或直接提取子字符串。不能对无效数据执行解码 - 编码往返。
peer_id:长度为 20 的字符串,用作此下载器的 ID 。每个下载器在开始新下载时都会随机生成自己的 ID 。该值也几乎肯定需要进行转义。
ip:可选参数,指定此对等方所在的 IP 地址(或 DNS 名称)。通常用于源地址,如果源位于与跟踪器相同的计算机上,则使用此参数。
port:此对等方正在侦听的端口号。常见行为是让下载器尝试侦听端口 6881,并在该端口被占用时尝试 6882 、 6883 等端口号,并在 6889 后放弃。
uploaded:到目前为止已上传的总量,以十进制 ASCII 编码。
downloaded:到目前为止已下载的总量,以十进制 ASCII 编码。
left:此对等方仍需下载的字节数,以十进制 ASCII 编码。请注意,这不能从 downloaded 和文件长度计算出来,因为它可能是恢复下载,并且有可能一些已下载的数据未通过完整性检查而需要重新下载。
event:这是一个可选关键字,映射到 started 、 completed 或 stopped(或空,与不存在相同)。如果不存在,则是定期通告之一。当下载开始时,使用 started 发送一个通知;当下载完成时,使用 completed 发送一个通知。如果在开始时文件已完成,则不发送 completed 。下载器停止下载时会使用 stopped 发送一个通告。
Tracker 响应是 bencoded 字典。如果跟踪器响应具有 failure reason 关键字,则该关键字映射到人类可读的字符串,用于解释查询失败的原因,并且不需要其他关键字。否则,它必须具有两个关键字:interval,其映射到下载器应在定期重新请求之间等待的秒数;并且 peers,其映射到对等方列表的字典,每个字典包含 peer id 、 ip 和 port 这三个关键字,分别表示对等方的自选择 ID 、 IP 地址或 DNS 名称以及端口号。请注意,如果发生事件或需要更多对等方,下载器可能在非定期时间上重新请求。
更常见的是,跟踪器会返回对等方列表的压缩表示形式,请参见 Tracker Returns Compact Peer Lists 。
如果您想对元信息文件或跟踪器查询进行任何扩展,请与 Bram Cohen 协调,以确保所有扩展都是兼容的。
通常还可以通过 UDP 跟踪器协议进行通告。
peer protocol#
BitTorrent 的对等协议通过 TCP 或 uTorrent transport protocol 运行。在 BitTorrent 中,下载者和上传者都是对等的,数据可以在它们之间相互流动。数据被分成许多小块,从多个源同时下载,从而提高下载速度和传输的可靠性。每当下载者完成一块文件的下载,并检查哈希值匹配时,它会向所有对等者广播该块的信息。
连接状态有两类:阻塞或未阻塞;感兴趣或不感兴趣。阻塞表示没有数据将被发送,直到解除阻塞。数据传输只有在一方感兴趣且另一方未阻塞的情况下才会发生。为了实现正确的数据传输,下载者必须保持感兴趣的状态并及时更新,尽管受到了阻塞。传输数据时,下载者应保持多个块的请求排队以获得良好的 TCP 性能,而请求不能立即写入 TCP 缓冲区的请求则应排队在内存中,以便在阻塞发生时全部丢弃。
对等协议由握手过程和永无止境的长度前缀消息组成。握手以十九(十进制)字符开头,后面跟着字符串 “BitTorrent 协议” 。在连接两个节点之前,它们需要进行握手以交换必要的信息。握手开始时,发送方会向接收方发送一个固定的握手消息,其中包括协议版本、保留字节、元数据哈希值和节点 ID 。接收方会验证这些信息,并检查它们是否与期望的值匹配。如果有任何不匹配,连接将断开。握手成功后,双方开始交换数据。数据是通过一系列长度前缀和消息来传输的。长度前缀表示该消息的长度,然后是实际的消息内容。
peer messages#
所有非激活消息都以单个字节开头,下面给出了它们的类型。
可能的值为:
- 0 – choke
- 1 – unchoke
- 2 – interested
- 3 – not interested
- 4 – have
- 5 – bitfield
- 6 – request
- 7 – piece
- 8 – cancel
‘choke’, ‘unchoke’, ‘interested’, and ‘not interested’ 没有有效载荷。
bitfield 消息:该消息只会作为第一个消息被发送。它的负载是一个位域,每个下载器发送的索引都被设置为 1,其余索引则被设置为 0 。没有任何数据的下载器可以跳过这个 “bitfield” 消息。位域的第一个字节对应于索引 0-7(从高位到低位),接下来是 8-15,以此类推。结尾处的多余位被设置为 0 。
have 消息:该消息的负载是一个单一的数字,表示该下载器完成并检查哈希值的索引。
request 消息:该消息包含一个索引、偏移量和长度。最后一个参数通常是 2 的幂次方,除非已经到达文件的末尾。所有当前的实现都使用 2^14(16 kiB),并关闭请求超出此限制的连接。
cancel 消息:与请求消息具有相同的有效负载。它们通常只在下载结束时发送,在所谓的 “终局模式” 期间。当下载几乎完成时,最后几部分倾向于从单个软管调制解调器线路下载,这需要很长时间。为了确保最后几块快速进入,一旦给定下载器尚未收到的所有部分的请求当前处于待处理状态,它就会向正在下载的每个人发送所有内容的请求。为了防止这变得非常低效,每次有一件作品到达时,它都会向其他人发送取消。
piece 消息:包含索引、开始和片段。请注意,它们与请求消息隐式关联。如果快速连续发送阻塞和取消阻塞消息和 / 或传输速度非常慢,则可能会出现意外的片段。
首先,下载器通常会随机下载文件的不同部分,从而避免出现某个下载器拥有其他下载器的子集或超集的情况。其次,为了保证每个下载器都能获得一致的下载速度,需要进行限制上传速度的操作(也称作 choking)。这种算法能够让下载器之间使用类似于” 以牙还牙” 的策略,以保证自己能获得稳定的下载速度。最后,当前已经在使用的 choking 算法,并强调了新算法需要在不同网络环境下都表现良好的重要性。
首先,为了保证良好的 TCP 性能,算法需要限制同时上传的连接数。其次,为了避免频繁地 choke 和 unchoke,算法需要稳定地选择上传对象。此外,在一定程度上应该回报那些让自己下载的 peer 。最后,算法应该尝试利用未使用的连接来提高下载速度,这被称作 optimistic unchoking 。
当前部署的 choking 算法采取了一些措施来实现这些目标。为了避免频繁的变化,算法每 10 秒才会更改哪些 peer 被 choke 。为了回报其他人,并限制上传连接数量,算法选择前四个下载速率最好且有兴趣的 peer 来 unchoke 。如果有其他 peer 的上传速率更好但是没有兴趣,则该 peer 也可能会被 unchoke,如果它突然有了兴趣,就会 choke 掉下载速率最差的 peer 。如果某个 downloader 已经完整地下载了文件,则它将使用上传速率而不是下载速率来决定 unchoke 哪些 peer 。
至于 optimistic unchoking,算法会每 30 秒轮流选择一个 peer 进行 unchoke,无论其上传速率如何(只要有兴趣就算一个下载器)。在 rotation 中,新连接被三倍概率放在当前 optimistic unchoke 的位置上,以便给它们足够的机会上传完整的数据块。