This commit is contained in:
桥下红药
2026-03-13 02:15:50 +08:00
parent 2ab9103e5b
commit 6e7c6f02e4
23 changed files with 1256 additions and 77 deletions
+93
View File
@@ -0,0 +1,93 @@
# dht-crawler-jni Java 示例
本目录是一个 Gradle 管理的 Java 项目,演示如何通过 JNI 使用 `dht-crawler` Rust 库。
## 项目结构
```
java/
├── build.gradle # Gradle 构建脚本
├── settings.gradle
└── src/main/java/cn/lmcw/dht/
├── model/
│ ├── TorrentInfo.java # 与 Rust TorrentInfo 一一对应
│ ├── FileInfo.java # 与 Rust FileInfo 一一对应
│ └── DHTOptions.java # 与 Rust DHTOptions 一一对应
├── DhtListener.java # 事件回调接口
├── DhtCrawlerJni.java # JNI 绑定类(native 方法声明)
└── DhtCrawlerExample.java # 可运行示例
```
## 快速开始
### 方式一:直接下载 Release 中的 JAR 运行(推荐)
从 GitHub Release 页面下载两个文件:
1. `dht-crawler-jni-example-<version>.jar` — 平台无关的 fat JAR
2. 对应平台的 JNI 动态库 zip(如 `dht_crawler_jni-<version>-x86_64-unknown-linux-gnu.zip`),解压得到 `libdht_crawler.so` / `dht_crawler.dll` / `libdht_crawler.dylib`
将 JAR 和动态库放到同一目录,然后执行:
```bash
# Linux / macOS
java -Djava.library.path=. -jar dht-crawler-jni-example-<version>.jar
# Windows(将 dht_crawler.dll 放在同目录)
java -Djava.library.path=. -jar dht-crawler-jni-example-<version>.jar
```
### 方式二:从源码编译并运行
#### 1. 编译 Rust JNI 动态库
在仓库**根目录**执行:
```bash
# 本机(Linux 产出 libdht_crawler.soWindows 产出 dht_crawler.dllmacOS 产出 libdht_crawler.dylib
cargo build --release --features jni
```
产物路径:`target/release/`
#### 2. 运行 Java 示例
在本目录(`jni/java/`)执行:
```bash
# 使用默认库路径(../../../target/release
gradle run
# 自定义库路径
gradle run -Plib.path=/path/to/your/lib
```
#### 3. 构建 fat JAR
```bash
gradle shadowJar
# 产物:build/libs/dht-crawler-jni-example-<version>.jar
```
## 在自己的项目中集成
1.`src/main/java/cn/lmcw/dht/` 下的文件复制到你的项目。
2. 将对应平台的 so/dll/dylib 放入 `java.library.path` 可访问的目录。
3. 确保 JVM 启动时加了 `-Djava.library.path=<路径>`
## JNI 生命周期
```java
DHTOptions options = new DHTOptions().setPort(6881);
long handle = DhtCrawlerJni.createServer(options, listener);
DhtCrawlerJni.startServer(handle);
// ... 运行 ...
DhtCrawlerJni.stopServer(handle);
DhtCrawlerJni.destroyServer(handle); // 必须调用,释放 Rust 资源
```
## 注意事项
- `destroyServer` 必须在不再使用后调用,否则 Rust 侧的 tokio runtime 和连接不会释放。
- 回调方法(`onTorrent``onError`)在 Rust 的 tokio 工作线程中触发,请确保实现是线程安全的。
- 同一个句柄不要在多个线程中并发调用 `destroyServer`
+39
View File
@@ -0,0 +1,39 @@
plugins {
id 'java'
id 'application'
id 'com.github.johnrengelman.shadow' version '8.1.1'
}
group = 'cn.lmcw.dht'
version = project.findProperty('projectVersion') ?: '0.1.0'
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
application {
mainClass = 'cn.lmcw.dht.DhtCrawlerExample'
def libPath = project.findProperty('lib.path') ?: '.'
applicationDefaultJvmArgs = ["-Djava.library.path=${libPath}"]
}
repositories {
mavenCentral()
}
// 运行时将 native 库路径传入 JVM
tasks.withType(JavaExec).configureEach {
def libPath = project.findProperty('lib.path') ?: '../../../target/release'
jvmArgs "-Djava.library.path=${libPath}"
}
// Fat JAR(含所有依赖 + Main-Class):执行 `gradle shadowJar` 生成
shadowJar {
archiveBaseName = 'dht-crawler-jni-example'
archiveClassifier = ''
archiveVersion = version
manifest {
attributes 'Main-Class': 'cn.lmcw.dht.DhtCrawlerExample'
}
}
+1
View File
@@ -0,0 +1 @@
rootProject.name = 'dht-crawler-jni'
@@ -0,0 +1,82 @@
package cn.lmcw.dht;
import cn.lmcw.dht.model.DHTOptions;
import cn.lmcw.dht.model.TorrentInfo;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicLong;
/**
* DHT-Crawler JNI 使用示例。
*
* <h3>运行前准备</h3>
* <ol>
* <li>在仓库根目录编译 Rust JNI 产物:
* <pre>cargo build --release --features jni</pre>
* </li>
* <li>在 {@code jni/java/} 目录下运行({@code lib.path} 指向 so/dll/dylib 所在目录):
* <pre>gradle run -Plib.path=../../target/release</pre>
* 或直接用默认路径({@code ../../../target/release}):
* <pre>gradle run</pre>
* </li>
* </ol>
*/
public class DhtCrawlerExample {
public static void main(String[] args) throws InterruptedException {
// 1. 构造配置
DHTOptions options = new DHTOptions()
.setPort(6881)
.setNetMode(0) // 0 = IPv4 Only
.setMetadataTimeout(5L)
.setMaxMetadataQueueSize(50_000)
.setMaxMetadataWorkerCount(500);
AtomicLong torrentCount = new AtomicLong();
CountDownLatch shutdown = new CountDownLatch(1);
// 2. 实现回调
DhtListener listener = new DhtListener() {
@Override
public void onTorrent(TorrentInfo info) {
long n = torrentCount.incrementAndGet();
System.out.printf("[#%d] %s (%s) files=%d%n",
n, info.getName(), info.getInfoHash(),
info.getFiles() != null ? info.getFiles().size() : 0);
}
@Override
public void onError(String message) {
System.err.println("[ERROR] " + message);
}
};
// 3. 创建并启动服务器
long handle = DhtCrawlerJni.createServer(options, listener);
if (handle == 0) {
System.err.println("创建服务器失败");
return;
}
System.out.println("DHT 服务器已创建,正在启动...");
DhtCrawlerJni.startServer(handle);
System.out.println("DHT 服务器已启动,端口 " + options.getPort());
// 4. 注册 JVM 关闭钩子,确保资源释放
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("\n收到退出信号,正在停止...");
DhtCrawlerJni.stopServer(handle);
try { Thread.sleep(500); } catch (InterruptedException ignored) {}
DhtCrawlerJni.destroyServer(handle);
System.out.println("服务器已销毁,共发现种子:" + torrentCount.get());
shutdown.countDown();
}));
// 5. 定期打印节点池大小
System.out.println("按 Ctrl+C 退出。每 10 秒打印一次节点池大小...");
while (true) {
Thread.sleep(10_000);
int poolSize = DhtCrawlerJni.getNodePoolSize(handle);
System.out.println("节点池大小: " + poolSize + " 已发现种子: " + torrentCount.get());
}
}
}
@@ -0,0 +1,68 @@
package cn.lmcw.dht;
import cn.lmcw.dht.model.DHTOptions;
/**
* Rust DHT-Crawler 库的 JNI 绑定入口。
*
* <p>所有方法均为 {@code static native},通过 JNI 调用 Rust 实现。
* 加载顺序由静态初始化块保证,使用前无需手动调用。</p>
*
* <h3>生命周期</h3>
* <pre>{@code
* long handle = DhtCrawlerJni.createServer(options, listener);
* DhtCrawlerJni.startServer(handle);
* // ... 运行中 ...
* DhtCrawlerJni.stopServer(handle);
* DhtCrawlerJni.destroyServer(handle); // 必须调用,否则 Rust 资源泄漏
* }</pre>
*/
public final class DhtCrawlerJni {
static {
System.loadLibrary("dht_crawler");
}
private DhtCrawlerJni() {}
/**
* 创建并初始化 DHT 服务器,返回 Rust 侧句柄。
*
* @param options 服务器配置,传 {@code null} 则使用 Rust 侧默认值
* @param listener 事件回调,传 {@code null} 则不注册任何回调
* @return 服务器句柄(非 0 表示成功),或 0(失败,同时会向 JVM 抛出异常)
*/
public static native long createServer(DHTOptions options, DhtListener listener);
/**
* 启动已创建的 DHT 服务器(非阻塞)。
* 服务器在 Rust tokio runtime 后台运行,此方法立即返回。
*
* @param handle {@link #createServer} 返回的句柄
*/
public static native void startServer(long handle);
/**
* 向服务器发送停止信号(非阻塞)。
* 调用后服务器将优雅退出,但资源尚未释放,需继续调用 {@link #destroyServer}。
*
* @param handle {@link #createServer} 返回的句柄
*/
public static native void stopServer(long handle);
/**
* 销毁服务器并释放所有 Rust 侧资源(tokio runtime、连接等)。
* <strong>必须调用</strong>,否则发生内存泄漏。调用后不得再使用该句柄。
*
* @param handle {@link #createServer} 返回的句柄
*/
public static native void destroyServer(long handle);
/**
* 获取当前节点池(routing table)中的节点数量。
*
* @param handle {@link #createServer} 返回的句柄
* @return 节点数量,句柄无效时返回 0
*/
public static native int getNodePoolSize(long handle);
}
@@ -0,0 +1,25 @@
package cn.lmcw.dht;
import cn.lmcw.dht.model.TorrentInfo;
/**
* DHT 爬虫事件回调接口。
* <p>实现此接口并传入 {@link DhtCrawlerJni#createServer} 即可接收事件通知。
* 回调在 Rust 内部的 tokio 工作线程触发,实现方需自行保证线程安全。</p>
*/
public interface DhtListener {
/**
* 成功获取到 torrent metadata 时触发。
*
* @param info 完整的 torrent 信息对象,由 Rust JNI 层映射构造
*/
void onTorrent(TorrentInfo info);
/**
* 内部发生错误时触发。
*
* @param message Rust 侧错误的文本描述
*/
void onError(String message);
}
@@ -0,0 +1,86 @@
package cn.lmcw.dht.model;
/**
* 与 Rust {@code DHTOptions} 一一对应的配置对象。
* <p>通过 JNI 传入 Rust 侧,由 Rust 读取各字段构造 {@code DHTOptions}。</p>
*
* <h3>netMode 取值</h3>
* <ul>
* <li>0 - IPv4 Only</li>
* <li>1 - IPv6 Only</li>
* <li>2 - Dual Stack(默认)</li>
* </ul>
*/
public final class DHTOptions {
/** 监听端口,默认 6881 */
private int port = 6881;
/** 获取 metadata 超时(秒),默认 3 */
private long metadataTimeout = 3L;
/** metadata 队列最大容量,默认 100000 */
private int maxMetadataQueueSize = 100_000;
/** 并发 metadata 拉取 worker 数量,默认 1000 */
private int maxMetadataWorkerCount = 1_000;
/** 节点队列容量,默认 100000 */
private int nodeQueueCapacity = 100_000;
/** hash 队列容量,默认 10000 */
private int hashQueueCapacity = 10_000;
/**
* 网络模式:0=IPv4Only, 1=IPv6Only, 2=DualStack(默认 0 / IPv4Only
* 与 Rust 侧 {@code NetMode::Ipv4Only} 对应)
*/
private int netMode = 0;
public DHTOptions() {}
public int getPort() { return port; }
public DHTOptions setPort(int port) { this.port = port; return this; }
public long getMetadataTimeout() { return metadataTimeout; }
public DHTOptions setMetadataTimeout(long metadataTimeout) {
this.metadataTimeout = metadataTimeout;
return this;
}
public int getMaxMetadataQueueSize() { return maxMetadataQueueSize; }
public DHTOptions setMaxMetadataQueueSize(int maxMetadataQueueSize) {
this.maxMetadataQueueSize = maxMetadataQueueSize;
return this;
}
public int getMaxMetadataWorkerCount() { return maxMetadataWorkerCount; }
public DHTOptions setMaxMetadataWorkerCount(int maxMetadataWorkerCount) {
this.maxMetadataWorkerCount = maxMetadataWorkerCount;
return this;
}
public int getNodeQueueCapacity() { return nodeQueueCapacity; }
public DHTOptions setNodeQueueCapacity(int nodeQueueCapacity) {
this.nodeQueueCapacity = nodeQueueCapacity;
return this;
}
public int getHashQueueCapacity() { return hashQueueCapacity; }
public DHTOptions setHashQueueCapacity(int hashQueueCapacity) {
this.hashQueueCapacity = hashQueueCapacity;
return this;
}
public int getNetMode() { return netMode; }
public DHTOptions setNetMode(int netMode) { this.netMode = netMode; return this; }
@Override
public String toString() {
return "DHTOptions{"
+ "port=" + port
+ ", metadataTimeout=" + metadataTimeout
+ ", netMode=" + netMode
+ '}';
}
}
@@ -0,0 +1,30 @@
package cn.lmcw.dht.model;
/**
* 与 Rust {@code FileInfo} 一一对应的 POJO。
* <p>表示一个种子内单个文件的路径与大小。</p>
*/
public final class FileInfo {
private final String path;
private final long size;
/**
* 供 JNI 层调用的全参构造器。
*
* @param path 文件相对路径
* @param size 文件字节数
*/
public FileInfo(String path, long size) {
this.path = path;
this.size = size;
}
public String getPath() { return path; }
public long getSize() { return size; }
@Override
public String toString() {
return "FileInfo{path='" + path + "', size=" + size + '}';
}
}
@@ -0,0 +1,70 @@
package cn.lmcw.dht.model;
import java.util.List;
/**
* 与 Rust {@code TorrentInfo} 一一对应的 POJO。
* <p>由 Rust JNI 层通过 {@code NewObject} / {@code SetField} 构造并填充,
* 通过 {@link cn.lmcw.dht.DhtListener#onTorrent(TorrentInfo)} 回调给 Java 层。</p>
*/
public final class TorrentInfo {
private final String infoHash;
private final String magnetLink;
private final String name;
private final long totalSize;
private final List<FileInfo> files;
private final long pieceLength;
private final List<String> peers;
private final long timestamp;
/**
* 供 JNI 层调用的全参构造器。
*
* @param infoHash 十六进制 info-hash 字符串
* @param magnetLink magnet 链接
* @param name 种子名称
* @param totalSize 总字节数
* @param files 文件列表
* @param pieceLength 分片长度(字节)
* @param peers 来源节点地址列表
* @param timestamp 发现时间戳(Unix 秒)
*/
public TorrentInfo(
String infoHash,
String magnetLink,
String name,
long totalSize,
List<FileInfo> files,
long pieceLength,
List<String> peers,
long timestamp) {
this.infoHash = infoHash;
this.magnetLink = magnetLink;
this.name = name;
this.totalSize = totalSize;
this.files = files;
this.pieceLength = pieceLength;
this.peers = peers;
this.timestamp = timestamp;
}
public String getInfoHash() { return infoHash; }
public String getMagnetLink() { return magnetLink; }
public String getName() { return name; }
public long getTotalSize() { return totalSize; }
public List<FileInfo> getFiles() { return files; }
public long getPieceLength() { return pieceLength; }
public List<String> getPeers() { return peers; }
public long getTimestamp() { return timestamp; }
@Override
public String toString() {
return "TorrentInfo{"
+ "infoHash='" + infoHash + '\''
+ ", name='" + name + '\''
+ ", totalSize=" + totalSize
+ ", files=" + (files != null ? files.size() : 0)
+ '}';
}
}