jni
This commit is contained in:
@@ -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.so,Windows 产出 dht_crawler.dll,macOS 产出 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`。
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user