在开发 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
对等方客户端随机唯一 IDip
对等方 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 进行了数次增强,你可以阅读其他文章来了解更多增强协议。