BT Tracker // BEP 0003 - The BitTorrent Protocol Specification

BT Tracker // BEP 0003 - The BitTorrent Protocol Specification

在开发 BitSapling/Sapling 的过程中,我意识到中文互联网似乎没有很好介绍 BitTorrent 协议的文章(当然,英文文献也并不是很多),同时 BitTorrent 官方的 BEP 介绍也含糊不清,特开新的一个文章系列来记录各个 BEP 的 Tracker 部分的具体实现方式。 B

在开发 BitSapling/Sapling 的过程中,我意识到中文互联网似乎没有很好介绍 BitTorrent 协议的文章(当然,英文文献也并不是很多),同时 BitTorrent 官方的 BEP 介绍也含糊不清,特开新的一个文章系列来记录各个 BEP 的 Tracker 部分的具体实现方式。


BEP 0003 - The BitTorrent Protocol Specification

0003 号 BEP 是 BitTorrent 协议中最基础的规范,它规范了一个 BitTorrent Tracker 最基础的功能。

Bencode 编码

在 BT 协议中,客户端、.torrent 文件和 Tracker 之间交流和存储信息都大量的使用了一种名为 “Bencode” 的编码方式,在网络上已有大量相关的说明,在此不再详细解释。

BitTorrent 指定 Bencode 编码为字符串时使用的是 ISO_8859_1 编码,又被称为 ISO-LATIN-1,它是一种单字节编码。

那么为什么不是 UTF-8?原因很简单,ISO_8859_1 可以将任何 byte[] 都编码成 String 而不会出现信息丢失。

而恰巧 BitTorrent 不仅仅用 Bencode 编码普通字符串,还用来编码任意二进制数据ISO_8859_1 这种编码方式确保了任意二进制数据被编码为 String 后还能正确被被原样解码回 byte 数组。

因此,你应该始终传递原始 byte 数组,而不是 String 本身,以便根据场景的不同将 byte 数组随时转换为 ISO_8859_1 编码的字符串(例如 SHA1 哈希值),或者是 UTF-8 编码的字符串(例如 filename 文件名或者 path 路径),或者直接作为二进制读取(info_hash)。

是的你没看错,BT 中不但是用了 ISO_8859_1 编码,也同时存在 UTF-8 编码——部分客户端中则更加混乱,这也是我建议你传递原始 byte 数组的原因。

在本套文章中,除非额外说明,编码和解码时均使用 ISO_8859_1 编码。

二进制字符串

将一个 byte 数组通过 Bencode ISO_8859_1 编码为 String 后,这个 String 在本系列文章中被称为 “二进制字符串”。

Tracker

本系列文章专注介绍 Tracker 部分的 BEP 规范,而非 Client 亦或是 .torrent 文件(尽管部分文章中仍会提及)。

BEP 0003 中规范一个 Tracker 接收一个 GET 请求,并通过 URL 查询字符串来传递客户端数据,以下是一个基本示例:

GET /announce?info_hash=j%25%7c%fe%12%0e%c0%9d%ee6%d5%df%03%bb%fda%cd%7b%97%b5&peer_id=-qB4510-1MTFo0SteXN2&port=22387&uploaded=0&downloaded=0&left=0&corrupt=0&key=332CA113&event=started&numwant=200&compact=1&no_peer_id=1&supportcrypto=1&redundant=0&ipv4=198.18.0.1&ipv6=2408%3a8214%3a2e11%3a8cb1%3a%3a9b8&ipv6=2408%3a8214%3a2e11%3a8cb1%3a7c27%3acac2%3a3b51%3a42fb&ipv6=2408%3a8214%3a2e11%3a8cb1%3ae0e7%3a8eb%3a4677%3a21f0

我们可以看到有众多查询参数,但实际上其中很多都是由其它拓展协议引入的,我们会在后面的文章中逐个介绍它们,在本文中,我们仅介绍 BEP 0003 中规范的几个参数:

  • info_hash - 这是 .torrent 文件的 info 段的 SHA-1 哈希值,它是个经过 URL 编码的二进制字符串
  • peer_id - 由客户端自行生成的唯一随机 ID,由于在 BT 网络中区分不同的客户端,部分客户端的 peer_id 可能包含不可读字符
  • uploaded - 代表本次会话期间,客户端一共上传了多少字节
  • downloaded - 代表本次会话期间,客户端一共下载了多少字节
  • left - 代表客户端还剩余多少字节等待下载(注意:由于存在校验环节,left 字段仅供参考)
  • event - 代表该 torrent 的事件
    • 事件列表
      • started 种子开始下载
      • completed 种子下载完成,开始做种
      • stopped 种子停止下载/做种,不再活动
      • empty 和没有此字段的情况完全相同
    • 如果一个 announce (宣告)中不包含此字段,则说明客户端正在定期和 tracker 更新数据,否则,代表该种子的状态发生了改变
    • 该字段是个可选字段

请求发送到 Tracker 后,不管是否出现错误,Tracker 都应该返回一个 200 OK 的状态码,并使用 Bencode 编码响应。

解析 info_hash

SHA-1 生成的哈希值其实原本是一个 byte 的数组,而我们通常看到的类似 6a257cfe120ec09dee36d5df03bbfd61cd7b97b5 其实是其 16 进制值(HEX)。

同理,解析 info_hash 则要先进行 URL 解码,然后将得到的 byte 数组以 16 进制的形式输出。

以下是一个 Java 代码片段:

    public static @NotNull String parseInfoHash(String encoded) throws IllegalArgumentException {
        try {
            StringBuilder r = new StringBuilder();
            for (int i = 0; i < encoded.length(); i++) {
                char c = encoded.charAt(i);
                if (c == '%') {
                    r.append(encoded.charAt(i + 1));
                    r.append(encoded.charAt(i + 2));
                    i = i + 2;
                } else {
                    r.append(String.format("%02x", (int) c));
                }
            }
            return r.toString().toLowerCase(Locale.ROOT);
        } catch (Exception e) {
            throw new IllegalArgumentException("Failed to decode info_hash: " + encoded);
        }
    }

输入:j%25%7c%fe%12%0e%c0%9d%ee6%d5%df%03%bb%fda%cd%7b%97%b5

输出:6a257cfe120ec09dee36d5df03bbfd61cd7b97b5

响应

响应分为 成功响应错误响应 两类。

成功响应

返回一个经过 Bencode 编码的 Dictionary (也就是 Map),包含以下 key:

  • interval - 代表间隔时间,单位是秒,表示 BT 客户端应该指定时间之后再与 Tracker 联系更新状态
  • peers - 这是个 List,每个 List 存储一个 Dictionary(也就是 Map),每个 Dictionary 包含以下 key:
    • peer id 对等方客户端随机唯一 ID
    • ip 对等方 IP 地址,既可是 IPV6,也可以是 IPV6,以常规字符串表示即可,如 `127.0.0.11 或者 ::1,也支持 DNS 名称
    • port 对等方端口号

示例(使用 JSON 格式以提高可读性)

{
    "interval": 3600,
    "peers": [
        {
            "ip": "192.168.1.1",
            "port": 12345,
            "peer id": "-qB4510-1MTFo0SteXN2"
        },
        {
            "ip": "192.168.1.2",
            "port": 12346,
            "peer id": "-qB4510-4MTFa1SteXb3"
        }
    ]
  }

参考代码

    @NotNull
    private Map<String, Object> generatePeersResponseStandard(@NotNull String infoHash, int numWant, boolean noPeerId) {
        PeerResult peers = gatherPeers(infoHash, numWant);
        List<Map<String, Object>> peerList = new ArrayList<>();
        List<Peer> allPeers = new ArrayList<>(peers.peers());
        allPeers.addAll(peers.peers6());
        for (Peer peer : allPeers) {
            Map<String, Object> peerMap = new LinkedHashMap<>();
            if (!noPeerId) {
                peerMap.put("peer id", peer.getPeerId());
            }
            peerMap.put("ip", peer.getIp());
            peerMap.put("port", peer.getPort());
            peerList.add(peerMap);
        }
        Map<String, Object> dict = new HashMap<>();
        dict.put("interval", randomInterval());
        dict.put("peers", peerList);
        return dict;
    }
String finalResponse = bencode(generatePeersResponseStandard(infoHash, numWant, noPeerId));

失败响应

返回一个经过 Bencode 编码的 Dictionary (也就是 Map),包含以下 key:

  • failure reason - 代表错误原因,返回一个人类可读的错误信息

示例(使用 JSON 格式以提高可读性)

{
    "failure reason": "This torrent not registered on this tracker."
}

至此,你已完成了一个最基本的 Tracker,能够接收客户端的 announce(宣告),将其存储起来后,在下一次宣告时返回已存储的其他人的 IP 和端口号,完成客户端之间的信息交换,客户端便能够互相连接和下载/上传文件了。

当然,这只是最基本的内容,BitTorrent 在过去的时光里通过多个 BEP 进行了数次增强,你可以阅读其他文章来了解更多增强协议。


Comment