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("");
}

 

阅读剩余
THE END