Files
dht/README.md
T

14 KiB
Raw Blame History

dht-crawler

Crates.io Documentation License

基于 Rust、Tokio 的 BitTorrent DHT 爬虫库。它加入 BEP-5 网络,接收有效的 announce_peer,并通过 BEP-9 ut_metadata 下载和校验种子元数据。

当前版本:0.2.0。0.2 重做了节点池、主动爬取、Metadata 调度和运行时观测接口, 从 0.1 升级时请先阅读迁移说明CHANGELOG

文档导航

主要能力

  • IPv4、IPv6 和双栈 DHT Socket。
  • 单所有者 crawl actor:严格 FIFO 节点池、最近探测状态、在途请求和所有速率预算 由一个 actor 管理,UDP worker 不锁节点池。
  • 查询 QPS、新目标/分钟、节点替换/分钟、总在途、子网在途、回复包、回复字节和 单来源回复分别限流。
  • 有界 Metadata 队列按 InfoHash 去重,并保留最多三个新鲜 Peer。
  • Metadata 总超时覆盖 TCP 连接、BitTorrent/扩展握手、分片下载、SHA1 校验和解析。
  • SocketAddr 缓存 Peer 的超时/连接失败,避免坏 Peer 反复占用 worker。
  • 传输无关的原子运行时快照和固定桶直方图;可选 metrics feature。
  • 可选 JNI 接口和 Java 示例。

安装

[dependencies]
dht-crawler = "0.2"
tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal"] }

如果应用需要通过 metrics facade 输出指标:

[dependencies]
dht-crawler = { version = "0.2", features = ["metrics"] }
metrics-exporter-prometheus = { version = "0.18", default-features = false, features = ["http-listener"] }

metrics feature 只负责记录指标,不会在库内启动 HTTP 服务。应用必须自行安装 recorder/exporter;完整指标清单见 docs/metrics.md

Cargo features

Feature 默认启用 作用
metrics 通过 metrics facade 记录低基数指标
jni 构建 Java JNI 接口和 cdylib
mimalloc 将 mimalloc 注册为全局分配器

库的默认 feature 集为空。启用 mimalloc 前,请确认最终二进制没有注册其他全局分配器。

快速开始

use dht_crawler::prelude::*;

#[tokio::main]
async fn main() -> Result<()> {
    let options = DHTOptions {
        port: 12313,
        netmode: NetMode::Ipv4Only,
        metadata: MetadataOptions {
            timeout_secs: 4,
            max_queue_size: 10_000,
            max_worker_count: 256,
            ..Default::default()
        },
        crawl: CrawlOptions {
            rate_limit: RateLimitOptions {
                max_find_node_rate_per_sec: 200,
                burst: 40,
                max_in_flight: 512,
                ..Default::default()
            },
            ..Default::default()
        },
        ..Default::default()
    };

    let server = DHTServer::new(options).await?;

    // 返回 true 才允许该 InfoHash 进入实际 Peer 下载阶段。
    server.on_metadata_fetch(|_info_hash| async move { true });

    // 简单回调总是接受交付。
    server.on_torrent(|torrent| {
        println!("{}: {}", torrent.info_hash, torrent.name);
    });

    server.on_error(|error| eprintln!("DHT runtime error: {error}"));

    let shutdown_server = server.clone();
    tokio::spawn(async move {
        if tokio::signal::ctrl_c().await.is_ok() {
            shutdown_server.shutdown();
        }
    });

    // start() 阻塞到 shutdown() 被调用。
    server.start().await
}

可运行版本见 examples/main.rs。如果自己的 Tokio 依赖没有启用 signal feature,可以使用其他取消源调用 shutdown()

架构与背压

UDP sockets
   ├─ bounded UDP worker queues ──→ KRPC workers ──→ bounded crawl events
   │                                      │
   │                                      └─ announce_peer → bounded hash ingress
   │
   └─ crawl egress ← single crawl actor ← priority/discovery events
                         │
                         ├─ strict FIFO node pool + recent-probe set
                         ├─ pending transaction map + subnet counters
                         └─ ArcSwap responsive-node snapshot

hash ingress → deduplicating Metadata queue → bounded workers → torrent callback

所有跨任务入口都是有界队列。达到容量时,事件会被拒绝、淘汰或计入 drop 指标, 不会依靠无限增长的缓冲区掩盖下游过载。主动爬取预算还会随 Metadata 队列压力下降。

主动爬取

  • 新地址通常只发送一次 find_node;默认等待回复 2s,不做同目标重试。
  • 超时不会自动降低配置 QPS。Metadata 队列压力达到 80% 后才开始自动降速,95% 时 降到 metadata_pressure_floor_percent 指定的比例;默认下限为配置 QPS 的 25%。
  • 节点池是严格 FIFO。重复地址、无效公网地址和超出 replacement budget 的替换会被拒绝。
  • 响应成功的节点进入一个独立、有界、带 TTL 的 responsive ring,用于回复其他 DHT 节点和 revisit 查询;它不是第二个爬取池。
  • 节点池低于 low_watermark 时触发 bootstrap。失败的 bootstrap 来源按配置退避。
  • UDP 回复总包数、总字节数和单来源包数分别限流,其中 10% 包/字节预算保留给 pingget_peers 的保底回复,但不会突破配置的总上限。

Metadata 调度

  • Hash ingress 和 Metadata pending queue 都是有界的。
  • Pending queue 按 InfoHash 去重,每个 Hash 最多保留三个不同且新鲜的 Peer。
  • Pending 项固定在 60 秒后过期。队列满时,较新的 Hash 可以淘汰最旧项;比当前 最旧项还旧的事件直接视为 stale。
  • worker 优先分派最新的可用 Hash,以提高 Peer 仍在线的概率。
  • timeout_secs 是一次 Peer 尝试的端到端期限,不会在连接、握手和下载阶段重复叠加。
  • 单个 metadata payload 上限为 10 MiB;下载完成后必须通过 SHA1 和 bencode 解析。
  • Peer failure cache 只缓存 timeoutconnect_failed,键为完整 SocketAddr (IP + port)。缓存命中不会发起网络请求,也不计入三次真实 Peer 尝试。
  • peer_failure_cache_capacity = 0peer_failure_ttl_secs = 0 会关闭缓存。

配置默认值

库本身不包含 P1/P15 等档位概念。应用如需档位,应将其转换成下列具体选项。

DHTOptions 与 Metadata

字段 默认值 说明
port 6881 DHT UDP 监听端口
netmode Ipv4Only DHTOptions::default() 的网络模式
hash_queue_capacity 10000 announce 到 Metadata scheduler 的 ingress 容量
metadata.timeout_secs 4 单 Peer 端到端超时
metadata.max_queue_size 10000 去重 Pending Hash 容量
metadata.max_worker_count 256 最大并发 Metadata job 数
metadata.peer_failure_cache_capacity 200000 坏 Peer 缓存容量
metadata.peer_failure_ttl_secs 60 坏 Peer 缓存 TTL

crawl.rate_limit

字段 默认值
max_find_node_rate_per_sec 200
burst 40
max_in_flight 512
request_timeout_secs 2
max_new_destinations_per_minute 10000
max_replacements_per_minute 25000
max_response_rate_per_sec 500
max_response_bytes_per_sec 1048576
max_response_rate_per_source 40
metadata_pressure_floor_percent 25
max_in_flight_per_subnet 8

Pool、Bootstrap、Target 与 Scheduler

字段 默认值
pool.capacity 100000
pool.recent_probe_ttl_secs 600
pool.responsive_capacity 16384
pool.responsive_ttl_secs 900
pool.low_watermark 10000
bootstrap.interval_secs 300
bootstrap.max_nodes_per_round 3
bootstrap.source_backoff_base_secs 300
bootstrap.source_backoff_max_secs 3600
target.random_walk_percent 70
target.sparse_bucket_percent 30
target.neighbor_sender_id true
scheduler.priority_event_channel_capacity 8192
scheduler.discovery_event_channel_capacity 16384
scheduler.event_batch_limit 256
scheduler.node_batch_limit 4096
scheduler.routing_snapshot_size 4096
scheduler.snapshot_refresh_millis 1000

默认 bootstrap 来源:

router.bittorrent.com:6881
dht.transmissionbt.com:6881
router.utorrent.com:6881
dht.aelitis.com:6881

内部会对不安全的零值和百分比做归一化,例如容量/在途至少为 1、百分比最大为 100、 low_watermark 不超过 pool capacity。建议调用方仍显式传入有效配置,不依赖归一化。

回调与生命周期

on_metadata_fetch

在 Hash 首次准备进入 Peer 下载前调用。返回 false 表示 gate reject:不下载、不触发 on_torrent,也不会触发 on_metadata_fetch_complete

on_torrenton_torrent_with_ack

  • on_torrent 适合无需确认交付的调用方;回调返回后视为 Accepted
  • on_torrent_with_ack 返回 true 表示应用接受交付,返回 false 表示 DeliveryRejected。后者代表 Metadata 已成功下载,但业务层没有接收,不等同于 FetchFailed
  • 如果没有注册任何 torrent callback,成功下载的 Metadata 同样按 DeliveryRejected 结束。
  • Torrent 回调发生 panic 时会被捕获并按拒绝交付处理。

on_metadata_fetch_complete

一个通过 gate 的 Hash 最终只发出一次完成通知:

状态 含义
Accepted 下载成功,torrent callback 接受交付
FetchFailed 所有可用 Peer 尝试失败
DeliveryRejected 下载成功,torrent callback 拒绝交付

attempts 只统计实际发起的 Peer 网络尝试;failure cache 命中不计入。

启动与停止

  • DHTServer::new() 创建并绑定所需 Socket,失败直接返回 Err
  • start() 启动后台任务并等待取消,因此通常应在应用主任务中 await。
  • shutdown() 可从 clone handle 调用,取消 DHT、crawl 和 Metadata 后台任务。
  • 已 shutdown 的同一实例不能重新 start;需要重新构造 DHTServer
  • on_error 用于接收运行期协议/worker 错误;初始化错误仍通过 Result 返回。

运行时观测

不启用 metrics feature 也可以读取运行时快照:

let stats = server.runtime_stats();

let runtime = stats.snapshot();
println!(
    "nodes={} metadata={}/{} in_flight={}",
    runtime.node_pool_size,
    runtime.metadata_queue_depth,
    runtime.metadata_queue_max,
    runtime.metadata_in_flight,
);

let observability = stats.observability_snapshot();
println!(
    "udp rx={}B tx={}B fetch_p95={:?}ms",
    observability.udp_rx_bytes,
    observability.udp_tx_bytes,
    observability.fetch_duration_ms.percentile(0.95),
);

快照使用 relaxed atomic load,适合监控,不是跨字段事务视图。计数器是进程生命周期 累计值,调用方通过相邻快照差值计算 rate,并应处理进程重启导致的 counter reset。

固定桶:

直方图 边界/单位
Metadata queue wait 10/50/100/250/500/1000/2000/5000 ms
Metadata fetch 250/500/1000/2000/4000/6000/10000 ms
Metadata payload size 16/32/64/128/256/512/1024 KiB, 10 MiB

counts[i] 表示小于等于 bounds[i] 的非累计桶计数,overflow 表示超过最后边界的 数量。percentile() 返回桶上界;落入 overflow 时只能返回最后一个上界,因此它是 有界近似值,不是精确分位数。

高级快照类型从 crate 根导出;当前 prelude 只重导出 DhtRuntimeStatsDhtRuntimeSnapshot

Prometheus / metrics

启用 metrics 后,库通过 metrics facade 记录 counter、gauge 和 histogram。 应用必须在创建/启动 server 前安装全局 recorder。示例:

use metrics_exporter_prometheus::PrometheusBuilder;

PrometheusBuilder::new()
    .with_http_listener("127.0.0.1:9000".parse().unwrap())
    .install()
    .unwrap();

完整名称、标签和单位见 docs/metrics.md

应用层集成

本 crate 只提供 DHT、BEP-9 Metadata、回调和观测能力,不包含 Redis、Manticore、 HTTP 看板或 P1P16 性能档位。同级的 dht-crawler-node 项目负责这些应用层策略, 并把档位转换成具体的 DHTOptions。开发两个项目时应保持下面的目录关系:

workspace-parent/
├── dht-crawler/
└── dht-crawler-node/

JNI

启用 jni feature 可构建 cdylib。Java 示例、线程模型和 JNI 配置字段见 examples-jni/README.md。Java DHTOptions 是 Rust 配置的 扁平化子集,未暴露的 Bootstrap、Target、Scheduler 和 Peer failure cache 字段使用 Rust 默认值。

从 0.1 迁移到 0.2

0.2 是 breaking release

0.1 0.2
metadata_timeout metadata.timeout_secs
max_metadata_queue_size metadata.max_queue_size
max_metadata_worker_count metadata.max_worker_count
node_queue_capacity crawl.pool.capacity
旧 active/candidate frontier 单所有者严格 FIFO pool + responsive ring
无交付确认 on_torrent_with_ack + DeliveryRejected
粗粒度统计 runtime_stats() 的两类原子快照和固定桶

DHTOptions 不提供旧字段兼容层,升级时必须修改构造代码。配置档位属于应用策略, 不在库内实现。

构建与验证

cargo fmt --all --check
cargo check --all-targets --all-features
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features
cargo doc --no-deps --all-features
cargo run --release --example dht_crawler_example

JNI

cargo build --release --features jni

许可证

MIT