Merge pull request #3 from 0xddy/codex/jni-boundary-cleanup-20260730

fix: harden JNI boundary and remove legacy example
This commit is contained in:
桥下红药
2026-07-30 14:32:07 +08:00
committed by GitHub
22 changed files with 262 additions and 1079 deletions
-32
View File
@@ -12,38 +12,6 @@ env:
CARGO_TERM_COLOR: always
jobs:
# ──────────────────────────────────────────────────────────────────
# Java fat JAR(平台无关,只需构建一次)
# 运行方式:java -Djava.library.path=<so/dll 目录> -jar dht-crawler-jni-example-<ver>.jar
# ──────────────────────────────────────────────────────────────────
build-java-jar:
name: Build Java Example JAR
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- name: Build fat JAR
working-directory: examples-jni
run: |
chmod +x gradlew
./gradlew shadowJar -PprojectVersion=${{ github.event.inputs.version }} --no-daemon
shell: bash
- name: Upload JAR to Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ github.event.inputs.version }}
files: examples-jni/build/libs/dht-crawler-jni-example-${{ github.event.inputs.version }}.jar
# ──────────────────────────────────────────────────────────────────
# 各平台 Rust 构建(example 可执行 + JNI so/dll/dylib
# ──────────────────────────────────────────────────────────────────
+2 -1
View File
@@ -59,6 +59,7 @@ jobs:
- "mimalloc"
- "metrics"
- "mimalloc,metrics"
- "jni"
steps:
- uses: actions/checkout@v4
- name: Install Rust
@@ -88,4 +89,4 @@ jobs:
cargo build --examples --verbose
else
cargo build --examples --verbose --features "${{ matrix.features }}"
fi
fi
-4
View File
@@ -18,7 +18,3 @@ torrents/
# 个人脚本(不提交到仓库)
scripts/
# Java / Gradleexamples-jni
examples-jni/build/
examples-jni/.gradle/
+2 -1
View File
@@ -244,7 +244,8 @@ dht-crawler = { version = "0.2", features = ["metrics"] }
| `jni` | 构建 Java JNI 接口和 `cdylib` |
| `mimalloc` | 将 mimalloc 注册为全局分配器 |
JNI 的构建方式和 Java 示例见 [examples-jni/README.md](examples-jni/README.md)。
Java/Kotlin 封装、平台 native JAR 和示例由独立项目
[`dht-crawler-java`](https://github.com/0xddy/dht-crawler-java) 维护。
启用 `mimalloc` 前,请确认最终二进制没有注册其他全局分配器。
## 开发
-124
View File
@@ -1,124 +0,0 @@
# dht-crawler JNI Java 示例
Gradle 项目,演示通过 JNI 调用 `dht-crawler`(需启用 Cargo feature `jni`)。
## 项目结构
```
examples-jni/
├── build.gradle
├── settings.gradle
└── src/main/java/cn/lmcw/dht/
├── model/ # DHTOptions, TorrentInfo, FileInfo
├── DhtCrawler.java # 面向对象入口(推荐)
├── DhtCrawlerJni.java # native 方法声明
├── DhtListener.java # 回调接口
└── DhtCrawlerExample.java
```
Rust JNI 实现位于 **`dht-crawler/jni/`**`lib` crate-type 含 `cdylib`)。
## 编译 native 库
**`dht-crawler`** 目录(本 README 的上一级)执行:
```bash
cargo build --release --features jni
```
产物路径(因平台而异):
- Linux: `target/release/libdht_crawler.so`
- Windows: `target/release/dht_crawler.dll`
- macOS: `target/release/libdht_crawler.dylib`
## 运行示例
### 方式一:Release 预编译包(推荐)
下载 Release 中的 fat JAR 与对应平台 native 库 zip,放在同一目录:
```bash
# Linux / macOS
java -Djava.library.path=. -jar dht-crawler-jni-example-<version>.jar
# Windows
java "-Djava.library.path=." -jar dht-crawler-jni-example-<version>.jar
```
### 方式二:源码 + Gradle
```bash
cd examples-jni
gradle run
# 指定 native 库目录(默认为 ../target/release
gradle run -Plib.path=/path/to/lib
```
构建 fat JAR
```bash
gradle shadowJar
```
## 集成到自己的 Java 项目
1. 复制 `cn/lmcw/dht/` 包(含 `model/``DhtCrawler``DhtCrawlerJni``DhtListener`)。
2. 将对应平台的 native 库加入 `java.library.path`
3. 使用与示例相同的 `dht-crawler` JNI 版本构建 `cdylib`
## API(面向对象)
```java
DhtCrawler crawler = DhtCrawler.createServer(options, listener);
crawler.start(); // 后台启动,不阻塞调用线程
// ...
crawler.stop(); // 或 try-with-resources
```
| 方法 | 说明 |
|------|------|
| `createServer(options, listener)` | 创建 `ServerHandle`(含独立 tokio `Runtime` + `DHTServer` |
| `start()` | 在 runtime 内 spawn `server.start()`;同一会话多次调用仅首次生效 |
| `stop()` / `close()` | `shutdown()` 后在后台线程 drop Runtime,避免阻塞 JNI 线程 |
| `getNodePoolSize()` | 节点池大小(`DHTServer::get_node_pool_size` |
Native 导出类:`cn.lmcw.dht.DhtCrawlerJni``createServer` / `startServer` / `stopServer` / `getNodePoolSize`)。
## 回调与线程
- `onTorrent` / `onError`:在 Rust 工作线程触发,Java 实现须线程安全。
- `onMetadataFetch`:在阻塞线程池中调用,应尽快返回 boolean。
行为与 Rust 库一致:InfoHash 来自 **`announce_peer`**`start()` 在 Rust 侧仍阻塞至 `shutdown()`JNI 通过单独 runtime + `spawn` 避免卡住 Java 主流程。
当前 JNI listener 暴露 `onTorrent``onMetadataFetch``onError`,不暴露 Rust
`on_torrent_with_ack` / `on_metadata_fetch_complete`。因此 Java `onTorrent` 返回后始终按
`Accepted` 处理;需要交付确认和最终状态的应用应扩展 JNI callback contract。
## JNI 配置映射
Java `DHTOptions` 是 Rust 配置的扁平化子集,不是全部 Rust 字段的一一镜像:
| Java 字段组 | Rust 目标 |
|---|---|
| port / netMode / hashQueueCapacity | `DHTOptions` 顶层 |
| metadataTimeout / maxMetadataQueueSize / maxMetadataWorkerCount | `metadata.*` |
| poolCapacity / recentProbeTtlSeconds / responsive* / poolLowWatermark | `crawl.pool.*` |
| findNode* / requestTimeout* / response* / pressure / replacements / subnet | `crawl.rate_limit.*` |
以下配置未通过当前 JNI 暴露,使用 Rust `Default`
- `metadata.peer_failure_cache_capacity``metadata.peer_failure_ttl_secs`
- `crawl.bootstrap.*`
- `crawl.target.*`
- `crawl.scheduler.*`
Java 字段默认值与当前 Rust 0.2 默认值保持一致;传入 `null` options 时直接使用完整的
`DHTOptions::default()`
## 注意事项
- Java 11+(见 `build.gradle`)。
- Java/Rust 版本必须一致,避免 JNI 按字段名和签名读取时失败。
- 停止后勿再使用同一 `long` 句柄。
-39
View File
@@ -1,39 +0,0 @@
plugins {
id 'java'
id 'application'
id 'com.gradleup.shadow' version '9.0.2'
}
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(默认指向仓库根 target/release
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'
}
}
Binary file not shown.
-7
View File
@@ -1,7 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-248
View File
@@ -1,248 +0,0 @@
#!/bin/sh
#
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
-84
View File
@@ -1,84 +0,0 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem having the script exit with the same return code as the Gradle build
if not ""=="%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
-1
View File
@@ -1 +0,0 @@
rootProject.name = 'dht-crawler-jni'
@@ -1,100 +0,0 @@
package cn.lmcw.dht;
import cn.lmcw.dht.model.DHTOptions;
/**
* DHT 爬虫会话:封装 native 句柄与生命周期,推荐使用的面向对象入口。
*
* <pre>{@code
* try (DhtCrawler crawler = DhtCrawler.createServer(options, listener)) {
* crawler.start();
* // ... 运行 ...
* } // close → stop,释放 Rust 资源
*
* // 或手动:
* DhtCrawler c = DhtCrawler.createServer(options, listener);
* c.start();
* // ...
* c.stop();
* }</pre>
*/
public final class DhtCrawler implements AutoCloseable {
private long handle;
private volatile boolean started;
private DhtCrawler(long handle) {
this.handle = handle;
}
/**
* 创建 DHT 服务器会话(尚未 {@link #start()},与 native createServer 对应)。
* Java 中方法不能命名为 {@code new},故用 {@code createServer}。
*
* @param options 配置,{@code null} 使用 Rust 默认
* @param listener 回调,{@code null} 不注册回调
* @return 已绑定 native 资源的会话
* @throws IllegalStateException 创建失败(句柄为 0,通常伴随 JVM 异常)
*/
public static DhtCrawler createServer(DHTOptions options, DhtListener listener) {
long h = DhtCrawlerJni.createServer(options, listener);
if (h == 0) {
throw new IllegalStateException("DHT 服务器创建失败(createServer 返回 0");
}
return new DhtCrawler(h);
}
/**
* 在后台启动 DHT(非阻塞)。可多次调用,仅首次生效。
*
* @throws IllegalStateException 已 {@link #stop()} 关闭后不能再启动
*/
public synchronized void start() {
if (handle == 0) {
throw new IllegalStateException("会话已关闭,无法 start");
}
if (started) {
return;
}
DhtCrawlerJni.startServer(handle);
started = true;
}
/**
* 停止并释放全部 Rust 资源(tokio runtime 等)。幂等:重复调用安全。
*/
public synchronized void stop() {
if (handle == 0) {
return;
}
DhtCrawlerJni.stopServer(handle);
handle = 0;
started = false;
}
/** 是否已调用过 {@link #start()} 且尚未 {@link #stop()}。 */
public synchronized boolean isStarted() {
return started && handle != 0;
}
/** 会话仍有效(未 stop)时为 true。 */
public synchronized boolean isOpen() {
return handle != 0;
}
/**
* 当前 routing table 节点数;已关闭时返回 0。
*/
public int getNodePoolSize() {
long h = handle;
if (h == 0) {
return 0;
}
return DhtCrawlerJni.getNodePoolSize(h);
}
@Override
public void close() {
stop();
}
}
@@ -1,75 +0,0 @@
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 examples-jni/} 目录下运行({@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 {
DHTOptions options = new DHTOptions()
.setPort(6881)
.setNetMode(0)
.setMetadataTimeout(5L)
.setMaxMetadataQueueSize(50_000)
.setMaxMetadataWorkerCount(500);
AtomicLong torrentCount = new AtomicLong();
CountDownLatch shutdown = new CountDownLatch(1);
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);
}
@Override
public boolean onMetadataFetch(String infoHash) {
return true;
}
};
final DhtCrawler crawler = DhtCrawler.createServer(options, listener);
crawler.start();
System.out.println("DHT 已启动,端口 " + options.getPort());
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("\n收到退出信号,正在停止...");
crawler.stop();
System.out.println("已关闭,共发现种子:" + torrentCount.get());
shutdown.countDown();
}));
System.out.println("按 Ctrl+C 退出。每 10 秒打印一次节点池大小...");
while (true) {
Thread.sleep(10_000);
System.out.println("节点池: " + crawler.getNodePoolSize()
+ " 已发现种子: " + torrentCount.get());
}
}
}
@@ -1,23 +0,0 @@
package cn.lmcw.dht;
import cn.lmcw.dht.model.DHTOptions;
/**
* 包内 JNI 绑定(由 {@link DhtCrawler} 使用)。业务代码请用 {@link DhtCrawler}。
*/
final class DhtCrawlerJni {
static {
System.loadLibrary("dht_crawler");
}
private DhtCrawlerJni() {}
static native long createServer(DHTOptions options, DhtListener listener);
static native void startServer(long handle);
static native void stopServer(long handle);
static native int getNodePoolSize(long handle);
}
@@ -1,38 +0,0 @@
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);
/**
* 在即将对某 info_hash 拉取 BitTorrent metadata 之前调用。
* 与 Rust {@code DHTServer::on_metadata_fetch} 对应:返回 {@code true} 才会真正去拉取;
* 返回 {@code false} 则跳过该 hash,不会触发 {@link #onTorrent}。
* <p>在 Rust 的 metadata 工作线程中通过阻塞线程池调用,应快速返回,避免长时间阻塞。</p>
*
* @param infoHash 40 字符小写十六进制 info_hash
* @return 是否允许拉取 metadata
*/
default boolean onMetadataFetch(String infoHash) {
return true;
}
}
@@ -1,144 +0,0 @@
package cn.lmcw.dht.model;
/**
* Rust {@code DHTOptions} 的 JNI 扁平化子集。
* <p>通过 JNI 传入 Rust 侧;未暴露的 Metadata failure cache、Bootstrap、Target 和
* Scheduler 字段采用 Rust 0.2 默认值。Java 与 native 库版本必须保持一致。</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 = 4L;
/** metadata 队列最大容量,默认 100000 */
private int maxMetadataQueueSize = 10_000;
/** 并发 metadata 拉取 worker 数量,默认 1000 */
private int maxMetadataWorkerCount = 256;
/** 节点队列容量,默认 100000 */
private int poolCapacity = 100_000;
private int findNodeRatePerSecond = 200;
private int findNodeBurst = 40;
private int maxFindNodeInFlight = 512;
private int maxNewDestinationsPerMinute = 10_000;
private int maxReplacementsPerMinute = 25_000;
private long requestTimeoutSeconds = 2L;
private int maxResponseRatePerSecond = 500;
private long maxResponseBytesPerSecond = 1_048_576L;
private int maxResponseRatePerSource = 40;
private int metadataPressureFloorPercent = 25;
private long recentProbeTtlSeconds = 600L;
private int responsiveCapacity = 16_384;
private long responsiveTtlSeconds = 900L;
private int poolLowWatermark = 10_000;
private int maxInFlightPerSubnet = 8;
/** 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 getPoolCapacity() { return poolCapacity; }
public DHTOptions setPoolCapacity(int poolCapacity) {
this.poolCapacity = poolCapacity;
return this;
}
public int getFindNodeRatePerSecond() { return findNodeRatePerSecond; }
public DHTOptions setFindNodeRatePerSecond(int value) { this.findNodeRatePerSecond = value; return this; }
public int getFindNodeBurst() { return findNodeBurst; }
public DHTOptions setFindNodeBurst(int value) { this.findNodeBurst = value; return this; }
public int getMaxFindNodeInFlight() { return maxFindNodeInFlight; }
public DHTOptions setMaxFindNodeInFlight(int value) { this.maxFindNodeInFlight = value; return this; }
public int getMaxNewDestinationsPerMinute() { return maxNewDestinationsPerMinute; }
public DHTOptions setMaxNewDestinationsPerMinute(int value) { this.maxNewDestinationsPerMinute = value; return this; }
public int getMaxReplacementsPerMinute() { return maxReplacementsPerMinute; }
public DHTOptions setMaxReplacementsPerMinute(int value) { this.maxReplacementsPerMinute = value; return this; }
public long getRequestTimeoutSeconds() { return requestTimeoutSeconds; }
public DHTOptions setRequestTimeoutSeconds(long value) { this.requestTimeoutSeconds = value; return this; }
public int getMaxResponseRatePerSecond() { return maxResponseRatePerSecond; }
public DHTOptions setMaxResponseRatePerSecond(int value) { this.maxResponseRatePerSecond = value; return this; }
public long getMaxResponseBytesPerSecond() { return maxResponseBytesPerSecond; }
public DHTOptions setMaxResponseBytesPerSecond(long value) { this.maxResponseBytesPerSecond = value; return this; }
public int getMaxResponseRatePerSource() { return maxResponseRatePerSource; }
public DHTOptions setMaxResponseRatePerSource(int value) { this.maxResponseRatePerSource = value; return this; }
public int getMetadataPressureFloorPercent() { return metadataPressureFloorPercent; }
public DHTOptions setMetadataPressureFloorPercent(int value) { this.metadataPressureFloorPercent = value; return this; }
public long getRecentProbeTtlSeconds() { return recentProbeTtlSeconds; }
public DHTOptions setRecentProbeTtlSeconds(long value) { this.recentProbeTtlSeconds = value; return this; }
public int getResponsiveCapacity() { return responsiveCapacity; }
public DHTOptions setResponsiveCapacity(int value) { this.responsiveCapacity = value; return this; }
public long getResponsiveTtlSeconds() { return responsiveTtlSeconds; }
public DHTOptions setResponsiveTtlSeconds(long value) { this.responsiveTtlSeconds = value; return this; }
public int getPoolLowWatermark() { return poolLowWatermark; }
public DHTOptions setPoolLowWatermark(int value) { this.poolLowWatermark = value; return this; }
public int getMaxInFlightPerSubnet() { return maxInFlightPerSubnet; }
public DHTOptions setMaxInFlightPerSubnet(int value) { this.maxInFlightPerSubnet = value; 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
+ '}';
}
}
@@ -1,30 +0,0 @@
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 + '}';
}
}
@@ -1,70 +0,0 @@
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)
+ '}';
}
}
+5 -4
View File
@@ -58,6 +58,7 @@ fn register_on_error(server: &Arc<DHTServer>, callback: JavaCallback) {
/// 注册 on_metadata_fetch:在拉取 metadata 前询问 Java `onMetadataFetch(infoHash)`。
/// 返回 `true` 才继续拉取;在阻塞线程池中执行 JNI,避免阻塞 tokio worker。
/// JNI 调用或阻塞任务失败时按拒绝处理。
fn register_on_metadata_fetch(server: &Arc<DHTServer>, callback: JavaCallback) {
server.on_metadata_fetch(move |info_hash| {
let cb = callback.clone();
@@ -78,12 +79,12 @@ fn register_on_metadata_fetch(server: &Arc<DHTServer>, callback: JavaCallback) {
match r {
Ok(Ok(allow)) => allow,
Ok(Err(e)) => {
log::error!("回调 onMetadataFetch 失败: {e}默认允许拉取");
true
log::error!("回调 onMetadataFetch 失败: {e}拒绝拉取");
false
}
Err(e) => {
log::error!("onMetadataFetch spawn_blocking 失败: {e}默认允许拉取");
true
log::error!("onMetadataFetch spawn_blocking 失败: {e}拒绝拉取");
false
}
}
}
+3 -3
View File
@@ -12,10 +12,10 @@ use jni::sys::{jint, jlong};
/// 创建 DHTServer 并返回句柄(jlong)。
///
/// Java 签名:`native long createServer(DHTOptions options, DhtListener listener);`
/// JVM 签名:`native long createServer(Object options, Object listener);`
///
/// - `options``cn.lmcw.dht.model.DHTOptions` 对象,或 null 则使用默认选项。
/// - `listener``cn.lmcw.dht.DhtListener` 实现,或 null 则不注册回调
/// - `options`包含 native 配置字段的 DTO,或 null 则使用默认选项。
/// - `listener`实现 `onTorrent`、`onError` 和 `onMetadataFetch` 的对象,或 null
/// - 返回:服务器句柄(成功)或 0(失败)。
#[unsafe(no_mangle)]
pub extern "system" fn Java_cn_lmcw_dht_DhtCrawlerJni_createServer(
+249 -50
View File
@@ -3,6 +3,76 @@ use jni::JNIEnv;
use jni::objects::{JObject, JString, JValue};
use jni::sys::jlong;
#[derive(Debug, thiserror::Error)]
pub enum DhtOptionsConversionError {
#[error(transparent)]
Jni(#[from] jni::errors::Error),
#[error("invalid DHTOptions.{field}: expected {expected}, got {value}")]
InvalidValue {
field: &'static str,
expected: &'static str,
value: i64,
},
}
type DhtOptionsResult<T> = Result<T, DhtOptionsConversionError>;
fn invalid_value(
field: &'static str,
expected: &'static str,
value: impl Into<i64>,
) -> DhtOptionsConversionError {
DhtOptionsConversionError::InvalidValue {
field,
expected,
value: value.into(),
}
}
fn checked_port(value: i32) -> DhtOptionsResult<u16> {
u16::try_from(value).map_err(|_| invalid_value("port", "an integer in 0..=65535", value))
}
fn checked_non_negative_u32(field: &'static str, value: i32) -> DhtOptionsResult<u32> {
u32::try_from(value).map_err(|_| invalid_value(field, "a non-negative integer", value))
}
fn checked_non_negative_u64(field: &'static str, value: i64) -> DhtOptionsResult<u64> {
u64::try_from(value).map_err(|_| invalid_value(field, "a non-negative integer", value))
}
fn checked_non_negative_usize(field: &'static str, value: i32) -> DhtOptionsResult<usize> {
usize::try_from(value).map_err(|_| invalid_value(field, "a non-negative integer", value))
}
fn checked_positive_usize(field: &'static str, value: i32) -> DhtOptionsResult<usize> {
let value = checked_non_negative_usize(field, value)?;
if value == 0 {
return Err(invalid_value(
field,
"an integer greater than or equal to 1",
0,
));
}
Ok(value)
}
fn checked_percentage(field: &'static str, value: i32) -> DhtOptionsResult<u8> {
if !(0..=100).contains(&value) {
return Err(invalid_value(field, "an integer in 0..=100", value));
}
Ok(u8::try_from(value).expect("0..=100 always fits in u8"))
}
fn checked_netmode(value: i32) -> DhtOptionsResult<NetMode> {
match value {
0 => Ok(NetMode::Ipv4Only),
1 => Ok(NetMode::Ipv6Only),
2 => Ok(NetMode::DualStack),
_ => Err(invalid_value("netMode", "one of 0, 1, or 2", value)),
}
}
// ──────────────────────────────────────────────────────────────────────────────
// 常量:Java 类全限定名
// ──────────────────────────────────────────────────────────────────────────────
@@ -32,14 +102,15 @@ pub fn file_info_to_java<'local>(
env: &mut JNIEnv<'local>,
fi: &FileInfo,
) -> jni::errors::Result<JObject<'local>> {
let cls = env.find_class(CLASS_FILE_INFO)?;
let path = rust_str_to_jstring(env, &fi.path)?;
let obj = env.new_object(
&cls,
"(Ljava/lang/String;J)V",
&[JValue::Object(&path), JValue::Long(fi.size as jlong)],
)?;
Ok(obj)
env.with_local_frame_returning_local(4, |env| {
let cls = env.find_class(CLASS_FILE_INFO)?;
let path = rust_str_to_jstring(env, &fi.path)?;
env.new_object(
&cls,
"(Ljava/lang/String;J)V",
&[JValue::Object(&path), JValue::Long(fi.size as jlong)],
)
})
}
// ──────────────────────────────────────────────────────────────────────────────
@@ -125,40 +196,99 @@ fn build_string_list<'local>(
// DHTOptions: Java → Rust
// ──────────────────────────────────────────────────────────────────────────────
/// 从 Java `cn.lmcw.dht.model.DHTOptions` 对象读取字段,构造 Rust `DHTOptions`。
pub fn java_to_dht_options(env: &mut JNIEnv, obj: &JObject) -> jni::errors::Result<DHTOptions> {
let port = env.get_field(obj, "port", "I")?.i()? as u16;
let metadata_timeout_secs = env.get_field(obj, "metadataTimeout", "J")?.j()? as u64;
let metadata_max_queue_size = env.get_field(obj, "maxMetadataQueueSize", "I")?.i()? as usize;
let metadata_max_worker_count =
env.get_field(obj, "maxMetadataWorkerCount", "I")?.i()? as usize;
let pool_capacity = env.get_field(obj, "poolCapacity", "I")?.i()? as usize;
let find_node_rate = env.get_field(obj, "findNodeRatePerSecond", "I")?.i()? as u32;
let find_node_burst = env.get_field(obj, "findNodeBurst", "I")?.i()? as u32;
let max_find_node_in_flight = env.get_field(obj, "maxFindNodeInFlight", "I")?.i()? as usize;
let max_new_destinations = env
.get_field(obj, "maxNewDestinationsPerMinute", "I")?
.i()? as u32;
let max_replacements = env.get_field(obj, "maxReplacementsPerMinute", "I")?.i()? as u32;
let request_timeout_secs = env.get_field(obj, "requestTimeoutSeconds", "J")?.j()? as u64;
let max_response_rate = env.get_field(obj, "maxResponseRatePerSecond", "I")?.i()? as u32;
let max_response_bytes = env.get_field(obj, "maxResponseBytesPerSecond", "J")?.j()? as u64;
let max_response_per_source = env.get_field(obj, "maxResponseRatePerSource", "I")?.i()? as u32;
let pressure_floor = env
.get_field(obj, "metadataPressureFloorPercent", "I")?
.i()? as u8;
let recent_probe_ttl = env.get_field(obj, "recentProbeTtlSeconds", "J")?.j()? as u64;
let responsive_capacity = env.get_field(obj, "responsiveCapacity", "I")?.i()? as usize;
let responsive_ttl = env.get_field(obj, "responsiveTtlSeconds", "J")?.j()? as u64;
let low_watermark = env.get_field(obj, "poolLowWatermark", "I")?.i()? as usize;
let subnet_in_flight = env.get_field(obj, "maxInFlightPerSubnet", "I")?.i()? as usize;
let hash_queue_capacity = env.get_field(obj, "hashQueueCapacity", "I")?.i()? as usize;
let netmode_ord = env.get_field(obj, "netMode", "I")?.i()?;
let netmode = match netmode_ord {
0 => NetMode::Ipv4Only,
1 => NetMode::Ipv6Only,
_ => NetMode::DualStack,
};
/// 从 JVM bindings 的 options DTO 读取字段,构造 Rust `DHTOptions`。
pub fn java_to_dht_options(env: &mut JNIEnv, obj: &JObject) -> DhtOptionsResult<DHTOptions> {
let port = checked_port(env.get_field(obj, "port", "I")?.i()?)?;
let metadata_timeout_secs = checked_non_negative_u64(
"metadataTimeout",
env.get_field(obj, "metadataTimeout", "J")?.j()?,
)?;
let metadata_max_queue_size = checked_positive_usize(
"maxMetadataQueueSize",
env.get_field(obj, "maxMetadataQueueSize", "I")?.i()?,
)?;
let metadata_max_worker_count = checked_positive_usize(
"maxMetadataWorkerCount",
env.get_field(obj, "maxMetadataWorkerCount", "I")?.i()?,
)?;
let pool_capacity = checked_positive_usize(
"poolCapacity",
env.get_field(obj, "poolCapacity", "I")?.i()?,
)?;
let find_node_rate = checked_non_negative_u32(
"findNodeRatePerSecond",
env.get_field(obj, "findNodeRatePerSecond", "I")?.i()?,
)?;
let find_node_burst = checked_non_negative_u32(
"findNodeBurst",
env.get_field(obj, "findNodeBurst", "I")?.i()?,
)?;
let max_find_node_in_flight = checked_positive_usize(
"maxFindNodeInFlight",
env.get_field(obj, "maxFindNodeInFlight", "I")?.i()?,
)?;
let max_new_destinations = checked_non_negative_u32(
"maxNewDestinationsPerMinute",
env.get_field(obj, "maxNewDestinationsPerMinute", "I")?
.i()?,
)?;
let max_replacements = checked_non_negative_u32(
"maxReplacementsPerMinute",
env.get_field(obj, "maxReplacementsPerMinute", "I")?.i()?,
)?;
let request_timeout_secs = checked_non_negative_u64(
"requestTimeoutSeconds",
env.get_field(obj, "requestTimeoutSeconds", "J")?.j()?,
)?;
let max_response_rate = checked_non_negative_u32(
"maxResponseRatePerSecond",
env.get_field(obj, "maxResponseRatePerSecond", "I")?.i()?,
)?;
let max_response_bytes = checked_non_negative_u64(
"maxResponseBytesPerSecond",
env.get_field(obj, "maxResponseBytesPerSecond", "J")?.j()?,
)?;
let max_response_per_source = checked_non_negative_u32(
"maxResponseRatePerSource",
env.get_field(obj, "maxResponseRatePerSource", "I")?.i()?,
)?;
let pressure_floor = checked_percentage(
"metadataPressureFloorPercent",
env.get_field(obj, "metadataPressureFloorPercent", "I")?
.i()?,
)?;
let recent_probe_ttl = checked_non_negative_u64(
"recentProbeTtlSeconds",
env.get_field(obj, "recentProbeTtlSeconds", "J")?.j()?,
)?;
let responsive_capacity = checked_positive_usize(
"responsiveCapacity",
env.get_field(obj, "responsiveCapacity", "I")?.i()?,
)?;
let responsive_ttl = checked_non_negative_u64(
"responsiveTtlSeconds",
env.get_field(obj, "responsiveTtlSeconds", "J")?.j()?,
)?;
let low_watermark = checked_non_negative_usize(
"poolLowWatermark",
env.get_field(obj, "poolLowWatermark", "I")?.i()?,
)?;
if low_watermark > pool_capacity {
return Err(invalid_value(
"poolLowWatermark",
"an integer no greater than poolCapacity",
i64::try_from(low_watermark).expect("Java int always fits in i64"),
));
}
let subnet_in_flight = checked_positive_usize(
"maxInFlightPerSubnet",
env.get_field(obj, "maxInFlightPerSubnet", "I")?.i()?,
)?;
let hash_queue_capacity = checked_positive_usize(
"hashQueueCapacity",
env.get_field(obj, "hashQueueCapacity", "I")?.i()?,
)?;
let netmode = checked_netmode(env.get_field(obj, "netMode", "I")?.i()?)?;
let mut options = DHTOptions {
port,
@@ -172,33 +302,102 @@ pub fn java_to_dht_options(env: &mut JNIEnv, obj: &JObject) -> jni::errors::Resu
},
..DHTOptions::default()
};
options.crawl.pool.capacity = pool_capacity.max(1);
options.crawl.pool.capacity = pool_capacity;
options.crawl.pool.recent_probe_ttl_secs = recent_probe_ttl;
options.crawl.pool.responsive_capacity = responsive_capacity.max(1);
options.crawl.pool.responsive_capacity = responsive_capacity;
options.crawl.pool.responsive_ttl_secs = responsive_ttl;
options.crawl.pool.low_watermark = low_watermark.min(pool_capacity);
options.crawl.pool.low_watermark = low_watermark;
options.crawl.rate_limit.max_find_node_rate_per_sec = find_node_rate;
options.crawl.rate_limit.burst = find_node_burst;
options.crawl.rate_limit.max_in_flight = max_find_node_in_flight.max(1);
options.crawl.rate_limit.max_in_flight = max_find_node_in_flight;
options.crawl.rate_limit.max_new_destinations_per_minute = max_new_destinations;
options.crawl.rate_limit.request_timeout_secs = request_timeout_secs;
options.crawl.rate_limit.max_response_rate_per_sec = max_response_rate;
options.crawl.rate_limit.max_response_bytes_per_sec = max_response_bytes;
options.crawl.rate_limit.max_response_rate_per_source = max_response_per_source;
options.crawl.rate_limit.metadata_pressure_floor_percent = pressure_floor.min(100);
options.crawl.rate_limit.metadata_pressure_floor_percent = pressure_floor;
options.crawl.rate_limit.max_replacements_per_minute = max_replacements;
options.crawl.rate_limit.max_in_flight_per_subnet = subnet_in_flight.max(1);
options.crawl.rate_limit.max_in_flight_per_subnet = subnet_in_flight;
Ok(options)
}
/// 从 Java `cn.lmcw.dht.model.DHTOptions` 对象读取,或若为 null 则返回默认选项。
/// 从 JVM bindings 的 options DTO 读取,或若为 null 则返回默认选项。
pub fn java_to_dht_options_or_default(
env: &mut JNIEnv,
obj: &JObject,
) -> jni::errors::Result<DHTOptions> {
) -> DhtOptionsResult<DHTOptions> {
if obj.is_null() {
Ok(DHTOptions::default())
} else {
java_to_dht_options(env, obj)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_invalid<T>(result: DhtOptionsResult<T>, field: &'static str, value: i64) {
match result {
Err(DhtOptionsConversionError::InvalidValue {
field: actual_field,
value: actual_value,
..
}) => {
assert_eq!(actual_field, field);
assert_eq!(actual_value, value);
}
_ => panic!("expected an invalid-value error"),
}
}
#[test]
fn port_accepts_java_boundaries_and_rejects_out_of_range_values() {
assert_eq!(checked_port(0).unwrap(), 0);
assert_eq!(checked_port(i32::from(u16::MAX)).unwrap(), u16::MAX);
assert_invalid(checked_port(-1), "port", -1);
assert_invalid(checked_port(i32::from(u16::MAX) + 1), "port", 65_536);
}
#[test]
fn signed_values_are_checked_before_unsigned_conversion() {
assert_eq!(checked_non_negative_u32("rate", 0).unwrap(), 0);
assert_eq!(
checked_non_negative_u32("rate", i32::MAX).unwrap(),
i32::MAX as u32
);
assert_invalid(checked_non_negative_u32("rate", -1), "rate", -1);
assert_eq!(checked_non_negative_u64("timeout", 0).unwrap(), 0);
assert_eq!(
checked_non_negative_u64("timeout", i64::MAX).unwrap(),
i64::MAX as u64
);
assert_invalid(checked_non_negative_u64("timeout", -1), "timeout", -1);
}
#[test]
fn capacities_and_in_flight_limits_must_be_positive() {
assert_eq!(checked_positive_usize("capacity", 1).unwrap(), 1);
assert_eq!(
checked_positive_usize("capacity", i32::MAX).unwrap(),
i32::MAX as usize
);
assert_invalid(checked_positive_usize("capacity", 0), "capacity", 0);
assert_invalid(checked_positive_usize("capacity", -1), "capacity", -1);
}
#[test]
fn percentage_and_netmode_only_accept_documented_values() {
assert_eq!(checked_percentage("percent", 0).unwrap(), 0);
assert_eq!(checked_percentage("percent", 100).unwrap(), 100);
assert_invalid(checked_percentage("percent", -1), "percent", -1);
assert_invalid(checked_percentage("percent", 101), "percent", 101);
assert_eq!(checked_netmode(0).unwrap(), NetMode::Ipv4Only);
assert_eq!(checked_netmode(1).unwrap(), NetMode::Ipv6Only);
assert_eq!(checked_netmode(2).unwrap(), NetMode::DualStack);
assert_invalid(checked_netmode(-1), "netMode", -1);
assert_invalid(checked_netmode(3), "netMode", 3);
}
}
+1 -1
View File
@@ -58,5 +58,5 @@ pub mod prelude {
#[cfg(feature = "jni")]
#[path = "../jni/mod.rs"]
/// JNI entry points used by the bundled Java wrapper.
/// JNI entry points consumed by the external JVM bindings.
pub mod jni_bindings;