eBit Tracker 实现文档
一、架构概述
1.1 技术栈
运行环境: Cloudflare Workers (Serverless)
存储系统: Cloudflare KV (BITTORRENT_TRACKER命名空间)
协议支持: HTTP Tracker协议 + Scrape扩展
代码架构: ES6 Module + 异步处理模型
1.2 数据存储设计
interface PeerRecord { key: string; // "ip:port" timestamp: number; // Unix时间戳 ip: string; port: number; } interface TorrentStats { seeders: number; leechers: number; downloads: number; } KV存储结构: - tracker:<infohash> : PeerRecord[] // Peer列表 - stats:<infohash> : TorrentStats // 统计信息 - call_count : number // API总调用次数
二、核心功能实现
2.1 Announce 处理流程
sequenceDiagram participant Client participant Worker participant KV Client->>Worker: GET /announce?info_hash=...&peer_id=... Worker->>KV: 获取tracker:<infohash> Worker->>Worker: 过滤过期Peer(30分钟) alt event=stopped Worker->>KV: 删除当前Peer else Worker->>KV: 添加/更新当前Peer end Worker->>KV: 更新stats:<infohash> Worker->>Client: 返回B编码响应
2.2 Compact 模式实现
// IPv4转换示例:192.168.1.100:6881 → 6字节二进制 function convertIPPortToBinary(ip, port) { return ip.split('.') .map(o => String.fromCharCode(+o)) // 4字节IP .concat([ String.fromCharCode(port >> 8 & 0xFF), // 高位端口 String.fromCharCode(port & 0xFF) // 低位端口 ]).join(''); } // 响应构造逻辑 if (compact) { const peersBinary = peerList.map(p => convertIPPortToBinary(p.ip, p.port)).join(''); responseStr = `d8:intervali${PEER_TIMEOUT}e5:peers${peersBinary.length}:` + peersBinary + "e"; }
2.3 统计系统实现
// 更新种子统计 async function updateStats(env, infoHash, event) { const statsKey = `stats:${infoHash}`; let stats = await env.BITTORRENT_TRACKER.get(statsKey); stats = stats ? JSON.parse(stats) : { seeders: 0, leechers: 0, downloads: 0 }; if (event === "completed") stats.downloads++; stats.seeders = peerList.length; stats.leechers = Math.max(stats.seeders - 1, 0); // 简化计算 await env.BITTORRENT_TRACKER.put(statsKey, JSON.stringify(stats)); }
三、高级配置
3.1 环境变量
# wrangler.toml 配置示例 [vars] PEER_TIMEOUT = 1800 # Peer过期时间(秒) MAX_PEERS = 50 # 单种返回Peer上限 TRACKER_URL = "http://ebit.e451.xin/announce"
3.2 KV 命名空间绑定
# wrangler.toml [[kv_namespaces]] name = "BITTORRENT_TRACKER" id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
四、扩展开发指南
4.1 添加 UDP 协议支持
// 伪代码示例 addEventListener('fetch', event => { if (isUDPPacket(event.request)) { event.respondWith(handleUDPAnnounce(event)); } }); async function handleUDPAnnounce(event) { // 解析UDP数据包 // 实现BEP15协议 }
4.2 实现分布式追踪
// 在handleAnnounce中添加 const dhtNodes = await fetch('https://dht-tracker.com/nodes'); responseDict['nodes'] = dhtNodes; // 按BEP5规范编码
4.3 安全扩展建议
// 实现IP黑名单 const BLACKLIST = ['1.1.1.1', '2.2.2.0/24']; function checkIP(ip) { return !BLACKLIST.some(range => ipInCIDR(ip, range)); } // 在handleAnnounce开头添加 if (!checkIP(clientIP)) { return new Response("IP Banned", {status: 403}); }
五、性能优化
5.1 KV 存储优化
// 批量写入示例 async function batchUpdatePeers(env, infoHash, peers) { const batchSize = 100; for (let i = 0; i < peers.length; i += batchSize) { const chunk = peers.slice(i, i + batchSize); await env.BITTORRENT_TRACKER.put( `tracker:${infoHash}:chunk${i/batchSize}`, JSON.stringify(chunk) ); } }
5.2 缓存机制
// 使用内存缓存 const cache = { peerLists: new Map(), lastUpdated: 0 }; async function getCachedPeers(env, infoHash) { if (Date.now() - cache.lastUpdated > 5000) { // 每5秒更新缓存 cache.peerLists.set(infoHash, await env.BITTORRENT_TRACKER.get(...)); cache.lastUpdated = Date.now(); } return cache.peerLists.get(infoHash); }
六、监控与调试
6.1 实时监控指标
// 在showHomePage中添加 const systemStats = { memoryUsage: process.memoryUsage().rss, uptime: Math.floor(process.uptime()), activeRequests: performance.now().toFixed(2) }; // 展示到状态页 html += `<p>内存使用: ${systemStats.memoryUsage} MB</p>`;
6.2 调试技巧
# 使用 curl 测试 Announce curl "http://tracker/announce?info_hash=test&peer_id=abc&port=6881&event=started" # 解析B编码响应 npm install -g bencode-cli curl ... | bencode
七、部署指南
7.1 使用 Wrangler 部署
# 安装 CLI npm install -g wrangler # 登录 Cloudflare wrangler login # 发布 Worker wrangler publish
7.2 压力测试
# 安装 vegeta go install github.com/tsenart/vegeta@latest # 创建测试用例 echo "GET http://tracker/announce?info_hash=test..." > targets.txt # 运行测试 vegeta attack -duration=60s -rate=100 < targets.txt | vegeta repor
源码
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === "/announce") {
return await handleAnnounce(url, env);
}
if (url.pathname === "/scrape") {
return await handleScrape(url, env);
}
if (url.pathname === "/") {
return await showHomePage(env);
}
return new Response("Invalid request", { status: 400 });
},
};
const PEER_TIMEOUT = 1800; // 30分钟未更新视为无效
const MAX_PEERS = 50; // 限制返回的 peers 数量
async function showHomePage(env) {
let keys = await env.BITTORRENT_TRACKER.list();
let totalTorrents = keys.keys.length;
let totalSeeders = 0;
let totalLeechers = 0;
for (let key of keys.keys) {
if (key.name.startsWith("stats:")) {
let stats = await env.BITTORRENT_TRACKER.get(key.name);
stats = stats ? JSON.parse(stats) : { seeders: 0, leechers: 0, downloads: 0 };
totalSeeders += stats.seeders;
totalLeechers += stats.leechers;
}
}
// 读取 API 调用次数
let callCount = await env.BITTORRENT_TRACKER.get("call_count");
callCount = callCount ? parseInt(callCount) : 0;
let html = `
<html>
<head>
<meta charset="UTF-8">
<title>eBit Tracker Status</title>
<style>
body { font-family: Arial, sans-serif; text-align: center; padding: 20px; }
h1 { color: #333; }
.stats { font-size: 18px; margin-top: 20px; }
.info { text-align: left; max-width: 600px; margin: 0 auto; }
.box { border: 1px solid #ddd; padding: 10px; margin: 10px 0; background: #f9f9f9; }
.credits { text-align: center; margin-top: 20px; font-size: 14px; color: #666; }
</style>
</head>
<body>
<h1>eBit Tracker 运行状态</h1>
<div class="stats">
<p> 总种子数: <strong>${totalTorrents}</strong></p>
<p> Seeder 数量: <strong>${totalSeeders}</strong></p>
<p> Leecher 数量: <strong>${totalLeechers}</strong></p>
<p> API 调用次数: <strong>${callCount}</strong></p>
</div>
<div class="info">
<h2>如何创建新种子?</h2>
<div class="box">
使用 <strong>BitTorrent 客户端</strong>(如 qBittorrent、Transmission)创建种子文件。<br>
在 Tracker URL 中填写:<br>
<code>http://ebit.e451.xin/announce</code><br>
生成种子文件,并开始做种!<br>
</div>
</div>
<div class="credits">
Powered By Cloudflare Workers. Copyright © 2025 By Ziyang-Bai All rights reserved.
</div>
</body>
</html>
`;
return new Response(html, { headers: { "Content-Type": "text/html; charset=UTF-8" } });
}
async function handleAnnounce(url, env) {
const params = url.searchParams;
const infoHash = params.get("info_hash");
const peerId = params.get("peer_id");
const ip = params.get("ip") || getClientIP(url);
const port = params.get("port") || "6881";
const event = params.get("event"); // started, stopped, completed
const compact = params.get("compact") === "1";
if (!infoHash || !peerId) {
return new Response("Missing info_hash or peer_id", { status: 400 });
}
// 记录 API 调用次数
let callCount = await env.BITTORRENT_TRACKER.get("call_count");
callCount = callCount ? parseInt(callCount) + 1 : 1;
await env.BITTORRENT_TRACKER.put("call_count", callCount.toString());
const peerKey = `${ip}:${port}`;
const trackerKey = `tracker:${infoHash}`;
const statsKey = `stats:${infoHash}`;
let peerList = await env.BITTORRENT_TRACKER.get(trackerKey);
peerList = peerList ? JSON.parse(peerList) : [];
// 处理不同的事件
if (event === "stopped") {
peerList = peerList.filter((p) => p.key !== peerKey); // 删除 Peer
} else {
const timestamp = Math.floor(Date.now() / 1000);
// 过滤掉超过超时时间的 peer
peerList = peerList.filter((p) => timestamp - p.timestamp < PEER_TIMEOUT);
peerList.push({ key: peerKey, timestamp, ip, port: parseInt(port) });
if (peerList.length > MAX_PEERS) {
peerList = peerList.slice(-MAX_PEERS);
}
}
await env.BITTORRENT_TRACKER.put(trackerKey, JSON.stringify(peerList));
// 更新种子统计信息
let stats = await env.BITTORRENT_TRACKER.get(statsKey);
stats = stats ? JSON.parse(stats) : { seeders: 0, leechers: 0, downloads: 0 };
if (event === "completed") {
stats.downloads++;
}
stats.seeders = peerList.length;
stats.leechers = Math.max(stats.seeders - 1, 0);
await env.BITTORRENT_TRACKER.put(statsKey, JSON.stringify(stats));
// 构造 bencoded 响应
let responseStr;
if (compact) {
// 构造二进制的 peers 数据,每个 peer 6 字节
const peersBinary = peerList
.map((p) => convertIPPortToBinary(p.ip, p.port))
.join("");
// 按 bencoding 规范,字符串值需要以 "<长度>:<数据>" 格式编码
responseStr = `d8:intervali${PEER_TIMEOUT}e5:peers${peersBinary.length}:` + peersBinary + "e";
} else {
responseStr = `d8:intervali${PEER_TIMEOUT}e5:peersl`;
responseStr += peerList
.map((p) => `d2:ip${p.ip.length}:${p.ip}4:porti${p.port}ee`)
.join("");
responseStr += "ee";
}
// 将字符串转换为 Uint8Array,确保返回原始二进制数据
const body = Uint8Array.from(responseStr, c => c.charCodeAt(0));
return new Response(body, {
headers: { "Content-Type": "application/octet-stream" }
});
}
async function handleScrape(url, env) {
const params = url.searchParams;
const infoHash = params.get("info_hash");
if (!infoHash) {
return new Response("Missing info_hash", { status: 400 });
}
const statsKey = `stats:${infoHash}`;
let stats = await env.BITTORRENT_TRACKER.get(statsKey);
stats = stats ? JSON.parse(stats) : { seeders: 0, leechers: 0, downloads: 0 };
const responseStr = `d5:filesd${infoHash}d8:completei${stats.seeders}e10:downloadedi${stats.downloads}e10:incompletei${stats.leechers}eee`;
const body = Uint8Array.from(responseStr, c => c.charCodeAt(0));
return new Response(body, { headers: { "Content-Type": "application/octet-stream" } });
}
// 获取客户端 IP
function getClientIP(url) {
return url.hostname; // Cloudflare 会自动解析客户端 IP
}
// 转换 IP 和端口到二进制格式 (compact 模式)
// 生成 6 字节字符串:前 4 字节为 IP,后 2 字节为端口
function convertIPPortToBinary(ip, port) {
let bytes = ip.split(".").map((octet) => String.fromCharCode(parseInt(octet, 10)));
bytes.push(String.fromCharCode((port >> 8) & 0xff));
bytes.push(String.fromCharCode(port & 0xff));
return bytes.join("");
}
阅读剩余
Copyright © 2024 Ziyang-Bai 保留所有权利
THE END