mirror of
https://github.com/ChronosX88/Influence-Bootstrap-Node.git
synced 2024-11-21 15:02:17 +00:00
Init commit
This commit is contained in:
commit
5200454447
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea/caches
|
||||||
|
/.idea/libraries
|
||||||
|
/.idea/modules.xml
|
||||||
|
/.idea/workspace.xml
|
||||||
|
/.idea/navEditor.xml
|
||||||
|
/.idea/assetWizardSettings.xml
|
||||||
|
.DS_Store
|
||||||
|
/out
|
||||||
|
/captures
|
||||||
|
.externalNativeBuild
|
9
.idea/artifacts/Kademlia_Bootstrap_Node_main_jar.xml
Normal file
9
.idea/artifacts/Kademlia_Bootstrap_Node_main_jar.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<component name="ArtifactManager">
|
||||||
|
<artifact type="jar" name="Kademlia-Bootstrap-Node.main:jar">
|
||||||
|
<output-path>$PROJECT_DIR$/out/artifacts/Kademlia_Bootstrap_Node_main_jar</output-path>
|
||||||
|
<root id="archive" name="Kademlia-Bootstrap-Node.main.jar">
|
||||||
|
<element id="module-output" name="Kademlia-Bootstrap-Node.main" />
|
||||||
|
<element id="extracted-dir" path="$USER_HOME$/.gradle/caches/modules-2/files-2.1/com.google.code.gson/gson/2.8.5/f645ed69d595b24d4cf8b3fbb64cc505bede8829/gson-2.8.5.jar" path-in-jar="/" />
|
||||||
|
</root>
|
||||||
|
</artifact>
|
||||||
|
</component>
|
4
.idea/encodings.xml
Normal file
4
.idea/encodings.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Encoding" addBOMForNewFiles="with NO BOM" />
|
||||||
|
</project>
|
17
.idea/gradle.xml
Normal file
17
.idea/gradle.xml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GradleSettings">
|
||||||
|
<option name="linkedExternalProjectsSettings">
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
<option name="useQualifiedModuleNames" value="true" />
|
||||||
|
</GradleProjectSettings>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
7
.idea/misc.xml
Normal file
7
.idea/misc.xml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||||
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
|
</component>
|
||||||
|
</project>
|
6
.idea/vcs.xml
Normal file
6
.idea/vcs.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
23
build.gradle
Normal file
23
build.gradle
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
plugins {
|
||||||
|
id 'java'
|
||||||
|
}
|
||||||
|
|
||||||
|
version '1.0-SNAPSHOT'
|
||||||
|
|
||||||
|
sourceCompatibility = 1.8
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation 'com.google.code.gson:gson:2.8.5'
|
||||||
|
}
|
||||||
|
|
||||||
|
jar {
|
||||||
|
manifest {
|
||||||
|
attributes(
|
||||||
|
'Main-Class': 'io.github.chronosx88.dhtBootstrap.Main'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#Thu Feb 28 14:04:57 MSK 2019
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10-all.zip
|
172
gradlew
vendored
Executable file
172
gradlew
vendored
Executable file
@ -0,0 +1,172 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
##
|
||||||
|
## Gradle start up script for UN*X
|
||||||
|
##
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
|
while [ -h "$PRG" ] ; do
|
||||||
|
ls=`ls -ld "$PRG"`
|
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
|
PRG="$link"
|
||||||
|
else
|
||||||
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
|
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >/dev/null
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS=""
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD="maximum"
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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
|
||||||
|
;;
|
||||||
|
MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
NONSTOP* )
|
||||||
|
nonstop=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
which java >/dev/null 2>&1 || 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
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||||
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
|
if [ $? -eq 0 ] ; then
|
||||||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
ulimit -n $MAX_FD
|
||||||
|
if [ $? -ne 0 ] ; then
|
||||||
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
|
if $darwin; then
|
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Cygwin, switch paths to Windows format before running java
|
||||||
|
if $cygwin ; then
|
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
|
SEP=""
|
||||||
|
for dir in $ROOTDIRSRAW ; do
|
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
|
SEP="|"
|
||||||
|
done
|
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||||
|
# Add a user-defined pattern to the cygpath arguments
|
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||||
|
fi
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
i=0
|
||||||
|
for arg in "$@" ; do
|
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||||
|
else
|
||||||
|
eval `echo args$i`="\"$arg\""
|
||||||
|
fi
|
||||||
|
i=$((i+1))
|
||||||
|
done
|
||||||
|
case $i in
|
||||||
|
(0) set -- ;;
|
||||||
|
(1) set -- "$args0" ;;
|
||||||
|
(2) set -- "$args0" "$args1" ;;
|
||||||
|
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
|
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
|
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
|
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
|
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
|
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
|
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Escape application args
|
||||||
|
save () {
|
||||||
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
|
echo " "
|
||||||
|
}
|
||||||
|
APP_ARGS=$(save "$@")
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||||
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||||
|
|
||||||
|
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||||
|
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
84
gradlew.bat
vendored
Normal file
84
gradlew.bat
vendored
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
@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=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@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=
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto init
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto init
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:init
|
||||||
|
@rem Get command-line arguments, handling Windows variants
|
||||||
|
|
||||||
|
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||||
|
|
||||||
|
:win9xME_args
|
||||||
|
@rem Slurp the command line arguments.
|
||||||
|
set CMD_LINE_ARGS=
|
||||||
|
set _SKIP=2
|
||||||
|
|
||||||
|
:win9xME_args_slurp
|
||||||
|
if "x%~1" == "x" goto execute
|
||||||
|
|
||||||
|
set CMD_LINE_ARGS=%*
|
||||||
|
|
||||||
|
: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 %CMD_LINE_ARGS%
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
2
settings.gradle
Normal file
2
settings.gradle
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
rootProject.name = 'Kademlia-Bootstrap-Node'
|
||||||
|
|
18
src/main/java/io/github/chronosx88/dhtBootstrap/Main.java
Normal file
18
src/main/java/io/github/chronosx88/dhtBootstrap/Main.java
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap;
|
||||||
|
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.JKademliaNode;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.KademliaId;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class Main {
|
||||||
|
private static JKademliaNode node;
|
||||||
|
public static void main(String[] args) {
|
||||||
|
try {
|
||||||
|
node = new JKademliaNode("Main Bootstrap Node", new KademliaId("D65D56E189E513A6AB8E38370E6B33386EB639D6"), 7243);
|
||||||
|
System.out.println(node.getNode().getNodeId().toString());
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,101 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A set of Kademlia configuration parameters. Default values are
|
||||||
|
* supplied and can be changed by the application as necessary.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class DefaultConfiguration implements KadConfiguration
|
||||||
|
{
|
||||||
|
|
||||||
|
private final static long RESTORE_INTERVAL = 60 * 1000; // in milliseconds
|
||||||
|
private final static long RESPONSE_TIMEOUT = 2000;
|
||||||
|
private final static long OPERATION_TIMEOUT = 2000;
|
||||||
|
private final static int CONCURRENCY = 10;
|
||||||
|
private final static int K = 5;
|
||||||
|
private final static int RCSIZE = 3;
|
||||||
|
private final static int STALE = 1;
|
||||||
|
private final static String LOCAL_FOLDER = "kademlia";
|
||||||
|
|
||||||
|
private final static boolean IS_TESTING = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default constructor to support Gson Serialization
|
||||||
|
*/
|
||||||
|
public DefaultConfiguration()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long restoreInterval()
|
||||||
|
{
|
||||||
|
return RESTORE_INTERVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long responseTimeout()
|
||||||
|
{
|
||||||
|
return RESPONSE_TIMEOUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long operationTimeout()
|
||||||
|
{
|
||||||
|
return OPERATION_TIMEOUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int maxConcurrentMessagesTransiting()
|
||||||
|
{
|
||||||
|
return CONCURRENCY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int k()
|
||||||
|
{
|
||||||
|
return K;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int replacementCacheSize()
|
||||||
|
{
|
||||||
|
return RCSIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int stale()
|
||||||
|
{
|
||||||
|
return STALE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getNodeDataFolder(String ownerId)
|
||||||
|
{
|
||||||
|
/* Setup the main storage folder if it doesn't exist */
|
||||||
|
String path = System.getProperty("user.home") + File.separator + DefaultConfiguration.LOCAL_FOLDER;
|
||||||
|
File folder = new File(path);
|
||||||
|
if (!folder.isDirectory())
|
||||||
|
{
|
||||||
|
folder.mkdir();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Setup subfolder for this owner if it doesn't exist */
|
||||||
|
File ownerFolder = new File(folder + File.separator + ownerId);
|
||||||
|
if (!ownerFolder.isDirectory())
|
||||||
|
{
|
||||||
|
ownerFolder.mkdir();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Return the path */
|
||||||
|
return ownerFolder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isTesting()
|
||||||
|
{
|
||||||
|
return IS_TESTING;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,428 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia;
|
||||||
|
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.Timer;
|
||||||
|
import java.util.TimerTask;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.dht.GetParameter;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.dht.DHT;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.dht.KadContent;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.dht.KademliaDHT;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.dht.JKademliaStorageEntry;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.exceptions.ContentNotFoundException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.exceptions.RoutingException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.message.MessageFactory;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.Node;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.KademliaId;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.operation.ConnectOperation;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.operation.ContentLookupOperation;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.operation.Operation;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.operation.KadRefreshOperation;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.operation.StoreOperation;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.routing.JKademliaRoutingTable;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.routing.KademliaRoutingTable;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.util.serializer.JsonDHTSerializer;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.util.serializer.JsonRoutingTableSerializer;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.util.serializer.JsonSerializer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main Kademlia Node on the network, this node manages everything for this local system.
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @since 20140215
|
||||||
|
*
|
||||||
|
* @todo When we receive a store message - if we have a newer version of the content, re-send this newer version to that node so as to update their version
|
||||||
|
* @todo Handle IPv6 Addresses
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class JKademliaNode implements KademliaNode
|
||||||
|
{
|
||||||
|
|
||||||
|
/* Kademlia Attributes */
|
||||||
|
private final String ownerId;
|
||||||
|
|
||||||
|
/* Objects to be used */
|
||||||
|
private final transient Node localNode;
|
||||||
|
private final transient KadServer server;
|
||||||
|
private final transient KademliaDHT dht;
|
||||||
|
private transient KademliaRoutingTable routingTable;
|
||||||
|
private final int udpPort;
|
||||||
|
private transient KadConfiguration config;
|
||||||
|
|
||||||
|
/* Timer used to execute refresh operations */
|
||||||
|
private transient Timer refreshOperationTimer;
|
||||||
|
private transient TimerTask refreshOperationTTask;
|
||||||
|
|
||||||
|
/* Factories */
|
||||||
|
private final transient MessageFactory messageFactory;
|
||||||
|
|
||||||
|
/* Statistics */
|
||||||
|
private final transient KadStatistician statistician;
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
statistician = new Statistician();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Kademlia DistributedMap using the specified name as filename base.
|
||||||
|
* If the id cannot be read from disk the specified defaultId is used.
|
||||||
|
* The instance is bootstraped to an existing network by specifying the
|
||||||
|
* address of a bootstrap node in the network.
|
||||||
|
*
|
||||||
|
* @param ownerId The Name of this node used for storage
|
||||||
|
* @param localNode The Local Node for this Kad instance
|
||||||
|
* @param udpPort The UDP port to use for routing messages
|
||||||
|
* @param dht The DHT for this instance
|
||||||
|
* @param config
|
||||||
|
* @param routingTable
|
||||||
|
*
|
||||||
|
* @throws IOException If an error occurred while reading id or local map
|
||||||
|
* from disk <i>or</i> a network error occurred while
|
||||||
|
* attempting to bootstrap to the network
|
||||||
|
* */
|
||||||
|
public JKademliaNode(String ownerId, Node localNode, int udpPort, KademliaDHT dht, KademliaRoutingTable routingTable, KadConfiguration config) throws IOException
|
||||||
|
{
|
||||||
|
this.ownerId = ownerId;
|
||||||
|
this.udpPort = udpPort;
|
||||||
|
this.localNode = localNode;
|
||||||
|
this.dht = dht;
|
||||||
|
this.config = config;
|
||||||
|
this.routingTable = routingTable;
|
||||||
|
this.messageFactory = new MessageFactory(this, this.dht, this.config);
|
||||||
|
this.server = new KadServer(udpPort, this.messageFactory, this.localNode, this.config, this.statistician);
|
||||||
|
this.startRefreshOperation();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void startRefreshOperation()
|
||||||
|
{
|
||||||
|
this.refreshOperationTimer = new Timer(true);
|
||||||
|
refreshOperationTTask = new TimerTask()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void run()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
/* Runs a DHT RefreshOperation */
|
||||||
|
JKademliaNode.this.refresh();
|
||||||
|
}
|
||||||
|
catch (IOException e)
|
||||||
|
{
|
||||||
|
System.err.println("KademliaNode: Refresh Operation Failed; Message: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
refreshOperationTimer.schedule(refreshOperationTTask, this.config.restoreInterval(), this.config.restoreInterval());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void stopRefreshOperation()
|
||||||
|
{
|
||||||
|
/* Close off the timer tasks */
|
||||||
|
this.refreshOperationTTask.cancel();
|
||||||
|
this.refreshOperationTimer.cancel();
|
||||||
|
this.refreshOperationTimer.purge();
|
||||||
|
}
|
||||||
|
|
||||||
|
public JKademliaNode(String ownerId, Node node, int udpPort, KademliaRoutingTable routingTable, KadConfiguration config) throws IOException
|
||||||
|
{
|
||||||
|
this(
|
||||||
|
ownerId,
|
||||||
|
node,
|
||||||
|
udpPort,
|
||||||
|
new DHT(ownerId, config),
|
||||||
|
routingTable,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JKademliaNode(String ownerId, Node node, int udpPort, KadConfiguration config) throws IOException
|
||||||
|
{
|
||||||
|
this(
|
||||||
|
ownerId,
|
||||||
|
node,
|
||||||
|
udpPort,
|
||||||
|
new JKademliaRoutingTable(node, config),
|
||||||
|
config
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JKademliaNode(String ownerId, KademliaId defaultId, int udpPort) throws IOException
|
||||||
|
{
|
||||||
|
this(
|
||||||
|
ownerId,
|
||||||
|
new Node(defaultId, InetAddress.getLocalHost(), udpPort),
|
||||||
|
udpPort,
|
||||||
|
new DefaultConfiguration()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load Stored state using default configuration
|
||||||
|
*
|
||||||
|
* @param ownerId The ID of the owner for the stored state
|
||||||
|
*
|
||||||
|
* @return A Kademlia instance loaded from a stored state in a file
|
||||||
|
*
|
||||||
|
* @throws FileNotFoundException
|
||||||
|
* @throws ClassNotFoundException
|
||||||
|
*/
|
||||||
|
public static JKademliaNode loadFromFile(String ownerId) throws FileNotFoundException, IOException, ClassNotFoundException
|
||||||
|
{
|
||||||
|
return JKademliaNode.loadFromFile(ownerId, new DefaultConfiguration());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load Stored state
|
||||||
|
*
|
||||||
|
* @param ownerId The ID of the owner for the stored state
|
||||||
|
* @param iconfig Configuration information to work with
|
||||||
|
*
|
||||||
|
* @return A Kademlia instance loaded from a stored state in a file
|
||||||
|
*
|
||||||
|
* @throws FileNotFoundException
|
||||||
|
* @throws ClassNotFoundException
|
||||||
|
*/
|
||||||
|
public static JKademliaNode loadFromFile(String ownerId, KadConfiguration iconfig) throws FileNotFoundException, IOException, ClassNotFoundException
|
||||||
|
{
|
||||||
|
DataInputStream din;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section Read Basic Kad data
|
||||||
|
*/
|
||||||
|
din = new DataInputStream(new FileInputStream(getStateStorageFolderName(ownerId, iconfig) + File.separator + "kad.kns"));
|
||||||
|
JKademliaNode ikad = new JsonSerializer<JKademliaNode>().read(din);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section Read the routing table
|
||||||
|
*/
|
||||||
|
din = new DataInputStream(new FileInputStream(getStateStorageFolderName(ownerId, iconfig) + File.separator + "routingtable.kns"));
|
||||||
|
KademliaRoutingTable irtbl = new JsonRoutingTableSerializer(iconfig).read(din);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section Read the node state
|
||||||
|
*/
|
||||||
|
din = new DataInputStream(new FileInputStream(getStateStorageFolderName(ownerId, iconfig) + File.separator + "node.kns"));
|
||||||
|
Node inode = new JsonSerializer<Node>().read(din);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section Read the DHT
|
||||||
|
*/
|
||||||
|
din = new DataInputStream(new FileInputStream(getStateStorageFolderName(ownerId, iconfig) + File.separator + "dht.kns"));
|
||||||
|
KademliaDHT idht = new JsonDHTSerializer().read(din);
|
||||||
|
idht.setConfiguration(iconfig);
|
||||||
|
|
||||||
|
return new JKademliaNode(ownerId, inode, ikad.getPort(), idht, irtbl, iconfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Node getNode()
|
||||||
|
{
|
||||||
|
return this.localNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KadServer getServer()
|
||||||
|
{
|
||||||
|
return this.server;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KademliaDHT getDHT()
|
||||||
|
{
|
||||||
|
return this.dht;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KadConfiguration getCurrentConfiguration()
|
||||||
|
{
|
||||||
|
return this.config;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized final void bootstrap(Node n) throws IOException, RoutingException
|
||||||
|
{
|
||||||
|
long startTime = System.nanoTime();
|
||||||
|
Operation op = new ConnectOperation(this.server, this, n, this.config);
|
||||||
|
op.execute();
|
||||||
|
long endTime = System.nanoTime();
|
||||||
|
this.statistician.setBootstrapTime(endTime - startTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int put(KadContent content) throws IOException
|
||||||
|
{
|
||||||
|
return this.put(new JKademliaStorageEntry(content));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int put(JKademliaStorageEntry entry) throws IOException
|
||||||
|
{
|
||||||
|
StoreOperation sop = new StoreOperation(this.server, this, entry, this.dht, this.config);
|
||||||
|
sop.execute();
|
||||||
|
|
||||||
|
/* Return how many nodes the content was stored on */
|
||||||
|
return sop.numNodesStoredAt();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void putLocally(KadContent content) throws IOException
|
||||||
|
{
|
||||||
|
this.dht.store(new JKademliaStorageEntry(content));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JKademliaStorageEntry get(GetParameter param) throws NoSuchElementException, IOException, ContentNotFoundException
|
||||||
|
{
|
||||||
|
if (this.dht.contains(param))
|
||||||
|
{
|
||||||
|
/* If the content exist in our own DHT, then return it. */
|
||||||
|
return this.dht.get(param);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Seems like it doesn't exist in our DHT, get it from other Nodes */
|
||||||
|
long startTime = System.nanoTime();
|
||||||
|
ContentLookupOperation clo = new ContentLookupOperation(server, this, param, this.config);
|
||||||
|
clo.execute();
|
||||||
|
long endTime = System.nanoTime();
|
||||||
|
this.statistician.addContentLookup(endTime - startTime, clo.routeLength(), clo.isContentFound());
|
||||||
|
return clo.getContentFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void refresh() throws IOException
|
||||||
|
{
|
||||||
|
new KadRefreshOperation(this.server, this, this.dht, this.config).execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getOwnerId()
|
||||||
|
{
|
||||||
|
return this.ownerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPort()
|
||||||
|
{
|
||||||
|
return this.udpPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutdown(final boolean saveState) throws IOException
|
||||||
|
{
|
||||||
|
/* Shut down the server */
|
||||||
|
this.server.shutdown();
|
||||||
|
|
||||||
|
this.stopRefreshOperation();
|
||||||
|
|
||||||
|
/* Save this Kademlia instance's state if required */
|
||||||
|
if (saveState)
|
||||||
|
{
|
||||||
|
/* Save the system state */
|
||||||
|
this.saveKadState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveKadState() throws IOException
|
||||||
|
{
|
||||||
|
DataOutputStream dout;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section Store Basic Kad data
|
||||||
|
*/
|
||||||
|
dout = new DataOutputStream(new FileOutputStream(getStateStorageFolderName(this.ownerId, this.config) + File.separator + "kad.kns"));
|
||||||
|
new JsonSerializer<JKademliaNode>().write(this, dout);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section Save the node state
|
||||||
|
*/
|
||||||
|
dout = new DataOutputStream(new FileOutputStream(getStateStorageFolderName(this.ownerId, this.config) + File.separator + "node.kns"));
|
||||||
|
new JsonSerializer<Node>().write(this.localNode, dout);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section Save the routing table
|
||||||
|
* We need to save the routing table separate from the node since the routing table will contain the node and the node will contain the routing table
|
||||||
|
* This will cause a serialization recursion, and in turn a Stack Overflow
|
||||||
|
*/
|
||||||
|
dout = new DataOutputStream(new FileOutputStream(getStateStorageFolderName(this.ownerId, this.config) + File.separator + "routingtable.kns"));
|
||||||
|
new JsonRoutingTableSerializer(this.config).write(this.getRoutingTable(), dout);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section Save the DHT
|
||||||
|
*/
|
||||||
|
dout = new DataOutputStream(new FileOutputStream(getStateStorageFolderName(this.ownerId, this.config) + File.separator + "dht.kns"));
|
||||||
|
new JsonDHTSerializer().write(this.dht, dout);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the name of the folder for which a content should be stored
|
||||||
|
*
|
||||||
|
* @return String The name of the folder to store node states
|
||||||
|
*/
|
||||||
|
private static String getStateStorageFolderName(String ownerId, KadConfiguration iconfig)
|
||||||
|
{
|
||||||
|
/* Setup the nodes storage folder if it doesn't exist */
|
||||||
|
String path = iconfig.getNodeDataFolder(ownerId) + File.separator + "nodeState";
|
||||||
|
File nodeStateFolder = new File(path);
|
||||||
|
if (!nodeStateFolder.isDirectory())
|
||||||
|
{
|
||||||
|
nodeStateFolder.mkdir();
|
||||||
|
}
|
||||||
|
return nodeStateFolder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KademliaRoutingTable getRoutingTable()
|
||||||
|
{
|
||||||
|
return this.routingTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KadStatistician getStatistician()
|
||||||
|
{
|
||||||
|
return this.statistician;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a string containing all data about this Kademlia instance
|
||||||
|
*
|
||||||
|
* @return The string representation of this Kad instance
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder("\n\nPrinting Kad State for instance with owner: ");
|
||||||
|
sb.append(this.ownerId);
|
||||||
|
sb.append("\n\n");
|
||||||
|
|
||||||
|
sb.append("\n");
|
||||||
|
sb.append("Local Node");
|
||||||
|
sb.append(this.localNode);
|
||||||
|
sb.append("\n");
|
||||||
|
|
||||||
|
sb.append("\n");
|
||||||
|
sb.append("Routing Table: ");
|
||||||
|
sb.append(this.getRoutingTable());
|
||||||
|
sb.append("\n");
|
||||||
|
|
||||||
|
sb.append("\n");
|
||||||
|
sb.append("DHT: ");
|
||||||
|
sb.append(this.dht);
|
||||||
|
sb.append("\n");
|
||||||
|
|
||||||
|
sb.append("\n\n\n");
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface that defines a KadConfiguration object
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @since 20140329
|
||||||
|
*/
|
||||||
|
public interface KadConfiguration
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Interval in milliseconds between execution of RestoreOperations.
|
||||||
|
*/
|
||||||
|
public long restoreInterval();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If no reply received from a node in this period (in milliseconds)
|
||||||
|
* consider the node unresponsive.
|
||||||
|
*
|
||||||
|
* @return The time it takes to consider a node unresponsive
|
||||||
|
*/
|
||||||
|
public long responseTimeout();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Maximum number of milliseconds for performing an operation.
|
||||||
|
*/
|
||||||
|
public long operationTimeout();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Maximum number of concurrent messages in transit.
|
||||||
|
*/
|
||||||
|
public int maxConcurrentMessagesTransiting();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return K-Value used throughout Kademlia
|
||||||
|
*/
|
||||||
|
public int k();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Size of replacement cache.
|
||||||
|
*/
|
||||||
|
public int replacementCacheSize();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return # of times a node can be marked as stale before it is actually removed.
|
||||||
|
*/
|
||||||
|
public int stale();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the folder in which this node data is to be stored.
|
||||||
|
*
|
||||||
|
* @param ownerId
|
||||||
|
*
|
||||||
|
* @return The folder path
|
||||||
|
*/
|
||||||
|
public String getNodeDataFolder(String ownerId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Whether we're in a testing or production system.
|
||||||
|
*/
|
||||||
|
public boolean isTesting();
|
||||||
|
}
|
@ -0,0 +1,356 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.DatagramPacket;
|
||||||
|
import java.net.DatagramSocket;
|
||||||
|
import java.net.SocketException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.Timer;
|
||||||
|
import java.util.TimerTask;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.exceptions.KadServerDownException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.message.KademliaMessageFactory;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.message.Message;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.Node;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.message.Receiver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The server that handles sending and receiving messages between nodes on the Kad Network
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @created 20140215
|
||||||
|
*/
|
||||||
|
public class KadServer
|
||||||
|
{
|
||||||
|
|
||||||
|
/* Maximum size of a Datagram Packet */
|
||||||
|
private static final int DATAGRAM_BUFFER_SIZE = 64 * 1024; // 64KB
|
||||||
|
|
||||||
|
/* Basic Kad Objects */
|
||||||
|
private final transient KadConfiguration config;
|
||||||
|
|
||||||
|
/* Server Objects */
|
||||||
|
private final DatagramSocket socket;
|
||||||
|
private transient boolean isRunning;
|
||||||
|
private final Map<Integer, Receiver> receivers;
|
||||||
|
private final Timer timer; // Schedule future tasks
|
||||||
|
private final Map<Integer, TimerTask> tasks; // Keep track of scheduled tasks
|
||||||
|
|
||||||
|
private final Node localNode;
|
||||||
|
|
||||||
|
/* Factories */
|
||||||
|
private final KademliaMessageFactory messageFactory;
|
||||||
|
|
||||||
|
private final KadStatistician statistician;
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
isRunning = true;
|
||||||
|
this.tasks = new HashMap<>();
|
||||||
|
this.receivers = new HashMap<>();
|
||||||
|
this.timer = new Timer(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize our KadServer
|
||||||
|
*
|
||||||
|
* @param udpPort The port to listen on
|
||||||
|
* @param mFactory Factory used to create messages
|
||||||
|
* @param localNode Local node on which this server runs on
|
||||||
|
* @param config
|
||||||
|
* @param statistician A statistician to manage the server statistics
|
||||||
|
*
|
||||||
|
* @throws SocketException
|
||||||
|
*/
|
||||||
|
public KadServer(int udpPort, KademliaMessageFactory mFactory, Node localNode, KadConfiguration config, KadStatistician statistician) throws SocketException
|
||||||
|
{
|
||||||
|
this.config = config;
|
||||||
|
this.socket = new DatagramSocket(udpPort);
|
||||||
|
this.localNode = localNode;
|
||||||
|
this.messageFactory = mFactory;
|
||||||
|
this.statistician = statistician;
|
||||||
|
|
||||||
|
/* Start listening for incoming requests in a new thread */
|
||||||
|
this.startListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the listener to listen for incoming messages
|
||||||
|
*/
|
||||||
|
private void startListener()
|
||||||
|
{
|
||||||
|
new Thread()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void run()
|
||||||
|
{
|
||||||
|
listen();
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a message
|
||||||
|
*
|
||||||
|
* @param msg The message to send
|
||||||
|
* @param to The node to send the message to
|
||||||
|
* @param recv The receiver to handle the response message
|
||||||
|
*
|
||||||
|
* @return Integer The communication ID of this message
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
* @throws KadServerDownException
|
||||||
|
*/
|
||||||
|
public synchronized int sendMessage(Node to, Message msg, Receiver recv) throws IOException, KadServerDownException
|
||||||
|
{
|
||||||
|
if (!isRunning)
|
||||||
|
{
|
||||||
|
throw new KadServerDownException(this.localNode + " - Kad Server is not running.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Generate a random communication ID */
|
||||||
|
int comm = new Random().nextInt();
|
||||||
|
|
||||||
|
/* If we have a receiver */
|
||||||
|
if (recv != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
/* Setup the receiver to handle message response */
|
||||||
|
receivers.put(comm, recv);
|
||||||
|
TimerTask task = new TimeoutTask(comm, recv);
|
||||||
|
timer.schedule(task, this.config.responseTimeout());
|
||||||
|
tasks.put(comm, task);
|
||||||
|
}
|
||||||
|
catch (IllegalStateException ex)
|
||||||
|
{
|
||||||
|
/* The timer is already cancelled so we cannot do anything here really */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Send the message */
|
||||||
|
sendMessage(to, msg, comm);
|
||||||
|
|
||||||
|
return comm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method called to reply to a message received
|
||||||
|
*
|
||||||
|
* @param to The Node to send the reply to
|
||||||
|
* @param msg The reply message
|
||||||
|
* @param comm The communication ID - the one received
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public synchronized void reply(Node to, Message msg, int comm) throws IOException
|
||||||
|
{
|
||||||
|
if (!isRunning)
|
||||||
|
{
|
||||||
|
throw new IllegalStateException("Kad Server is not running.");
|
||||||
|
}
|
||||||
|
sendMessage(to, msg, comm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal sendMessage method called by the public sendMessage method after a communicationId is generated
|
||||||
|
*/
|
||||||
|
private void sendMessage(Node to, Message msg, int comm) throws IOException
|
||||||
|
{
|
||||||
|
/* Use a try-with resource to auto-close streams after usage */
|
||||||
|
try (ByteArrayOutputStream bout = new ByteArrayOutputStream(); DataOutputStream dout = new DataOutputStream(bout);)
|
||||||
|
{
|
||||||
|
/* Setup the message for transmission */
|
||||||
|
dout.writeInt(comm);
|
||||||
|
dout.writeByte(msg.code());
|
||||||
|
msg.toStream(dout);
|
||||||
|
dout.close();
|
||||||
|
|
||||||
|
byte[] data = bout.toByteArray();
|
||||||
|
|
||||||
|
if (data.length > DATAGRAM_BUFFER_SIZE)
|
||||||
|
{
|
||||||
|
throw new IOException("Message is too big");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Everything is good, now create the packet and send it */
|
||||||
|
DatagramPacket pkt = new DatagramPacket(data, 0, data.length);
|
||||||
|
pkt.setSocketAddress(to.getSocketAddress());
|
||||||
|
socket.send(pkt);
|
||||||
|
|
||||||
|
/* Lets inform the statistician that we've sent some data */
|
||||||
|
this.statistician.sentData(data.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listen for incoming messages in a separate thread
|
||||||
|
*/
|
||||||
|
private void listen()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (isRunning)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
/* Wait for a packet */
|
||||||
|
byte[] buffer = new byte[DATAGRAM_BUFFER_SIZE];
|
||||||
|
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
|
||||||
|
socket.receive(packet);
|
||||||
|
|
||||||
|
/* Lets inform the statistician that we've received some data */
|
||||||
|
this.statistician.receivedData(packet.getLength());
|
||||||
|
|
||||||
|
if (this.config.isTesting())
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Simulating network latency
|
||||||
|
* We pause for 1 millisecond/100 bytes
|
||||||
|
*/
|
||||||
|
int pause = packet.getLength() / 100;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Thread.sleep(pause);
|
||||||
|
}
|
||||||
|
catch (InterruptedException ex)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* We've received a packet, now handle it */
|
||||||
|
try (ByteArrayInputStream bin = new ByteArrayInputStream(packet.getData(), packet.getOffset(), packet.getLength());
|
||||||
|
DataInputStream din = new DataInputStream(bin);)
|
||||||
|
{
|
||||||
|
|
||||||
|
/* Read in the conversation Id to know which handler to handle this response */
|
||||||
|
int comm = din.readInt();
|
||||||
|
byte messCode = din.readByte();
|
||||||
|
|
||||||
|
Message msg = messageFactory.createMessage(messCode, din);
|
||||||
|
din.close();
|
||||||
|
|
||||||
|
/* Get a receiver for this message */
|
||||||
|
Receiver receiver;
|
||||||
|
if (this.receivers.containsKey(comm))
|
||||||
|
{
|
||||||
|
/* If there is a reciever in the receivers to handle this */
|
||||||
|
synchronized (this)
|
||||||
|
{
|
||||||
|
receiver = this.receivers.remove(comm);
|
||||||
|
TimerTask task = (TimerTask) tasks.remove(comm);
|
||||||
|
if (task != null)
|
||||||
|
{
|
||||||
|
task.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* There is currently no receivers, try to get one */
|
||||||
|
receiver = messageFactory.createReceiver(messCode, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Invoke the receiver */
|
||||||
|
if (receiver != null)
|
||||||
|
{
|
||||||
|
receiver.receive(msg, comm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException e)
|
||||||
|
{
|
||||||
|
//this.isRunning = false;
|
||||||
|
System.err.println("Server ran into a problem in listener method. Message: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (!socket.isClosed())
|
||||||
|
{
|
||||||
|
socket.close();
|
||||||
|
}
|
||||||
|
this.isRunning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a conversation receiver
|
||||||
|
*
|
||||||
|
* @param comm The id of this conversation
|
||||||
|
*/
|
||||||
|
private synchronized void unregister(int comm)
|
||||||
|
{
|
||||||
|
receivers.remove(comm);
|
||||||
|
this.tasks.remove(comm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops listening and shuts down the server
|
||||||
|
*/
|
||||||
|
public synchronized void shutdown()
|
||||||
|
{
|
||||||
|
this.isRunning = false;
|
||||||
|
this.socket.close();
|
||||||
|
timer.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Task that gets called by a separate thread if a timeout for a receiver occurs.
|
||||||
|
* When a reply arrives this task must be canceled using the <code>cancel()</code>
|
||||||
|
* method inherited from <code>TimerTask</code>. In this case the caller is
|
||||||
|
* responsible for removing the task from the <code>tasks</code> map.
|
||||||
|
* */
|
||||||
|
class TimeoutTask extends TimerTask
|
||||||
|
{
|
||||||
|
|
||||||
|
private final int comm;
|
||||||
|
private final Receiver recv;
|
||||||
|
|
||||||
|
public TimeoutTask(int comm, Receiver recv)
|
||||||
|
{
|
||||||
|
this.comm = comm;
|
||||||
|
this.recv = recv;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run()
|
||||||
|
{
|
||||||
|
if (!KadServer.this.isRunning)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
unregister(comm);
|
||||||
|
recv.timeout(comm);
|
||||||
|
}
|
||||||
|
catch (IOException e)
|
||||||
|
{
|
||||||
|
System.err.println("Cannot unregister a receiver. Message: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void printReceivers()
|
||||||
|
{
|
||||||
|
for (Integer r : this.receivers.keySet())
|
||||||
|
{
|
||||||
|
System.out.println("Receiver for comm: " + r + "; Receiver: " + this.receivers.get(r));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRunning()
|
||||||
|
{
|
||||||
|
return this.isRunning;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,87 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specification for class that keeps statistics for a Kademlia instance.
|
||||||
|
*
|
||||||
|
* These statistics are temporary and will be lost when Kad is shut down.
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @since 20140507
|
||||||
|
*/
|
||||||
|
public interface KadStatistician
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to indicate some data is sent
|
||||||
|
*
|
||||||
|
* @param size The size of the data sent
|
||||||
|
*/
|
||||||
|
public void sentData(long size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The total data sent in KiloBytes
|
||||||
|
*/
|
||||||
|
public long getTotalDataSent();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to indicate some data was received
|
||||||
|
*
|
||||||
|
* @param size The size of the data received
|
||||||
|
*/
|
||||||
|
public void receivedData(long size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The total data received in KiloBytes
|
||||||
|
*/
|
||||||
|
public long getTotalDataReceived();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the bootstrap time for this Kademlia Node
|
||||||
|
*
|
||||||
|
* @param time The bootstrap time in nanoseconds
|
||||||
|
*/
|
||||||
|
public void setBootstrapTime(long time);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return How long the system took to bootstrap in milliseconds
|
||||||
|
*/
|
||||||
|
public long getBootstrapTime();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the timing for a new content lookup operation that took place
|
||||||
|
*
|
||||||
|
* @param time The time the content lookup took in nanoseconds
|
||||||
|
* @param routeLength The length of the route it took to get the content
|
||||||
|
* @param isSuccessful Whether the content lookup was successful or not
|
||||||
|
*/
|
||||||
|
public void addContentLookup(long time, int routeLength, boolean isSuccessful);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The total number of content lookups performed.
|
||||||
|
*/
|
||||||
|
public int numContentLookups();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return How many content lookups have failed.
|
||||||
|
*/
|
||||||
|
public int numFailedContentLookups();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The total time spent on content lookups.
|
||||||
|
*/
|
||||||
|
public long totalContentLookupTime();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the average time a content lookup took
|
||||||
|
*
|
||||||
|
* @return The average time in milliseconds
|
||||||
|
*/
|
||||||
|
public double averageContentLookupTime();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the average route length of content lookup operations.
|
||||||
|
*
|
||||||
|
* @return The average route length
|
||||||
|
*/
|
||||||
|
public double averageContentLookupRouteLength();
|
||||||
|
}
|
@ -0,0 +1,154 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.dht.GetParameter;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.dht.JKademliaStorageEntry;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.dht.KadContent;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.dht.KademliaDHT;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.exceptions.ContentNotFoundException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.exceptions.RoutingException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.Node;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.routing.KademliaRoutingTable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main Kademlia Node on the network, this node manages everything for this local system.
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @since 20140523
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public interface KademliaNode
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule the recurring refresh operation
|
||||||
|
*/
|
||||||
|
public void startRefreshOperation();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the recurring refresh operation
|
||||||
|
*/
|
||||||
|
public void stopRefreshOperation();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Node The local node for this system
|
||||||
|
*/
|
||||||
|
public Node getNode();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The KadServer used to send/receive messages
|
||||||
|
*/
|
||||||
|
public KadServer getServer();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The DHT for this kad instance
|
||||||
|
*/
|
||||||
|
public KademliaDHT getDHT();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The current KadConfiguration object being used
|
||||||
|
*/
|
||||||
|
public KadConfiguration getCurrentConfiguration();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to an existing peer-to-peer network.
|
||||||
|
*
|
||||||
|
* @param n The known node in the peer-to-peer network
|
||||||
|
*
|
||||||
|
* @throws RoutingException If the bootstrap node could not be contacted
|
||||||
|
* @throws IOException If a network error occurred
|
||||||
|
* @throws IllegalStateException If this object is closed
|
||||||
|
* */
|
||||||
|
public void bootstrap(Node n) throws IOException, RoutingException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the specified value under the given key
|
||||||
|
* This value is stored on K nodes on the network, or all nodes if there are > K total nodes in the network
|
||||||
|
*
|
||||||
|
* @param content The content to put onto the DHT
|
||||||
|
*
|
||||||
|
* @return Integer How many nodes the content was stored on
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public int put(KadContent content) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the specified value under the given key
|
||||||
|
* This value is stored on K nodes on the network, or all nodes if there are > K total nodes in the network
|
||||||
|
*
|
||||||
|
* @param entry The StorageEntry with the content to put onto the DHT
|
||||||
|
*
|
||||||
|
* @return Integer How many nodes the content was stored on
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public int put(JKademliaStorageEntry entry) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a content on the local node's DHT
|
||||||
|
*
|
||||||
|
* @param content The content to put on the DHT
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public void putLocally(KadContent content) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get some content stored on the DHT
|
||||||
|
*
|
||||||
|
* @param param The parameters used to search for the content
|
||||||
|
*
|
||||||
|
* @return DHTContent The content
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
* @throws ContentNotFoundException
|
||||||
|
*/
|
||||||
|
public JKademliaStorageEntry get(GetParameter param) throws NoSuchElementException, IOException, ContentNotFoundException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow the user of the System to call refresh even out of the normal Kad refresh timing
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public void refresh() throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return String The ID of the owner of this local network
|
||||||
|
*/
|
||||||
|
public String getOwnerId();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Integer The port on which this kad instance is running
|
||||||
|
*/
|
||||||
|
public int getPort();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Here we handle properly shutting down the Kademlia instance
|
||||||
|
*
|
||||||
|
* @param saveState Whether to save the application state or not
|
||||||
|
*
|
||||||
|
* @throws java.io.FileNotFoundException
|
||||||
|
*/
|
||||||
|
public void shutdown(final boolean saveState) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the node state to a text file
|
||||||
|
*
|
||||||
|
* @throws java.io.FileNotFoundException
|
||||||
|
*/
|
||||||
|
public void saveKadState() throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The routing table for this node.
|
||||||
|
*/
|
||||||
|
public KademliaRoutingTable getRoutingTable();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The statistician that manages all statistics
|
||||||
|
*/
|
||||||
|
public KadStatistician getStatistician();
|
||||||
|
}
|
@ -0,0 +1,182 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia;
|
||||||
|
|
||||||
|
import java.text.DecimalFormat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class that keeps statistics for this Kademlia instance.
|
||||||
|
*
|
||||||
|
* These statistics are temporary and will be lost when Kad is shut down.
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @since 20140505
|
||||||
|
*/
|
||||||
|
public class Statistician implements KadStatistician
|
||||||
|
{
|
||||||
|
|
||||||
|
/* How much data was sent and received by the server over the network */
|
||||||
|
private long totalDataSent, totalDataReceived;
|
||||||
|
private long numDataSent, numDataReceived;
|
||||||
|
|
||||||
|
/* Bootstrap timings */
|
||||||
|
private long bootstrapTime;
|
||||||
|
|
||||||
|
/* Content lookup operation timing & route length */
|
||||||
|
private int numContentLookups, numFailedContentLookups;
|
||||||
|
private long totalContentLookupTime;
|
||||||
|
private long totalRouteLength;
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
this.totalDataSent = 0;
|
||||||
|
this.totalDataReceived = 0;
|
||||||
|
this.bootstrapTime = 0;
|
||||||
|
this.numContentLookups = 0;
|
||||||
|
this.totalContentLookupTime = 0;
|
||||||
|
this.totalRouteLength = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sentData(long size)
|
||||||
|
{
|
||||||
|
this.totalDataSent += size;
|
||||||
|
this.numDataSent++;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getTotalDataSent()
|
||||||
|
{
|
||||||
|
if (this.totalDataSent == 0)
|
||||||
|
{
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.totalDataSent / 1000L;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void receivedData(long size)
|
||||||
|
{
|
||||||
|
this.totalDataReceived += size;
|
||||||
|
this.numDataReceived++;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getTotalDataReceived()
|
||||||
|
{
|
||||||
|
if (this.totalDataReceived == 0)
|
||||||
|
{
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
return this.totalDataReceived / 1000L;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBootstrapTime(long time)
|
||||||
|
{
|
||||||
|
this.bootstrapTime = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getBootstrapTime()
|
||||||
|
{
|
||||||
|
return this.bootstrapTime / 1000000L;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addContentLookup(long time, int routeLength, boolean isSuccessful)
|
||||||
|
{
|
||||||
|
if (isSuccessful)
|
||||||
|
{
|
||||||
|
this.numContentLookups++;
|
||||||
|
this.totalContentLookupTime += time;
|
||||||
|
this.totalRouteLength += routeLength;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.numFailedContentLookups++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int numContentLookups()
|
||||||
|
{
|
||||||
|
return this.numContentLookups;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int numFailedContentLookups()
|
||||||
|
{
|
||||||
|
return this.numFailedContentLookups;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long totalContentLookupTime()
|
||||||
|
{
|
||||||
|
return this.totalContentLookupTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double averageContentLookupTime()
|
||||||
|
{
|
||||||
|
if (this.numContentLookups == 0)
|
||||||
|
{
|
||||||
|
return 0D;
|
||||||
|
}
|
||||||
|
|
||||||
|
double avg = (double) ((double) this.totalContentLookupTime / (double) this.numContentLookups) / 1000000D;
|
||||||
|
DecimalFormat df = new DecimalFormat("#.00");
|
||||||
|
return new Double(df.format(avg));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double averageContentLookupRouteLength()
|
||||||
|
{
|
||||||
|
if (this.numContentLookups == 0)
|
||||||
|
{
|
||||||
|
return 0D;
|
||||||
|
}
|
||||||
|
double avg = (double) ((double) this.totalRouteLength / (double) this.numContentLookups);
|
||||||
|
DecimalFormat df = new DecimalFormat("#.00");
|
||||||
|
return new Double(df.format(avg));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder("Statistician: [");
|
||||||
|
|
||||||
|
sb.append("Bootstrap Time: ");
|
||||||
|
sb.append(this.getBootstrapTime());
|
||||||
|
sb.append("; ");
|
||||||
|
|
||||||
|
sb.append("Data Sent: ");
|
||||||
|
sb.append("(");
|
||||||
|
sb.append(this.numDataSent);
|
||||||
|
sb.append(") ");
|
||||||
|
sb.append(this.getTotalDataSent());
|
||||||
|
sb.append(" bytes; ");
|
||||||
|
|
||||||
|
sb.append("Data Received: ");
|
||||||
|
sb.append("(");
|
||||||
|
sb.append(this.numDataReceived);
|
||||||
|
sb.append(") ");
|
||||||
|
sb.append(this.getTotalDataReceived());
|
||||||
|
sb.append(" bytes; ");
|
||||||
|
|
||||||
|
sb.append("Num Content Lookups: ");
|
||||||
|
sb.append(this.numContentLookups());
|
||||||
|
sb.append("; ");
|
||||||
|
|
||||||
|
sb.append("Avg Content Lookup Time: ");
|
||||||
|
sb.append(this.averageContentLookupTime());
|
||||||
|
sb.append("; ");
|
||||||
|
|
||||||
|
sb.append("Avg Content Lookup Route Lth: ");
|
||||||
|
sb.append(this.averageContentLookupRouteLength());
|
||||||
|
sb.append("; ");
|
||||||
|
|
||||||
|
sb.append("]");
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,265 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.dht;
|
||||||
|
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KadConfiguration;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.exceptions.ContentExistException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.exceptions.ContentNotFoundException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.KademliaId;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.util.serializer.JsonSerializer;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.util.serializer.KadSerializer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main Distributed Hash Table class that manages the entire DHT
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @since 20140226
|
||||||
|
*/
|
||||||
|
public class DHT implements KademliaDHT
|
||||||
|
{
|
||||||
|
|
||||||
|
private transient StoredContentManager contentManager;
|
||||||
|
private transient KadSerializer<JKademliaStorageEntry> serializer = null;
|
||||||
|
private transient KadConfiguration config;
|
||||||
|
|
||||||
|
private final String ownerId;
|
||||||
|
|
||||||
|
public DHT(String ownerId, KadConfiguration config)
|
||||||
|
{
|
||||||
|
this.ownerId = ownerId;
|
||||||
|
this.config = config;
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void initialize()
|
||||||
|
{
|
||||||
|
contentManager = new StoredContentManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setConfiguration(KadConfiguration con)
|
||||||
|
{
|
||||||
|
this.config = con;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KadSerializer<JKademliaStorageEntry> getSerializer()
|
||||||
|
{
|
||||||
|
if (null == serializer)
|
||||||
|
{
|
||||||
|
serializer = new JsonSerializer<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return serializer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean store(JKademliaStorageEntry content) throws IOException
|
||||||
|
{
|
||||||
|
/* Lets check if we have this content and it's the updated version */
|
||||||
|
if (this.contentManager.contains(content.getContentMetadata()))
|
||||||
|
{
|
||||||
|
KademliaStorageEntryMetadata current = this.contentManager.get(content.getContentMetadata());
|
||||||
|
|
||||||
|
/* update the last republished time */
|
||||||
|
current.updateLastRepublished();
|
||||||
|
|
||||||
|
if (current.getLastUpdatedTimestamp() >= content.getContentMetadata().getLastUpdatedTimestamp())
|
||||||
|
{
|
||||||
|
/* We have the current content, no need to update it! just leave this method now */
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* We have this content, but not the latest version, lets delete it so the new version will be added below */
|
||||||
|
try
|
||||||
|
{
|
||||||
|
//System.out.println("Removing older content to update it");
|
||||||
|
this.remove(content.getContentMetadata());
|
||||||
|
}
|
||||||
|
catch (ContentNotFoundException ex)
|
||||||
|
{
|
||||||
|
/* This won't ever happen at this point since we only get here if the content is found, lets ignore it */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If we got here means we don't have this content, or we need to update the content
|
||||||
|
* If we need to update the content, the code above would've already deleted it, so we just need to re-add it
|
||||||
|
*/
|
||||||
|
try
|
||||||
|
{
|
||||||
|
//System.out.println("Adding new content.");
|
||||||
|
/* Keep track of this content in the entries manager */
|
||||||
|
KademliaStorageEntryMetadata sEntry = this.contentManager.put(content.getContentMetadata());
|
||||||
|
|
||||||
|
/* Now we store the content locally in a file */
|
||||||
|
String contentStorageFolder = this.getContentStorageFolderName(content.getContentMetadata().getKey());
|
||||||
|
|
||||||
|
try (FileOutputStream fout = new FileOutputStream(contentStorageFolder + File.separator + sEntry.hashCode() + ".kct");
|
||||||
|
DataOutputStream dout = new DataOutputStream(fout))
|
||||||
|
{
|
||||||
|
this.getSerializer().write(content, dout);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (ContentExistException e)
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Content already exist on the DHT
|
||||||
|
* This won't happen because above takes care of removing the content if it's older and needs to be updated,
|
||||||
|
* or returning if we already have the current content version.
|
||||||
|
*/
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean store(KadContent content) throws IOException
|
||||||
|
{
|
||||||
|
return this.store(new JKademliaStorageEntry(content));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JKademliaStorageEntry retrieve(KademliaId key, int hashCode) throws FileNotFoundException, IOException, ClassNotFoundException
|
||||||
|
{
|
||||||
|
String folder = this.getContentStorageFolderName(key);
|
||||||
|
DataInputStream din = new DataInputStream(new FileInputStream(folder + File.separator + hashCode + ".kct"));
|
||||||
|
return this.getSerializer().read(din);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean contains(GetParameter param)
|
||||||
|
{
|
||||||
|
return this.contentManager.contains(param);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JKademliaStorageEntry get(KademliaStorageEntryMetadata entry) throws IOException, NoSuchElementException
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return this.retrieve(entry.getKey(), entry.hashCode());
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException e)
|
||||||
|
{
|
||||||
|
System.err.println("Error while loading file for content. Message: " + e.getMessage());
|
||||||
|
}
|
||||||
|
catch (ClassNotFoundException e)
|
||||||
|
{
|
||||||
|
System.err.println("The class for some content was not found. Message: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If we got here, means we got no entries */
|
||||||
|
throw new NoSuchElementException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JKademliaStorageEntry get(GetParameter param) throws NoSuchElementException, IOException
|
||||||
|
{
|
||||||
|
/* Load a KadContent if any exist for the given criteria */
|
||||||
|
try
|
||||||
|
{
|
||||||
|
KademliaStorageEntryMetadata e = this.contentManager.get(param);
|
||||||
|
return this.retrieve(e.getKey(), e.hashCode());
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException e)
|
||||||
|
{
|
||||||
|
System.err.println("Error while loading file for content. Message: " + e.getMessage());
|
||||||
|
}
|
||||||
|
catch (ClassNotFoundException e)
|
||||||
|
{
|
||||||
|
System.err.println("The class for some content was not found. Message: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If we got here, means we got no entries */
|
||||||
|
throw new NoSuchElementException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void remove(KadContent content) throws ContentNotFoundException
|
||||||
|
{
|
||||||
|
this.remove(new StorageEntryMetadata(content));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void remove(KademliaStorageEntryMetadata entry) throws ContentNotFoundException
|
||||||
|
{
|
||||||
|
String folder = this.getContentStorageFolderName(entry.getKey());
|
||||||
|
File file = new File(folder + File.separator + entry.hashCode() + ".kct");
|
||||||
|
|
||||||
|
contentManager.remove(entry);
|
||||||
|
|
||||||
|
if (file.exists())
|
||||||
|
{
|
||||||
|
file.delete();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new ContentNotFoundException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the name of the folder for which a content should be stored
|
||||||
|
*
|
||||||
|
* @param key The key of the content
|
||||||
|
*
|
||||||
|
* @return String The name of the folder
|
||||||
|
*/
|
||||||
|
private String getContentStorageFolderName(KademliaId key)
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Each content is stored in a folder named after the first 2 characters of the NodeId
|
||||||
|
*
|
||||||
|
* The name of the file containing the content is the hash of this content
|
||||||
|
*/
|
||||||
|
String folderName = key.hexRepresentation().substring(0, 2);
|
||||||
|
File contentStorageFolder = new File(this.config.getNodeDataFolder(ownerId) + File.separator + folderName);
|
||||||
|
|
||||||
|
/* Create the content folder if it doesn't exist */
|
||||||
|
if (!contentStorageFolder.isDirectory())
|
||||||
|
{
|
||||||
|
contentStorageFolder.mkdir();
|
||||||
|
}
|
||||||
|
|
||||||
|
return contentStorageFolder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<KademliaStorageEntryMetadata> getStorageEntries()
|
||||||
|
{
|
||||||
|
return contentManager.getAllEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void putStorageEntries(List<KademliaStorageEntryMetadata> ientries)
|
||||||
|
{
|
||||||
|
for (KademliaStorageEntryMetadata e : ientries)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
this.contentManager.put(e);
|
||||||
|
}
|
||||||
|
catch (ContentExistException ex)
|
||||||
|
{
|
||||||
|
/* Entry already exist, no need to store it again */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized String toString()
|
||||||
|
{
|
||||||
|
return this.contentManager.toString();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,117 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.dht;
|
||||||
|
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.KademliaId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A GET request can get content based on Key, Owner, Type, etc
|
||||||
|
*
|
||||||
|
* This is a class containing the parameters to be passed in a GET request
|
||||||
|
*
|
||||||
|
* We use a class since the number of filtering parameters can change later
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @since 20140224
|
||||||
|
*/
|
||||||
|
public class GetParameter
|
||||||
|
{
|
||||||
|
|
||||||
|
private KademliaId key;
|
||||||
|
private String ownerId = null;
|
||||||
|
private String type = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a GetParameter to search for data by NodeId and owner
|
||||||
|
*
|
||||||
|
* @param key
|
||||||
|
* @param type
|
||||||
|
*/
|
||||||
|
public GetParameter(KademliaId key, String type)
|
||||||
|
{
|
||||||
|
this.key = key;
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a GetParameter to search for data by NodeId, owner, type
|
||||||
|
*
|
||||||
|
* @param key
|
||||||
|
* @param type
|
||||||
|
* @param owner
|
||||||
|
*/
|
||||||
|
public GetParameter(KademliaId key, String type, String owner)
|
||||||
|
{
|
||||||
|
this(key, type);
|
||||||
|
this.ownerId = owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct our get parameter from a Content
|
||||||
|
*
|
||||||
|
* @param c
|
||||||
|
*/
|
||||||
|
public GetParameter(KadContent c)
|
||||||
|
{
|
||||||
|
this.key = c.getKey();
|
||||||
|
|
||||||
|
if (c.getType() != null)
|
||||||
|
{
|
||||||
|
this.type = c.getType();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.getOwnerId() != null)
|
||||||
|
{
|
||||||
|
this.ownerId = c.getOwnerId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct our get parameter from a StorageEntryMeta data
|
||||||
|
*
|
||||||
|
* @param md
|
||||||
|
*/
|
||||||
|
public GetParameter(KademliaStorageEntryMetadata md)
|
||||||
|
{
|
||||||
|
this.key = md.getKey();
|
||||||
|
|
||||||
|
if (md.getType() != null)
|
||||||
|
{
|
||||||
|
this.type = md.getType();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (md.getOwnerId() != null)
|
||||||
|
{
|
||||||
|
this.ownerId = md.getOwnerId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public KademliaId getKey()
|
||||||
|
{
|
||||||
|
return this.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOwnerId(String ownerId)
|
||||||
|
{
|
||||||
|
this.ownerId = ownerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOwnerId()
|
||||||
|
{
|
||||||
|
return this.ownerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setType(String type)
|
||||||
|
{
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getType()
|
||||||
|
{
|
||||||
|
return this.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return "GetParameter - [Key: " + key + "][Owner: " + this.ownerId + "][Type: " + this.type + "]";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.dht;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A JKademliaStorageEntry class that is used to store a content on the DHT
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @since 20140402
|
||||||
|
*/
|
||||||
|
public class JKademliaStorageEntry implements KademliaStorageEntry
|
||||||
|
{
|
||||||
|
|
||||||
|
private String content;
|
||||||
|
private final StorageEntryMetadata metadata;
|
||||||
|
|
||||||
|
public JKademliaStorageEntry(final KadContent content)
|
||||||
|
{
|
||||||
|
this(content, new StorageEntryMetadata(content));
|
||||||
|
}
|
||||||
|
|
||||||
|
public JKademliaStorageEntry(final KadContent content, final StorageEntryMetadata metadata)
|
||||||
|
{
|
||||||
|
this.setContent(content.toSerializedForm());
|
||||||
|
this.metadata = metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void setContent(final byte[] data)
|
||||||
|
{
|
||||||
|
this.content = new String(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final byte[] getContent()
|
||||||
|
{
|
||||||
|
return this.content.getBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final KademliaStorageEntryMetadata getContentMetadata()
|
||||||
|
{
|
||||||
|
return this.metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder("[StorageEntry: ");
|
||||||
|
|
||||||
|
sb.append("[Content: ");
|
||||||
|
sb.append(this.getContent());
|
||||||
|
sb.append("]");
|
||||||
|
|
||||||
|
sb.append(this.getContentMetadata());
|
||||||
|
|
||||||
|
sb.append("]");
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.dht;
|
||||||
|
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.KademliaId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Any piece of content that needs to be stored on the DHT
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
*
|
||||||
|
* @since 20140224
|
||||||
|
*/
|
||||||
|
public interface KadContent
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return NodeId The DHT key for this content
|
||||||
|
*/
|
||||||
|
public KademliaId getKey();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return String The type of content
|
||||||
|
*/
|
||||||
|
public String getType();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Each content will have an created date
|
||||||
|
* This allows systems to know when to delete a content form his/her machine
|
||||||
|
*
|
||||||
|
* @return long The create date of this content
|
||||||
|
*/
|
||||||
|
public long getCreatedTimestamp();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Each content will have an update timestamp
|
||||||
|
* This allows the DHT to keep only the latest version of a content
|
||||||
|
*
|
||||||
|
* @return long The timestamp of when this content was last updated
|
||||||
|
*/
|
||||||
|
public long getLastUpdatedTimestamp();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The ID of the owner of this content
|
||||||
|
*/
|
||||||
|
public String getOwnerId();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Each content needs to be in byte format for transporting and storage,
|
||||||
|
* this method takes care of that.
|
||||||
|
*
|
||||||
|
* Each object is responsible for transforming itself to byte format since the
|
||||||
|
* structure of methods may differ.
|
||||||
|
*
|
||||||
|
* @return The content in byte format
|
||||||
|
*/
|
||||||
|
public byte[] toSerializedForm();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given the Content in byte format, read it
|
||||||
|
*
|
||||||
|
* @param data The object in byte format
|
||||||
|
*
|
||||||
|
* @return A new object from the given
|
||||||
|
*/
|
||||||
|
public KadContent fromSerializedForm(byte[] data);
|
||||||
|
}
|
@ -0,0 +1,122 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.dht;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KadConfiguration;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.exceptions.ContentNotFoundException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.KademliaId;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.util.serializer.KadSerializer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main Distributed Hash Table interface that manages the entire DHT
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @since 20140523
|
||||||
|
*/
|
||||||
|
public interface KademliaDHT
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize this DHT to it's default state
|
||||||
|
*/
|
||||||
|
public void initialize();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a new configuration. Mainly used when we restore the DHT state from a file
|
||||||
|
*
|
||||||
|
* @param con The new configuration file
|
||||||
|
*/
|
||||||
|
public void setConfiguration(KadConfiguration con);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Serializer or returns an existing serializer
|
||||||
|
*
|
||||||
|
* @return The new ContentSerializer
|
||||||
|
*/
|
||||||
|
public KadSerializer<JKademliaStorageEntry> getSerializer();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle storing content locally
|
||||||
|
*
|
||||||
|
* @param content The DHT content to store
|
||||||
|
*
|
||||||
|
* @return boolean true if we stored the content, false if the content already exists and is up to date
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public boolean store(JKademliaStorageEntry content) throws IOException;
|
||||||
|
|
||||||
|
public boolean store(KadContent content) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a Content from local storage
|
||||||
|
*
|
||||||
|
* @param key The Key of the content to retrieve
|
||||||
|
* @param hashCode The hash code of the content to retrieve
|
||||||
|
*
|
||||||
|
* @return A KadContent object
|
||||||
|
*
|
||||||
|
* @throws FileNotFoundException
|
||||||
|
* @throws ClassNotFoundException
|
||||||
|
*/
|
||||||
|
public JKademliaStorageEntry retrieve(KademliaId key, int hashCode) throws FileNotFoundException, IOException, ClassNotFoundException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if any content for the given criteria exists in this DHT
|
||||||
|
*
|
||||||
|
* @param param The content search criteria
|
||||||
|
*
|
||||||
|
* @return boolean Whether any content exist that satisfy the criteria
|
||||||
|
*/
|
||||||
|
public boolean contains(GetParameter param);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve and create a KadContent object given the StorageEntry object
|
||||||
|
*
|
||||||
|
* @param entry The StorageEntry used to retrieve this content
|
||||||
|
*
|
||||||
|
* @return KadContent The content object
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public JKademliaStorageEntry get(KademliaStorageEntryMetadata entry) throws IOException, NoSuchElementException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the StorageEntry for the content if any exist.
|
||||||
|
*
|
||||||
|
* @param param The parameters used to filter the content needed
|
||||||
|
*
|
||||||
|
* @return KadContent A KadContent found on the DHT satisfying the given criteria
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public JKademliaStorageEntry get(GetParameter param) throws NoSuchElementException, IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a content from local storage
|
||||||
|
*
|
||||||
|
* @param content The Content to Remove
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @throws ContentNotFoundException
|
||||||
|
*/
|
||||||
|
public void remove(KadContent content) throws ContentNotFoundException;
|
||||||
|
|
||||||
|
public void remove(KademliaStorageEntryMetadata entry) throws ContentNotFoundException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return A List of all StorageEntries for this node
|
||||||
|
*/
|
||||||
|
public List<KademliaStorageEntryMetadata> getStorageEntries();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to add a list of storage entries for existing content to the DHT.
|
||||||
|
* Mainly used when retrieving StorageEntries from a saved state file.
|
||||||
|
*
|
||||||
|
* @param ientries The entries to add
|
||||||
|
*/
|
||||||
|
public void putStorageEntries(List<KademliaStorageEntryMetadata> ientries);
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.dht;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A StorageEntry interface for the storage entry class used to store a content on the DHT
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @since 20140523
|
||||||
|
*/
|
||||||
|
public interface KademliaStorageEntry
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the content to the storage entry
|
||||||
|
*
|
||||||
|
* @param data The content data in byte[] format
|
||||||
|
*/
|
||||||
|
public void setContent(final byte[] data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the content from this storage entry
|
||||||
|
*
|
||||||
|
* @return The content in byte format
|
||||||
|
*/
|
||||||
|
public byte[] getContent();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the metadata for this storage entry
|
||||||
|
*
|
||||||
|
* @return the storage entry metadata
|
||||||
|
*/
|
||||||
|
public KademliaStorageEntryMetadata getContentMetadata();
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.dht;
|
||||||
|
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.KademliaId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps track of data for a Content stored in the DHT
|
||||||
|
* Used by the StorageEntryManager class
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @since 20140226
|
||||||
|
*/
|
||||||
|
public interface KademliaStorageEntryMetadata
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The Kademlia ID of this content
|
||||||
|
*/
|
||||||
|
public KademliaId getKey();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The content's owner ID
|
||||||
|
*/
|
||||||
|
public String getOwnerId();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The type of this content
|
||||||
|
*/
|
||||||
|
public String getType();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return A hash of the content
|
||||||
|
*/
|
||||||
|
public int getContentHash();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The last time this content was updated
|
||||||
|
*/
|
||||||
|
public long getLastUpdatedTimestamp();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a node is looking for content, he sends the search criteria in a GetParameter object
|
||||||
|
* Here we take this GetParameter object and check if this StorageEntry satisfies the given parameters
|
||||||
|
*
|
||||||
|
* @param params
|
||||||
|
*
|
||||||
|
* @return boolean Whether this content satisfies the parameters
|
||||||
|
*/
|
||||||
|
public boolean satisfiesParameters(GetParameter params);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The timestamp for the last time this content was republished
|
||||||
|
*/
|
||||||
|
public long lastRepublished();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whenever we republish a content or get this content from the network, we update the last republished time
|
||||||
|
*/
|
||||||
|
public void updateLastRepublished();
|
||||||
|
}
|
@ -0,0 +1,152 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.dht;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.KademliaId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps track of data for a Content stored in the DHT
|
||||||
|
* Used by the StorageEntryManager class
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @since 20140226
|
||||||
|
*/
|
||||||
|
public class StorageEntryMetadata implements KademliaStorageEntryMetadata
|
||||||
|
{
|
||||||
|
|
||||||
|
private final KademliaId key;
|
||||||
|
private final String ownerId;
|
||||||
|
private final String type;
|
||||||
|
private final int contentHash;
|
||||||
|
private final long updatedTs;
|
||||||
|
|
||||||
|
/* This value is the last time this content was last updated from the network */
|
||||||
|
private long lastRepublished;
|
||||||
|
|
||||||
|
public StorageEntryMetadata(KadContent content)
|
||||||
|
{
|
||||||
|
this.key = content.getKey();
|
||||||
|
this.ownerId = content.getOwnerId();
|
||||||
|
this.type = content.getType();
|
||||||
|
this.contentHash = content.hashCode();
|
||||||
|
this.updatedTs = content.getLastUpdatedTimestamp();
|
||||||
|
|
||||||
|
this.lastRepublished = System.currentTimeMillis() / 1000L;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KademliaId getKey()
|
||||||
|
{
|
||||||
|
return this.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getOwnerId()
|
||||||
|
{
|
||||||
|
return this.ownerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getType()
|
||||||
|
{
|
||||||
|
return this.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getContentHash()
|
||||||
|
{
|
||||||
|
return this.contentHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getLastUpdatedTimestamp()
|
||||||
|
{
|
||||||
|
return this.updatedTs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a node is looking for content, he sends the search criteria in a GetParameter object
|
||||||
|
* Here we take this GetParameter object and check if this StorageEntry satisfies the given parameters
|
||||||
|
*
|
||||||
|
* @param params
|
||||||
|
*
|
||||||
|
* @return boolean Whether this content satisfies the parameters
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean satisfiesParameters(GetParameter params)
|
||||||
|
{
|
||||||
|
/* Check that owner id matches */
|
||||||
|
if ((params.getOwnerId() != null) && (!params.getOwnerId().equals(this.ownerId)))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check that type matches */
|
||||||
|
if ((params.getType() != null) && (!params.getType().equals(this.type)))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check that key matches */
|
||||||
|
if ((params.getKey() != null) && (!params.getKey().equals(this.key)))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long lastRepublished()
|
||||||
|
{
|
||||||
|
return this.lastRepublished;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whenever we republish a content or get this content from the network, we update the last republished time
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void updateLastRepublished()
|
||||||
|
{
|
||||||
|
this.lastRepublished = System.currentTimeMillis() / 1000L;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o)
|
||||||
|
{
|
||||||
|
if (o instanceof KademliaStorageEntryMetadata)
|
||||||
|
{
|
||||||
|
return this.hashCode() == o.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode()
|
||||||
|
{
|
||||||
|
int hash = 3;
|
||||||
|
hash = 23 * hash + Objects.hashCode(this.key);
|
||||||
|
hash = 23 * hash + Objects.hashCode(this.ownerId);
|
||||||
|
hash = 23 * hash + Objects.hashCode(this.type);
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder("[StorageEntry: ");
|
||||||
|
|
||||||
|
sb.append("{Key: ");
|
||||||
|
sb.append(this.key);
|
||||||
|
sb.append("} ");
|
||||||
|
sb.append("{Owner: ");
|
||||||
|
sb.append(this.ownerId);
|
||||||
|
sb.append("} ");
|
||||||
|
sb.append("{Type: ");
|
||||||
|
sb.append(this.type);
|
||||||
|
sb.append("} ");
|
||||||
|
sb.append("]");
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,202 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.dht;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.exceptions.ContentExistException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.exceptions.ContentNotFoundException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.KademliaId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It would be infeasible to keep all content in memory to be send when requested
|
||||||
|
* Instead we store content into files
|
||||||
|
* We use this Class to keep track of all content stored
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @since 20140226
|
||||||
|
*/
|
||||||
|
class StoredContentManager
|
||||||
|
{
|
||||||
|
|
||||||
|
private final Map<KademliaId, List<KademliaStorageEntryMetadata>> entries;
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
entries = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new entry to our storage
|
||||||
|
*
|
||||||
|
* @param content The content to store a reference to
|
||||||
|
*/
|
||||||
|
public KademliaStorageEntryMetadata put(KadContent content) throws ContentExistException
|
||||||
|
{
|
||||||
|
return this.put(new StorageEntryMetadata(content));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new entry to our storage
|
||||||
|
*
|
||||||
|
* @param entry The StorageEntry to store
|
||||||
|
*/
|
||||||
|
public KademliaStorageEntryMetadata put(KademliaStorageEntryMetadata entry) throws ContentExistException
|
||||||
|
{
|
||||||
|
if (!this.entries.containsKey(entry.getKey()))
|
||||||
|
{
|
||||||
|
this.entries.put(entry.getKey(), new ArrayList<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If this entry doesn't already exist, then we add it */
|
||||||
|
if (!this.contains(entry))
|
||||||
|
{
|
||||||
|
this.entries.get(entry.getKey()).add(entry);
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new ContentExistException("Content already exists on this DHT");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if our DHT has a Content for the given criteria
|
||||||
|
*
|
||||||
|
* @param param The parameters used to search for a content
|
||||||
|
*
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
public synchronized boolean contains(GetParameter param)
|
||||||
|
{
|
||||||
|
if (this.entries.containsKey(param.getKey()))
|
||||||
|
{
|
||||||
|
/* Content with this key exist, check if any match the rest of the search criteria */
|
||||||
|
for (KademliaStorageEntryMetadata e : this.entries.get(param.getKey()))
|
||||||
|
{
|
||||||
|
/* If any entry satisfies the given parameters, return true */
|
||||||
|
if (e.satisfiesParameters(param))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a content exist in the DHT
|
||||||
|
*/
|
||||||
|
public synchronized boolean contains(KadContent content)
|
||||||
|
{
|
||||||
|
return this.contains(new GetParameter(content));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a StorageEntry exist on this DHT
|
||||||
|
*/
|
||||||
|
public synchronized boolean contains(KademliaStorageEntryMetadata entry)
|
||||||
|
{
|
||||||
|
return this.contains(new GetParameter(entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if our DHT has a Content for the given criteria
|
||||||
|
*
|
||||||
|
* @param param The parameters used to search for a content
|
||||||
|
*
|
||||||
|
* @return List of content for the specific search parameters
|
||||||
|
*/
|
||||||
|
public KademliaStorageEntryMetadata get(GetParameter param) throws NoSuchElementException
|
||||||
|
{
|
||||||
|
if (this.entries.containsKey(param.getKey()))
|
||||||
|
{
|
||||||
|
/* Content with this key exist, check if any match the rest of the search criteria */
|
||||||
|
for (KademliaStorageEntryMetadata e : this.entries.get(param.getKey()))
|
||||||
|
{
|
||||||
|
/* If any entry satisfies the given parameters, return true */
|
||||||
|
if (e.satisfiesParameters(param))
|
||||||
|
{
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If we got here, means we didn't find any entry */
|
||||||
|
throw new NoSuchElementException();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new NoSuchElementException("No content exist for the given parameters");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public KademliaStorageEntryMetadata get(KademliaStorageEntryMetadata md)
|
||||||
|
{
|
||||||
|
return this.get(new GetParameter(md));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return A list of all storage entries
|
||||||
|
*/
|
||||||
|
public synchronized List<KademliaStorageEntryMetadata> getAllEntries()
|
||||||
|
{
|
||||||
|
List<KademliaStorageEntryMetadata> entriesRet = new ArrayList<>();
|
||||||
|
|
||||||
|
for (List<KademliaStorageEntryMetadata> entrySet : this.entries.values())
|
||||||
|
{
|
||||||
|
if (entrySet.size() > 0)
|
||||||
|
{
|
||||||
|
entriesRet.addAll(entrySet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entriesRet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void remove(KadContent content) throws ContentNotFoundException
|
||||||
|
{
|
||||||
|
this.remove(new StorageEntryMetadata(content));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void remove(KademliaStorageEntryMetadata entry) throws ContentNotFoundException
|
||||||
|
{
|
||||||
|
if (contains(entry))
|
||||||
|
{
|
||||||
|
this.entries.get(entry.getKey()).remove(entry);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new ContentNotFoundException("This content does not exist in the Storage Entries");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized String toString()
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder("Stored Content: \n");
|
||||||
|
int count = 0;
|
||||||
|
for (List<KademliaStorageEntryMetadata> es : this.entries.values())
|
||||||
|
{
|
||||||
|
if (entries.size() < 1)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (KademliaStorageEntryMetadata e : es)
|
||||||
|
{
|
||||||
|
sb.append(++count);
|
||||||
|
sb.append(". ");
|
||||||
|
sb.append(e);
|
||||||
|
sb.append("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.append("\n");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.exceptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An exception used to indicate that a content already exist on the DHT
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @created 20140322
|
||||||
|
*/
|
||||||
|
public class ContentExistException extends Exception
|
||||||
|
{
|
||||||
|
|
||||||
|
public ContentExistException()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ContentExistException(String message)
|
||||||
|
{
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.exceptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An exception used to indicate that a content does not exist on the DHT
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @created 20140322
|
||||||
|
*/
|
||||||
|
public class ContentNotFoundException extends Exception
|
||||||
|
{
|
||||||
|
|
||||||
|
public ContentNotFoundException()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ContentNotFoundException(String message)
|
||||||
|
{
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.exceptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An exception to be thrown whenever the Kad Server is down
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @created 20140428
|
||||||
|
*/
|
||||||
|
public class KadServerDownException extends RoutingException
|
||||||
|
{
|
||||||
|
|
||||||
|
public KadServerDownException()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public KadServerDownException(String message)
|
||||||
|
{
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.exceptions;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An exception to be thrown whenever there is a routing problem
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @created 20140219
|
||||||
|
*/
|
||||||
|
public class RoutingException extends IOException
|
||||||
|
{
|
||||||
|
|
||||||
|
public RoutingException()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public RoutingException(String message)
|
||||||
|
{
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.exceptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An exception used to indicate an unknown message type or communication identifier
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @created 20140219
|
||||||
|
*/
|
||||||
|
public class UnknownMessageException extends RuntimeException
|
||||||
|
{
|
||||||
|
|
||||||
|
public UnknownMessageException()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public UnknownMessageException(String message)
|
||||||
|
{
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.message;
|
||||||
|
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.Node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A message used to acknowledge a request from a node; can be used in many situations.
|
||||||
|
* - Mainly used to acknowledge a connect message
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @created 20140218
|
||||||
|
*/
|
||||||
|
public class AcknowledgeMessage implements Message
|
||||||
|
{
|
||||||
|
|
||||||
|
private Node origin;
|
||||||
|
public static final byte CODE = 0x01;
|
||||||
|
|
||||||
|
public AcknowledgeMessage(Node origin)
|
||||||
|
{
|
||||||
|
this.origin = origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AcknowledgeMessage(DataInputStream in) throws IOException
|
||||||
|
{
|
||||||
|
this.fromStream(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void fromStream(DataInputStream in) throws IOException
|
||||||
|
{
|
||||||
|
this.origin = new Node(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void toStream(DataOutputStream out) throws IOException
|
||||||
|
{
|
||||||
|
origin.toStream(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Node getOrigin()
|
||||||
|
{
|
||||||
|
return this.origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte code()
|
||||||
|
{
|
||||||
|
return CODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return "AcknowledgeMessage[origin=" + origin.getNodeId() + "]";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.message;
|
||||||
|
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.Node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A message sent to another node requesting to connect to them.
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @created 20140218
|
||||||
|
*/
|
||||||
|
public class ConnectMessage implements Message
|
||||||
|
{
|
||||||
|
|
||||||
|
private Node origin;
|
||||||
|
public static final byte CODE = 0x02;
|
||||||
|
|
||||||
|
public ConnectMessage(Node origin)
|
||||||
|
{
|
||||||
|
this.origin = origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ConnectMessage(DataInputStream in) throws IOException
|
||||||
|
{
|
||||||
|
this.fromStream(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void fromStream(DataInputStream in) throws IOException
|
||||||
|
{
|
||||||
|
this.origin = new Node(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void toStream(DataOutputStream out) throws IOException
|
||||||
|
{
|
||||||
|
origin.toStream(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Node getOrigin()
|
||||||
|
{
|
||||||
|
return this.origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte code()
|
||||||
|
{
|
||||||
|
return CODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return "ConnectMessage[origin NodeId=" + origin.getNodeId() + "]";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.message;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KadServer;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KademliaNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receives a ConnectMessage and sends an AcknowledgeMessage as reply.
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @created 20140219
|
||||||
|
*/
|
||||||
|
public class ConnectReceiver implements Receiver
|
||||||
|
{
|
||||||
|
|
||||||
|
private final KadServer server;
|
||||||
|
private final KademliaNode localNode;
|
||||||
|
|
||||||
|
public ConnectReceiver(KadServer server, KademliaNode local)
|
||||||
|
{
|
||||||
|
this.server = server;
|
||||||
|
this.localNode = local;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle receiving a ConnectMessage
|
||||||
|
*
|
||||||
|
* @param comm
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void receive(Message incoming, int comm) throws IOException
|
||||||
|
{
|
||||||
|
ConnectMessage mess = (ConnectMessage) incoming;
|
||||||
|
|
||||||
|
/* Update the local space by inserting the origin node. */
|
||||||
|
this.localNode.getRoutingTable().insert(mess.getOrigin());
|
||||||
|
|
||||||
|
/* Respond to the connect request */
|
||||||
|
AcknowledgeMessage msg = new AcknowledgeMessage(this.localNode.getNode());
|
||||||
|
|
||||||
|
/* Reply to the connect message with an Acknowledgement */
|
||||||
|
this.server.reply(mess.getOrigin(), msg, comm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We don't need to do anything here
|
||||||
|
*
|
||||||
|
* @param comm
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void timeout(int comm) throws IOException
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.message;
|
||||||
|
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.dht.GetParameter;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.Node;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.util.serializer.JsonSerializer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Messages used to send to another node requesting content.
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @since 20140226
|
||||||
|
*/
|
||||||
|
public class ContentLookupMessage implements Message
|
||||||
|
{
|
||||||
|
|
||||||
|
public static final byte CODE = 0x03;
|
||||||
|
|
||||||
|
private Node origin;
|
||||||
|
private GetParameter params;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param origin The node where this lookup came from
|
||||||
|
* @param params The parameters used to find the content
|
||||||
|
*/
|
||||||
|
public ContentLookupMessage(Node origin, GetParameter params)
|
||||||
|
{
|
||||||
|
this.origin = origin;
|
||||||
|
this.params = params;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ContentLookupMessage(DataInputStream in) throws IOException
|
||||||
|
{
|
||||||
|
this.fromStream(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GetParameter getParameters()
|
||||||
|
{
|
||||||
|
return this.params;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Node getOrigin()
|
||||||
|
{
|
||||||
|
return this.origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void toStream(DataOutputStream out) throws IOException
|
||||||
|
{
|
||||||
|
this.origin.toStream(out);
|
||||||
|
|
||||||
|
/* Write the params to the stream */
|
||||||
|
new JsonSerializer<GetParameter>().write(this.params, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void fromStream(DataInputStream in) throws IOException
|
||||||
|
{
|
||||||
|
this.origin = new Node(in);
|
||||||
|
|
||||||
|
/* Read the params from the stream */
|
||||||
|
try
|
||||||
|
{
|
||||||
|
this.params = new JsonSerializer<GetParameter>().read(in);
|
||||||
|
}
|
||||||
|
catch (ClassNotFoundException e)
|
||||||
|
{
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte code()
|
||||||
|
{
|
||||||
|
return CODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.message;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KadConfiguration;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KadServer;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KademliaNode;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.dht.KademliaDHT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Responds to a ContentLookupMessage by sending a ContentMessage containing the requested content;
|
||||||
|
* if the requested content is not found, a NodeReplyMessage containing the K closest nodes to the request key is sent.
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @since 20140226
|
||||||
|
*/
|
||||||
|
public class ContentLookupReceiver implements Receiver
|
||||||
|
{
|
||||||
|
|
||||||
|
private final KadServer server;
|
||||||
|
private final KademliaNode localNode;
|
||||||
|
private final KademliaDHT dht;
|
||||||
|
private final KadConfiguration config;
|
||||||
|
|
||||||
|
public ContentLookupReceiver(KadServer server, KademliaNode localNode, KademliaDHT dht, KadConfiguration config)
|
||||||
|
{
|
||||||
|
this.server = server;
|
||||||
|
this.localNode = localNode;
|
||||||
|
this.dht = dht;
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void receive(Message incoming, int comm) throws IOException
|
||||||
|
{
|
||||||
|
ContentLookupMessage msg = (ContentLookupMessage) incoming;
|
||||||
|
this.localNode.getRoutingTable().insert(msg.getOrigin());
|
||||||
|
|
||||||
|
/* Check if we can have this data */
|
||||||
|
if (this.dht.contains(msg.getParameters()))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
/* Return a ContentMessage with the required data */
|
||||||
|
ContentMessage cMsg = new ContentMessage(localNode.getNode(), this.dht.get(msg.getParameters()));
|
||||||
|
server.reply(msg.getOrigin(), cMsg, comm);
|
||||||
|
}
|
||||||
|
catch (NoSuchElementException ex)
|
||||||
|
{
|
||||||
|
/* @todo Not sure why this exception is thrown here, checkup the system when tests are writtem*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Return a the K closest nodes to this content identifier
|
||||||
|
* We create a NodeLookupReceiver and let this receiver handle this operation
|
||||||
|
*/
|
||||||
|
NodeLookupMessage lkpMsg = new NodeLookupMessage(msg.getOrigin(), msg.getParameters().getKey());
|
||||||
|
new NodeLookupReceiver(server, localNode, this.config).receive(lkpMsg, comm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void timeout(int comm)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.message;
|
||||||
|
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.dht.JKademliaStorageEntry;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.Node;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.util.serializer.JsonSerializer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Message used to send content between nodes
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @since 20140226
|
||||||
|
*/
|
||||||
|
public class ContentMessage implements Message
|
||||||
|
{
|
||||||
|
|
||||||
|
public static final byte CODE = 0x04;
|
||||||
|
|
||||||
|
private JKademliaStorageEntry content;
|
||||||
|
private Node origin;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param origin Where the message came from
|
||||||
|
* @param content The content to be stored
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public ContentMessage(Node origin, JKademliaStorageEntry content)
|
||||||
|
{
|
||||||
|
this.content = content;
|
||||||
|
this.origin = origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ContentMessage(DataInputStream in) throws IOException
|
||||||
|
{
|
||||||
|
this.fromStream(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void toStream(DataOutputStream out) throws IOException
|
||||||
|
{
|
||||||
|
this.origin.toStream(out);
|
||||||
|
|
||||||
|
/* Serialize the KadContent, then send it to the stream */
|
||||||
|
new JsonSerializer<JKademliaStorageEntry>().write(content, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void fromStream(DataInputStream in) throws IOException
|
||||||
|
{
|
||||||
|
this.origin = new Node(in);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
this.content = new JsonSerializer<JKademliaStorageEntry>().read(in);
|
||||||
|
}
|
||||||
|
catch (ClassNotFoundException e)
|
||||||
|
{
|
||||||
|
System.err.println("ClassNotFoundException when reading StorageEntry; Message: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Node getOrigin()
|
||||||
|
{
|
||||||
|
return this.origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JKademliaStorageEntry getContent()
|
||||||
|
{
|
||||||
|
return this.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte code()
|
||||||
|
{
|
||||||
|
return CODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return "ContentMessage[origin=" + origin + ",content=" + content + "]";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.message;
|
||||||
|
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KadServer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A factory that handles creating messages and receivers
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @since 20140523
|
||||||
|
*/
|
||||||
|
public interface KademliaMessageFactory
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method that creates a message based on the code and input stream
|
||||||
|
*
|
||||||
|
* @param code The message code
|
||||||
|
* @param in An input stream with the message data
|
||||||
|
*
|
||||||
|
* @return A message
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public Message createMessage(byte code, DataInputStream in) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method that returns a receiver to handle a specific type of message
|
||||||
|
*
|
||||||
|
* @param code The message code
|
||||||
|
* @param server
|
||||||
|
*
|
||||||
|
* @return A receiver
|
||||||
|
*/
|
||||||
|
public Receiver createReceiver(byte code, KadServer server);
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.message;
|
||||||
|
|
||||||
|
public interface Message extends Streamable
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The unique code for the message type, used to differentiate all messages
|
||||||
|
* from each other. Since this is of <code>byte</code> type there can
|
||||||
|
* be at most 256 different message types.
|
||||||
|
*
|
||||||
|
* @return byte A unique code representing the message type
|
||||||
|
* */
|
||||||
|
public byte code();
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.message;
|
||||||
|
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KadConfiguration;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KadServer;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KademliaNode;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.dht.KademliaDHT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles creating messages and receivers
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @since 20140202
|
||||||
|
*/
|
||||||
|
public class MessageFactory implements KademliaMessageFactory
|
||||||
|
{
|
||||||
|
|
||||||
|
private final KademliaNode localNode;
|
||||||
|
private final KademliaDHT dht;
|
||||||
|
private final KadConfiguration config;
|
||||||
|
|
||||||
|
public MessageFactory(KademliaNode local, KademliaDHT dht, KadConfiguration config)
|
||||||
|
{
|
||||||
|
this.localNode = local;
|
||||||
|
this.dht = dht;
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Message createMessage(byte code, DataInputStream in) throws IOException
|
||||||
|
{
|
||||||
|
switch (code)
|
||||||
|
{
|
||||||
|
case AcknowledgeMessage.CODE:
|
||||||
|
return new AcknowledgeMessage(in);
|
||||||
|
case ConnectMessage.CODE:
|
||||||
|
return new ConnectMessage(in);
|
||||||
|
case ContentMessage.CODE:
|
||||||
|
return new ContentMessage(in);
|
||||||
|
case ContentLookupMessage.CODE:
|
||||||
|
return new ContentLookupMessage(in);
|
||||||
|
case NodeLookupMessage.CODE:
|
||||||
|
return new NodeLookupMessage(in);
|
||||||
|
case NodeReplyMessage.CODE:
|
||||||
|
return new NodeReplyMessage(in);
|
||||||
|
case SimpleMessage.CODE:
|
||||||
|
return new SimpleMessage(in);
|
||||||
|
case StoreContentMessage.CODE:
|
||||||
|
return new StoreContentMessage(in);
|
||||||
|
default:
|
||||||
|
//System.out.println(this.localNode + " - No Message handler found for message. Code: " + code);
|
||||||
|
return new SimpleMessage(in);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Receiver createReceiver(byte code, KadServer server)
|
||||||
|
{
|
||||||
|
switch (code)
|
||||||
|
{
|
||||||
|
case ConnectMessage.CODE:
|
||||||
|
return new ConnectReceiver(server, this.localNode);
|
||||||
|
case ContentLookupMessage.CODE:
|
||||||
|
return new ContentLookupReceiver(server, this.localNode, this.dht, this.config);
|
||||||
|
case NodeLookupMessage.CODE:
|
||||||
|
return new NodeLookupReceiver(server, this.localNode, this.config);
|
||||||
|
case StoreContentMessage.CODE:
|
||||||
|
return new StoreContentReceiver(server, this.localNode, this.dht);
|
||||||
|
default:
|
||||||
|
//System.out.println("No receiver found for message. Code: " + code);
|
||||||
|
return new SimpleReceiver();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,75 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.message;
|
||||||
|
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.Node;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.KademliaId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A message sent to other nodes requesting the K-Closest nodes to a key sent in this message.
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @created 20140218
|
||||||
|
*/
|
||||||
|
public class NodeLookupMessage implements Message
|
||||||
|
{
|
||||||
|
|
||||||
|
private Node origin;
|
||||||
|
private KademliaId lookupId;
|
||||||
|
|
||||||
|
public static final byte CODE = 0x05;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A new NodeLookupMessage to find nodes
|
||||||
|
*
|
||||||
|
* @param origin The Node from which the message is coming from
|
||||||
|
* @param lookup The key for which to lookup nodes for
|
||||||
|
*/
|
||||||
|
public NodeLookupMessage(Node origin, KademliaId lookup)
|
||||||
|
{
|
||||||
|
this.origin = origin;
|
||||||
|
this.lookupId = lookup;
|
||||||
|
}
|
||||||
|
|
||||||
|
public NodeLookupMessage(DataInputStream in) throws IOException
|
||||||
|
{
|
||||||
|
this.fromStream(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void fromStream(DataInputStream in) throws IOException
|
||||||
|
{
|
||||||
|
this.origin = new Node(in);
|
||||||
|
this.lookupId = new KademliaId(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void toStream(DataOutputStream out) throws IOException
|
||||||
|
{
|
||||||
|
this.origin.toStream(out);
|
||||||
|
this.lookupId.toStream(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Node getOrigin()
|
||||||
|
{
|
||||||
|
return this.origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KademliaId getLookupId()
|
||||||
|
{
|
||||||
|
return this.lookupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte code()
|
||||||
|
{
|
||||||
|
return CODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return "NodeLookupMessage[origin=" + origin + ",lookup=" + lookupId + "]";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,72 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.message;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KadConfiguration;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KadServer;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KademliaNode;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.Node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receives a NodeLookupMessage and sends a NodeReplyMessage as reply with the K-Closest nodes to the ID sent.
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @created 20140219
|
||||||
|
*/
|
||||||
|
public class NodeLookupReceiver implements Receiver
|
||||||
|
{
|
||||||
|
|
||||||
|
private final KadServer server;
|
||||||
|
private final KademliaNode localNode;
|
||||||
|
private final KadConfiguration config;
|
||||||
|
|
||||||
|
public NodeLookupReceiver(KadServer server, KademliaNode local, KadConfiguration config)
|
||||||
|
{
|
||||||
|
this.server = server;
|
||||||
|
this.localNode = local;
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle receiving a NodeLookupMessage
|
||||||
|
* Find the set of K nodes closest to the lookup ID and return them
|
||||||
|
*
|
||||||
|
* @param comm
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void receive(Message incoming, int comm) throws IOException
|
||||||
|
{
|
||||||
|
NodeLookupMessage msg = (NodeLookupMessage) incoming;
|
||||||
|
|
||||||
|
Node origin = msg.getOrigin();
|
||||||
|
|
||||||
|
/* Update the local space by inserting the origin node. */
|
||||||
|
this.localNode.getRoutingTable().insert(origin);
|
||||||
|
|
||||||
|
/* Find nodes closest to the LookupId */
|
||||||
|
List<Node> nodes = this.localNode.getRoutingTable().findClosest(msg.getLookupId(), this.config.k());
|
||||||
|
|
||||||
|
/* Respond to the NodeLookupMessage */
|
||||||
|
Message reply = new NodeReplyMessage(this.localNode.getNode(), nodes);
|
||||||
|
|
||||||
|
if (this.server.isRunning())
|
||||||
|
{
|
||||||
|
/* Let the Server send the reply */
|
||||||
|
this.server.reply(origin, reply, comm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We don't need to do anything here
|
||||||
|
*
|
||||||
|
* @param comm
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void timeout(int comm) throws IOException
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,94 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.message;
|
||||||
|
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.Node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A message used to connect nodes.
|
||||||
|
* When a NodeLookup Request comes in, we respond with a NodeReplyMessage.
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @created 20140218
|
||||||
|
*/
|
||||||
|
public class NodeReplyMessage implements Message
|
||||||
|
{
|
||||||
|
|
||||||
|
private Node origin;
|
||||||
|
public static final byte CODE = 0x06;
|
||||||
|
private List<Node> nodes;
|
||||||
|
|
||||||
|
public NodeReplyMessage(Node origin, List<Node> nodes)
|
||||||
|
{
|
||||||
|
this.origin = origin;
|
||||||
|
this.nodes = nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public NodeReplyMessage(DataInputStream in) throws IOException
|
||||||
|
{
|
||||||
|
this.fromStream(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void fromStream(DataInputStream in) throws IOException
|
||||||
|
{
|
||||||
|
/* Read in the origin */
|
||||||
|
this.origin = new Node(in);
|
||||||
|
|
||||||
|
/* Get the number of incoming nodes */
|
||||||
|
int len = in.readInt();
|
||||||
|
this.nodes = new ArrayList<>(len);
|
||||||
|
|
||||||
|
/* Read in all nodes */
|
||||||
|
for (int i = 0; i < len; i++)
|
||||||
|
{
|
||||||
|
this.nodes.add(new Node(in));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void toStream(DataOutputStream out) throws IOException
|
||||||
|
{
|
||||||
|
/* Add the origin node to the stream */
|
||||||
|
origin.toStream(out);
|
||||||
|
|
||||||
|
/* Add all other nodes to the stream */
|
||||||
|
int len = this.nodes.size();
|
||||||
|
if (len > 255)
|
||||||
|
{
|
||||||
|
throw new IndexOutOfBoundsException("Too many nodes in list to send in NodeReplyMessage. Size: " + len);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Writing the nodes to the stream */
|
||||||
|
out.writeInt(len);
|
||||||
|
for (Node n : this.nodes)
|
||||||
|
{
|
||||||
|
n.toStream(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Node getOrigin()
|
||||||
|
{
|
||||||
|
return this.origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte code()
|
||||||
|
{
|
||||||
|
return CODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Node> getNodes()
|
||||||
|
{
|
||||||
|
return this.nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return "NodeReplyMessage[origin NodeId=" + origin.getNodeId() + "]";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.message;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A receiver waits for incoming messages and perform some action when the message is received
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @created 20140218
|
||||||
|
*/
|
||||||
|
public interface Receiver
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message is received, now handle it
|
||||||
|
*
|
||||||
|
* @param conversationId The ID of this conversation, used for further conversations
|
||||||
|
* @param incoming The incoming
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public void receive(Message incoming, int conversationId) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If no reply is received in <code>MessageServer.TIMEOUT</code> seconds for the
|
||||||
|
* message with communication id <code>comm</code>, the MessageServer calls this method
|
||||||
|
*
|
||||||
|
* @param conversationId The conversation ID of this communication
|
||||||
|
*
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
* */
|
||||||
|
public void timeout(int conversationId) throws IOException;
|
||||||
|
}
|
@ -0,0 +1,72 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.message;
|
||||||
|
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple message used for testing the system; Default message constructed if the message type sent is not available
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @created 20140217
|
||||||
|
*/
|
||||||
|
public class SimpleMessage implements Message
|
||||||
|
{
|
||||||
|
|
||||||
|
/* Message constants */
|
||||||
|
public static final byte CODE = 0x07;
|
||||||
|
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
public SimpleMessage(String message)
|
||||||
|
{
|
||||||
|
this.content = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SimpleMessage(DataInputStream in)
|
||||||
|
{
|
||||||
|
this.fromStream(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte code()
|
||||||
|
{
|
||||||
|
return CODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void toStream(DataOutputStream out)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
out.writeInt(this.content.length());
|
||||||
|
out.writeBytes(this.content);
|
||||||
|
}
|
||||||
|
catch (IOException e)
|
||||||
|
{
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void fromStream(DataInputStream in)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
byte[] buff = new byte[in.readInt()];
|
||||||
|
in.readFully(buff);
|
||||||
|
|
||||||
|
this.content = new String(buff);
|
||||||
|
}
|
||||||
|
catch (IOException e)
|
||||||
|
{
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return this.content;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.message;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default receiver if none other is called
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @created 20140202
|
||||||
|
*/
|
||||||
|
public class SimpleReceiver implements Receiver
|
||||||
|
{
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void receive(Message incoming, int conversationId)
|
||||||
|
{
|
||||||
|
//System.out.println("Received message: " + incoming);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void timeout(int conversationId) throws IOException
|
||||||
|
{
|
||||||
|
//System.out.println("SimpleReceiver message timeout.");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.message;
|
||||||
|
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.dht.JKademliaStorageEntry;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.Node;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.util.serializer.JsonSerializer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A StoreContentMessage used to send a store message to a node
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @since 20140225
|
||||||
|
*/
|
||||||
|
public class StoreContentMessage implements Message
|
||||||
|
{
|
||||||
|
|
||||||
|
public static final byte CODE = 0x08;
|
||||||
|
|
||||||
|
private JKademliaStorageEntry content;
|
||||||
|
private Node origin;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param origin Where the message came from
|
||||||
|
* @param content The content to be stored
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public StoreContentMessage(Node origin, JKademliaStorageEntry content)
|
||||||
|
{
|
||||||
|
this.content = content;
|
||||||
|
this.origin = origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StoreContentMessage(DataInputStream in) throws IOException
|
||||||
|
{
|
||||||
|
this.fromStream(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void toStream(DataOutputStream out) throws IOException
|
||||||
|
{
|
||||||
|
this.origin.toStream(out);
|
||||||
|
|
||||||
|
/* Serialize the KadContent, then send it to the stream */
|
||||||
|
new JsonSerializer<JKademliaStorageEntry>().write(content, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void fromStream(DataInputStream in) throws IOException
|
||||||
|
{
|
||||||
|
this.origin = new Node(in);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
this.content = new JsonSerializer<JKademliaStorageEntry>().read(in);
|
||||||
|
}
|
||||||
|
catch (ClassNotFoundException e)
|
||||||
|
{
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Node getOrigin()
|
||||||
|
{
|
||||||
|
return this.origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JKademliaStorageEntry getContent()
|
||||||
|
{
|
||||||
|
return this.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte code()
|
||||||
|
{
|
||||||
|
return CODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return "StoreContentMessage[origin=" + origin + ",content=" + content + "]";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.message;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KadServer;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KademliaNode;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.dht.KademliaDHT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receiver for incoming StoreContentMessage
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @since 20140225
|
||||||
|
*/
|
||||||
|
public class StoreContentReceiver implements Receiver
|
||||||
|
{
|
||||||
|
|
||||||
|
private final KadServer server;
|
||||||
|
private final KademliaNode localNode;
|
||||||
|
private final KademliaDHT dht;
|
||||||
|
|
||||||
|
public StoreContentReceiver(KadServer server, KademliaNode localNode, KademliaDHT dht)
|
||||||
|
{
|
||||||
|
this.server = server;
|
||||||
|
this.localNode = localNode;
|
||||||
|
this.dht = dht;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void receive(Message incoming, int comm)
|
||||||
|
{
|
||||||
|
/* It's a StoreContentMessage we're receiving */
|
||||||
|
StoreContentMessage msg = (StoreContentMessage) incoming;
|
||||||
|
|
||||||
|
/* Insert the message sender into this node's routing table */
|
||||||
|
this.localNode.getRoutingTable().insert(msg.getOrigin());
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
/* Store this Content into the DHT */
|
||||||
|
this.dht.store(msg.getContent());
|
||||||
|
}
|
||||||
|
catch (IOException e)
|
||||||
|
{
|
||||||
|
System.err.println("Unable to store received content; Message: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void timeout(int comm)
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* This receiver only handles Receiving content when we've received the message,
|
||||||
|
* so no timeout will happen with this receiver.
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.message;
|
||||||
|
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Streamable object is able to write it's state to an output stream and
|
||||||
|
* a class implementing Streamable must be able to recreate an instance of
|
||||||
|
* the class from an input stream. No information about class name is written
|
||||||
|
* to the output stream so it must be known what class type is expected when
|
||||||
|
* reading objects back in from an input stream. This gives a space
|
||||||
|
* advantage over Serializable.
|
||||||
|
* <p>
|
||||||
|
* Since the exact class must be known anyway prior to reading, it is incouraged
|
||||||
|
* that classes implementing Streamble also provide a constructor of the form:
|
||||||
|
* <p>
|
||||||
|
* <code>Streamable(DataInput in) throws IOException;</code>
|
||||||
|
* */
|
||||||
|
public interface Streamable
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the internal state of the Streamable object to the output stream
|
||||||
|
* in a format that can later be read by the same Streamble class using
|
||||||
|
* the {@link #fromStream} method.
|
||||||
|
*
|
||||||
|
* @param out
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public void toStream(DataOutputStream out) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the internal state of the Streamable object from the input stream.
|
||||||
|
*
|
||||||
|
* @param out
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public void fromStream(DataInputStream out) throws IOException;
|
||||||
|
}
|
@ -0,0 +1,264 @@
|
|||||||
|
/**
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @created 20140215
|
||||||
|
* @desc Represents a Kademlia Node ID
|
||||||
|
*/
|
||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.node;
|
||||||
|
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.BitSet;
|
||||||
|
import java.util.Random;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.message.Streamable;
|
||||||
|
|
||||||
|
import javax.xml.bind.DatatypeConverter;
|
||||||
|
|
||||||
|
public class KademliaId implements Streamable, Serializable
|
||||||
|
{
|
||||||
|
|
||||||
|
public final transient static int ID_LENGTH = 160;
|
||||||
|
private byte[] keyBytes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct the NodeId from some string
|
||||||
|
*
|
||||||
|
* @param data The user generated key string
|
||||||
|
*/
|
||||||
|
public KademliaId(String data)
|
||||||
|
{
|
||||||
|
keyBytes = DatatypeConverter.parseHexBinary(data);
|
||||||
|
if (keyBytes.length != ID_LENGTH / 8)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("Specified Data need to be " + (ID_LENGTH / 8) + " characters long.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a random key
|
||||||
|
*/
|
||||||
|
public KademliaId()
|
||||||
|
{
|
||||||
|
keyBytes = new byte[ID_LENGTH / 8];
|
||||||
|
new Random().nextBytes(keyBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the NodeId from a given byte[]
|
||||||
|
*
|
||||||
|
* @param bytes
|
||||||
|
*/
|
||||||
|
public KademliaId(byte[] bytes)
|
||||||
|
{
|
||||||
|
/*if (bytes.length != ID_LENGTH / 8)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("Specified Data need to be " + (ID_LENGTH / 8) + " characters long. Data Given: '" + new String(bytes) + "'");
|
||||||
|
}*/
|
||||||
|
this.keyBytes = bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the NodeId from a DataInput stream
|
||||||
|
*
|
||||||
|
* @param in The stream from which to load the NodeId
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public KademliaId(DataInputStream in) throws IOException
|
||||||
|
{
|
||||||
|
this.fromStream(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getBytes()
|
||||||
|
{
|
||||||
|
return this.keyBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The BigInteger representation of the key
|
||||||
|
*/
|
||||||
|
public BigInteger getInt()
|
||||||
|
{
|
||||||
|
return new BigInteger(1, this.getBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares a NodeId to this NodeId
|
||||||
|
*
|
||||||
|
* @param o The NodeId to compare to this NodeId
|
||||||
|
*
|
||||||
|
* @return boolean Whether the 2 NodeIds are equal
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o)
|
||||||
|
{
|
||||||
|
if (o instanceof KademliaId)
|
||||||
|
{
|
||||||
|
KademliaId nid = (KademliaId) o;
|
||||||
|
return this.hashCode() == nid.hashCode();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode()
|
||||||
|
{
|
||||||
|
int hash = 7;
|
||||||
|
hash = 83 * hash + Arrays.hashCode(this.keyBytes);
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the distance between this and another NodeId
|
||||||
|
*
|
||||||
|
* @param nid
|
||||||
|
*
|
||||||
|
* @return The distance of this NodeId from the given NodeId
|
||||||
|
*/
|
||||||
|
public KademliaId xor(KademliaId nid)
|
||||||
|
{
|
||||||
|
byte[] result = new byte[ID_LENGTH / 8];
|
||||||
|
byte[] nidBytes = nid.getBytes();
|
||||||
|
|
||||||
|
for (int i = 0; i < ID_LENGTH / 8; i++)
|
||||||
|
{
|
||||||
|
result[i] = (byte) (this.keyBytes[i] ^ nidBytes[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
KademliaId resNid = new KademliaId(result);
|
||||||
|
|
||||||
|
return resNid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a NodeId that is some distance away from this NodeId
|
||||||
|
*
|
||||||
|
* @param distance in number of bits
|
||||||
|
*
|
||||||
|
* @return NodeId The newly generated NodeId
|
||||||
|
*/
|
||||||
|
public KademliaId generateNodeIdByDistance(int distance)
|
||||||
|
{
|
||||||
|
byte[] result = new byte[ID_LENGTH / 8];
|
||||||
|
|
||||||
|
/* Since distance = ID_LENGTH - prefixLength, we need to fill that amount with 0's */
|
||||||
|
int numByteZeroes = (ID_LENGTH - distance) / 8;
|
||||||
|
int numBitZeroes = 8 - (distance % 8);
|
||||||
|
|
||||||
|
/* Filling byte zeroes */
|
||||||
|
for (int i = 0; i < numByteZeroes; i++)
|
||||||
|
{
|
||||||
|
result[i] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filling bit zeroes */
|
||||||
|
BitSet bits = new BitSet(8);
|
||||||
|
bits.set(0, 8);
|
||||||
|
|
||||||
|
for (int i = 0; i < numBitZeroes; i++)
|
||||||
|
{
|
||||||
|
/* Shift 1 zero into the start of the value */
|
||||||
|
bits.clear(i);
|
||||||
|
}
|
||||||
|
bits.flip(0, 8); // Flip the bits since they're in reverse order
|
||||||
|
result[numByteZeroes] = (byte) bits.toByteArray()[0];
|
||||||
|
|
||||||
|
/* Set the remaining bytes to Maximum value */
|
||||||
|
for (int i = numByteZeroes + 1; i < result.length; i++)
|
||||||
|
{
|
||||||
|
result[i] = Byte.MAX_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.xor(new KademliaId(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts the number of leading 0's in this NodeId
|
||||||
|
*
|
||||||
|
* @return Integer The number of leading 0's
|
||||||
|
*/
|
||||||
|
public int getFirstSetBitIndex()
|
||||||
|
{
|
||||||
|
int prefixLength = 0;
|
||||||
|
|
||||||
|
for (byte b : this.keyBytes)
|
||||||
|
{
|
||||||
|
if (b == 0)
|
||||||
|
{
|
||||||
|
prefixLength += 8;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* If the byte is not 0, we need to count how many MSBs are 0 */
|
||||||
|
int count = 0;
|
||||||
|
for (int i = 7; i >= 0; i--)
|
||||||
|
{
|
||||||
|
boolean a = (b & (1 << i)) == 0;
|
||||||
|
if (a)
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break; // Reset the count if we encounter a non-zero number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add the count of MSB 0s to the prefix length */
|
||||||
|
prefixLength += count;
|
||||||
|
|
||||||
|
/* Break here since we've now covered the MSB 0s */
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return prefixLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the distance from this NodeId to another NodeId
|
||||||
|
*
|
||||||
|
* @param to
|
||||||
|
*
|
||||||
|
* @return Integer The distance
|
||||||
|
*/
|
||||||
|
public int getDistance(KademliaId to)
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Compute the xor of this and to
|
||||||
|
* Get the index i of the first set bit of the xor returned NodeId
|
||||||
|
* The distance between them is ID_LENGTH - i
|
||||||
|
*/
|
||||||
|
return ID_LENGTH - this.xor(to).getFirstSetBitIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void toStream(DataOutputStream out) throws IOException
|
||||||
|
{
|
||||||
|
/* Add the NodeId to the stream */
|
||||||
|
out.write(this.getBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void fromStream(DataInputStream in) throws IOException
|
||||||
|
{
|
||||||
|
byte[] input = new byte[ID_LENGTH / 8];
|
||||||
|
in.readFully(input);
|
||||||
|
this.keyBytes = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String hexRepresentation()
|
||||||
|
{
|
||||||
|
/* Returns the hex format of this NodeId */
|
||||||
|
return DatatypeConverter.printHexBinary(keyBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return this.hexRepresentation();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.node;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Comparator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Comparator to compare 2 keys to a given key
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @since 20140322
|
||||||
|
*/
|
||||||
|
public class KeyComparator implements Comparator<Node>
|
||||||
|
{
|
||||||
|
|
||||||
|
private final BigInteger key;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param key The NodeId relative to which the distance should be measured.
|
||||||
|
*/
|
||||||
|
public KeyComparator(KademliaId key)
|
||||||
|
{
|
||||||
|
this.key = key.getInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare two objects which must both be of type <code>Node</code>
|
||||||
|
* and determine which is closest to the identifier specified in the
|
||||||
|
* constructor.
|
||||||
|
*
|
||||||
|
* @param n1 Node 1 to compare distance from the key
|
||||||
|
* @param n2 Node 2 to compare distance from the key
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int compare(Node n1, Node n2)
|
||||||
|
{
|
||||||
|
BigInteger b1 = n1.getNodeId().getInt();
|
||||||
|
BigInteger b2 = n2.getNodeId().getInt();
|
||||||
|
|
||||||
|
b1 = b1.xor(key);
|
||||||
|
b2 = b2.xor(key);
|
||||||
|
|
||||||
|
return b1.abs().compareTo(b2.abs());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,134 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.node;
|
||||||
|
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.message.Streamable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Node in the Kademlia network - Contains basic node network information.
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @since 20140202
|
||||||
|
* @version 0.1
|
||||||
|
*/
|
||||||
|
public class Node implements Streamable, Serializable
|
||||||
|
{
|
||||||
|
|
||||||
|
private KademliaId nodeId;
|
||||||
|
private InetAddress inetAddress;
|
||||||
|
private int port;
|
||||||
|
private final String strRep;
|
||||||
|
|
||||||
|
public Node(KademliaId nid, InetAddress ip, int port)
|
||||||
|
{
|
||||||
|
this.nodeId = nid;
|
||||||
|
this.inetAddress = ip;
|
||||||
|
this.port = port;
|
||||||
|
this.strRep = this.nodeId.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the Node's data from a DataInput stream
|
||||||
|
*
|
||||||
|
* @param in
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public Node(DataInputStream in) throws IOException
|
||||||
|
{
|
||||||
|
this.fromStream(in);
|
||||||
|
this.strRep = this.nodeId.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the InetAddress of this node
|
||||||
|
*
|
||||||
|
* @param addr The new InetAddress of this node
|
||||||
|
*/
|
||||||
|
public void setInetAddress(InetAddress addr)
|
||||||
|
{
|
||||||
|
this.inetAddress = addr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The NodeId object of this node
|
||||||
|
*/
|
||||||
|
public KademliaId getNodeId()
|
||||||
|
{
|
||||||
|
return this.nodeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a SocketAddress for this node
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public InetSocketAddress getSocketAddress()
|
||||||
|
{
|
||||||
|
return new InetSocketAddress(this.inetAddress, this.port);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void toStream(DataOutputStream out) throws IOException
|
||||||
|
{
|
||||||
|
/* Add the NodeId to the stream */
|
||||||
|
this.nodeId.toStream(out);
|
||||||
|
|
||||||
|
/* Add the Node's IP address to the stream */
|
||||||
|
byte[] a = inetAddress.getAddress();
|
||||||
|
if (a.length != 4)
|
||||||
|
{
|
||||||
|
throw new RuntimeException("Expected InetAddress of 4 bytes, got " + a.length);
|
||||||
|
}
|
||||||
|
out.write(a);
|
||||||
|
|
||||||
|
/* Add the port to the stream */
|
||||||
|
out.writeInt(port);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void fromStream(DataInputStream in) throws IOException
|
||||||
|
{
|
||||||
|
/* Load the NodeId */
|
||||||
|
this.nodeId = new KademliaId(in);
|
||||||
|
|
||||||
|
/* Load the IP Address */
|
||||||
|
byte[] ip = new byte[4];
|
||||||
|
in.readFully(ip);
|
||||||
|
this.inetAddress = InetAddress.getByAddress(ip);
|
||||||
|
|
||||||
|
/* Read in the port */
|
||||||
|
this.port = in.readInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o)
|
||||||
|
{
|
||||||
|
if (o instanceof Node)
|
||||||
|
{
|
||||||
|
Node n = (Node) o;
|
||||||
|
if (n == this)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return this.getNodeId().equals(n.getNodeId());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode()
|
||||||
|
{
|
||||||
|
return this.getNodeId().hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return this.getNodeId().toString();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.operation;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KadConfiguration;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KadServer;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KademliaNode;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.KademliaId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* At each time interval t, nodes need to refresh their K-Buckets
|
||||||
|
* This operation takes care of refreshing this node's K-Buckets
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @created 20140224
|
||||||
|
*/
|
||||||
|
public class BucketRefreshOperation implements Operation
|
||||||
|
{
|
||||||
|
|
||||||
|
private final KadServer server;
|
||||||
|
private final KademliaNode localNode;
|
||||||
|
private final KadConfiguration config;
|
||||||
|
|
||||||
|
public BucketRefreshOperation(KadServer server, KademliaNode localNode, KadConfiguration config)
|
||||||
|
{
|
||||||
|
this.server = server;
|
||||||
|
this.localNode = localNode;
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Each bucket need to be refreshed at every time interval t.
|
||||||
|
* Find an identifier in each bucket's range, use it to look for nodes closest to this identifier
|
||||||
|
* allowing the bucket to be refreshed.
|
||||||
|
*
|
||||||
|
* Then Do a NodeLookupOperation for each of the generated NodeIds,
|
||||||
|
* This will find the K-Closest nodes to that ID, and update the necessary K-Bucket
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public synchronized void execute() throws IOException
|
||||||
|
{
|
||||||
|
for (int i = 1; i < KademliaId.ID_LENGTH; i++)
|
||||||
|
{
|
||||||
|
/* Construct a NodeId that is i bits away from the current node Id */
|
||||||
|
final KademliaId current = this.localNode.getNode().getNodeId().generateNodeIdByDistance(i);
|
||||||
|
|
||||||
|
/* Run the Node Lookup Operation, each in a different thread to speed up things */
|
||||||
|
new Thread()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void run()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
new NodeLookupOperation(server, localNode, current, BucketRefreshOperation.this.config).execute();
|
||||||
|
}
|
||||||
|
catch (IOException e)
|
||||||
|
{
|
||||||
|
//System.err.println("Bucket Refresh Operation Failed. Msg: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,140 @@
|
|||||||
|
/**
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @created 20140218
|
||||||
|
* @desc Operation that handles connecting to an existing Kademlia network using a bootstrap node
|
||||||
|
*/
|
||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.operation;
|
||||||
|
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.message.Receiver;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KadConfiguration;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KadServer;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KademliaNode;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.exceptions.RoutingException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.message.AcknowledgeMessage;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.message.ConnectMessage;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.message.Message;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.Node;
|
||||||
|
|
||||||
|
public class ConnectOperation implements Operation, Receiver
|
||||||
|
{
|
||||||
|
|
||||||
|
public static final int MAX_CONNECT_ATTEMPTS = 5; // Try 5 times to connect to a node
|
||||||
|
|
||||||
|
private final KadServer server;
|
||||||
|
private final KademliaNode localNode;
|
||||||
|
private final Node bootstrapNode;
|
||||||
|
private final KadConfiguration config;
|
||||||
|
|
||||||
|
private boolean error;
|
||||||
|
private int attempts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param server The message server used to send/receive messages
|
||||||
|
* @param local The local node
|
||||||
|
* @param bootstrap Node to use to bootstrap the local node onto the network
|
||||||
|
* @param config
|
||||||
|
*/
|
||||||
|
public ConnectOperation(KadServer server, KademliaNode local, Node bootstrap, KadConfiguration config)
|
||||||
|
{
|
||||||
|
this.server = server;
|
||||||
|
this.localNode = local;
|
||||||
|
this.bootstrapNode = bootstrap;
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void execute() throws IOException
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
/* Contact the bootstrap node */
|
||||||
|
this.error = true;
|
||||||
|
this.attempts = 0;
|
||||||
|
Message m = new ConnectMessage(this.localNode.getNode());
|
||||||
|
|
||||||
|
/* Send a connect message to the bootstrap node */
|
||||||
|
server.sendMessage(this.bootstrapNode, m, this);
|
||||||
|
|
||||||
|
/* If we haven't finished as yet, wait for a maximum of config.operationTimeout() time */
|
||||||
|
int totalTimeWaited = 0;
|
||||||
|
int timeInterval = 50; // We re-check every 300 milliseconds
|
||||||
|
while (totalTimeWaited < this.config.operationTimeout())
|
||||||
|
{
|
||||||
|
if (error)
|
||||||
|
{
|
||||||
|
wait(timeInterval);
|
||||||
|
totalTimeWaited += timeInterval;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (error)
|
||||||
|
{
|
||||||
|
/* If we still haven't received any responses by then, do a routing timeout */
|
||||||
|
throw new RoutingException("ConnectOperation: Bootstrap node did not respond: " + bootstrapNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Perform lookup for our own ID to get nodes close to us */
|
||||||
|
Operation lookup = new NodeLookupOperation(this.server, this.localNode, this.localNode.getNode().getNodeId(), this.config);
|
||||||
|
lookup.execute();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh buckets to get a good routing table
|
||||||
|
* After the above lookup operation, K nodes will be in our routing table,
|
||||||
|
* Now we try to populate all of our buckets.
|
||||||
|
*/
|
||||||
|
new BucketRefreshOperation(this.server, this.localNode, this.config).execute();
|
||||||
|
}
|
||||||
|
catch (InterruptedException e)
|
||||||
|
{
|
||||||
|
System.err.println("Connect operation was interrupted. ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receives an AcknowledgeMessage from the bootstrap node.
|
||||||
|
*
|
||||||
|
* @param comm
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public synchronized void receive(Message incoming, int comm)
|
||||||
|
{
|
||||||
|
/* The incoming message will be an acknowledgement message */
|
||||||
|
AcknowledgeMessage msg = (AcknowledgeMessage) incoming;
|
||||||
|
|
||||||
|
/* The bootstrap node has responded, insert it into our space */
|
||||||
|
this.localNode.getRoutingTable().insert(this.bootstrapNode);
|
||||||
|
|
||||||
|
/* We got a response, so the error is false */
|
||||||
|
error = false;
|
||||||
|
|
||||||
|
/* Wake up any waiting thread */
|
||||||
|
notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resends a ConnectMessage to the boot strap node a maximum of MAX_ATTEMPTS
|
||||||
|
* times.
|
||||||
|
*
|
||||||
|
* @param comm
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public synchronized void timeout(int comm) throws IOException
|
||||||
|
{
|
||||||
|
if (++this.attempts < MAX_CONNECT_ATTEMPTS)
|
||||||
|
{
|
||||||
|
this.server.sendMessage(this.bootstrapNode, new ConnectMessage(this.localNode.getNode()), this);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* We just exit, so notify all other threads that are possibly waiting */
|
||||||
|
notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,342 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.operation;
|
||||||
|
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.message.Receiver;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.JKademliaNode;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.dht.GetParameter;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KadConfiguration;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KadServer;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.dht.JKademliaStorageEntry;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.exceptions.ContentNotFoundException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.exceptions.RoutingException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.exceptions.UnknownMessageException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.message.ContentLookupMessage;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.message.ContentMessage;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.message.Message;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.message.NodeReplyMessage;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.KeyComparator;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.Node;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.util.RouteLengthChecker;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks up a specified identifier and returns the value associated with it
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @since 20140226
|
||||||
|
*/
|
||||||
|
public class ContentLookupOperation implements Operation, Receiver
|
||||||
|
{
|
||||||
|
|
||||||
|
/* Constants */
|
||||||
|
private static final Byte UNASKED = (byte) 0x00;
|
||||||
|
private static final Byte AWAITING = (byte) 0x01;
|
||||||
|
private static final Byte ASKED = (byte) 0x02;
|
||||||
|
private static final Byte FAILED = (byte) 0x03;
|
||||||
|
|
||||||
|
private final KadServer server;
|
||||||
|
private final JKademliaNode localNode;
|
||||||
|
private JKademliaStorageEntry contentFound = null;
|
||||||
|
private final KadConfiguration config;
|
||||||
|
|
||||||
|
private final ContentLookupMessage lookupMessage;
|
||||||
|
|
||||||
|
private boolean isContentFound;
|
||||||
|
private final SortedMap<Node, Byte> nodes;
|
||||||
|
|
||||||
|
/* Tracks messages in transit and awaiting reply */
|
||||||
|
private final Map<Integer, Node> messagesTransiting;
|
||||||
|
|
||||||
|
/* Used to sort nodes */
|
||||||
|
private final Comparator comparator;
|
||||||
|
|
||||||
|
/* Statistical information */
|
||||||
|
private final RouteLengthChecker routeLengthChecker;
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
messagesTransiting = new HashMap<>();
|
||||||
|
isContentFound = false;
|
||||||
|
routeLengthChecker = new RouteLengthChecker();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param server
|
||||||
|
* @param localNode
|
||||||
|
* @param params The parameters to search for the content which we need to find
|
||||||
|
* @param config
|
||||||
|
*/
|
||||||
|
public ContentLookupOperation(KadServer server, JKademliaNode localNode, GetParameter params, KadConfiguration config)
|
||||||
|
{
|
||||||
|
/* Construct our lookup message */
|
||||||
|
this.lookupMessage = new ContentLookupMessage(localNode.getNode(), params);
|
||||||
|
|
||||||
|
this.server = server;
|
||||||
|
this.localNode = localNode;
|
||||||
|
this.config = config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We initialize a TreeMap to store nodes.
|
||||||
|
* This map will be sorted by which nodes are closest to the lookupId
|
||||||
|
*/
|
||||||
|
this.comparator = new KeyComparator(params.getKey());
|
||||||
|
this.nodes = new TreeMap(this.comparator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws IOException
|
||||||
|
* @throws RoutingException
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public synchronized void execute() throws IOException, RoutingException
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
/* Set the local node as already asked */
|
||||||
|
nodes.put(this.localNode.getNode(), ASKED);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We add all nodes here instead of the K-Closest because there may be the case that the K-Closest are offline
|
||||||
|
* - The operation takes care of looking at the K-Closest.
|
||||||
|
*/
|
||||||
|
List<Node> allNodes = this.localNode.getRoutingTable().getAllNodes();
|
||||||
|
this.addNodes(allNodes);
|
||||||
|
|
||||||
|
/* Also add the initial set of nodes to the routeLengthChecker */
|
||||||
|
this.routeLengthChecker.addInitialNodes(allNodes);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If we haven't found the requested amount of content as yet,
|
||||||
|
* keey trying until config.operationTimeout() time has expired
|
||||||
|
*/
|
||||||
|
int totalTimeWaited = 0;
|
||||||
|
int timeInterval = 10; // We re-check every n milliseconds
|
||||||
|
while (totalTimeWaited < this.config.operationTimeout())
|
||||||
|
{
|
||||||
|
if (!this.askNodesorFinish() && !isContentFound)
|
||||||
|
{
|
||||||
|
wait(timeInterval);
|
||||||
|
totalTimeWaited += timeInterval;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (InterruptedException e)
|
||||||
|
{
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add nodes from this list to the set of nodes to lookup
|
||||||
|
*
|
||||||
|
* @param list The list from which to add nodes
|
||||||
|
*/
|
||||||
|
public void addNodes(List<Node> list)
|
||||||
|
{
|
||||||
|
for (Node o : list)
|
||||||
|
{
|
||||||
|
/* If this node is not in the list, add the node */
|
||||||
|
if (!nodes.containsKey(o))
|
||||||
|
{
|
||||||
|
nodes.put(o, UNASKED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asks some of the K closest nodes seen but not yet queried.
|
||||||
|
* Assures that no more than DefaultConfiguration.CONCURRENCY messages are in transit at a time
|
||||||
|
*
|
||||||
|
* This method should be called every time a reply is received or a timeout occurs.
|
||||||
|
*
|
||||||
|
* If all K closest nodes have been asked and there are no messages in transit,
|
||||||
|
* the algorithm is finished.
|
||||||
|
*
|
||||||
|
* @return <code>true</code> if finished OR <code>false</code> otherwise
|
||||||
|
*/
|
||||||
|
private boolean askNodesorFinish() throws IOException
|
||||||
|
{
|
||||||
|
/* If >= CONCURRENCY nodes are in transit, don't do anything */
|
||||||
|
if (this.config.maxConcurrentMessagesTransiting() <= this.messagesTransiting.size())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Get unqueried nodes among the K closest seen that have not FAILED */
|
||||||
|
List<Node> unasked = this.closestNodesNotFailed(UNASKED);
|
||||||
|
|
||||||
|
if (unasked.isEmpty() && this.messagesTransiting.isEmpty())
|
||||||
|
{
|
||||||
|
/* We have no unasked nodes nor any messages in transit, we're finished! */
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sort nodes according to criteria */
|
||||||
|
Collections.sort(unasked, this.comparator);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send messages to nodes in the list;
|
||||||
|
* making sure than no more than CONCURRENCY messsages are in transit
|
||||||
|
*/
|
||||||
|
for (int i = 0; (this.messagesTransiting.size() < this.config.maxConcurrentMessagesTransiting()) && (i < unasked.size()); i++)
|
||||||
|
{
|
||||||
|
Node n = (Node) unasked.get(i);
|
||||||
|
|
||||||
|
int comm = server.sendMessage(n, lookupMessage, this);
|
||||||
|
|
||||||
|
this.nodes.put(n, AWAITING);
|
||||||
|
this.messagesTransiting.put(comm, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* We're not finished as yet, return false */
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find The K closest nodes to the target lookupId given that have not FAILED.
|
||||||
|
* From those K, get those that have the specified status
|
||||||
|
*
|
||||||
|
* @param status The status of the nodes to return
|
||||||
|
*
|
||||||
|
* @return A List of the closest nodes
|
||||||
|
*/
|
||||||
|
private List<Node> closestNodesNotFailed(Byte status)
|
||||||
|
{
|
||||||
|
List<Node> closestNodes = new ArrayList<>(this.config.k());
|
||||||
|
int remainingSpaces = this.config.k();
|
||||||
|
|
||||||
|
for (Map.Entry e : this.nodes.entrySet())
|
||||||
|
{
|
||||||
|
if (!FAILED.equals(e.getValue()))
|
||||||
|
{
|
||||||
|
if (status.equals(e.getValue()))
|
||||||
|
{
|
||||||
|
/* We got one with the required status, now add it */
|
||||||
|
closestNodes.add((Node) e.getKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (--remainingSpaces == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return closestNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void receive(Message incoming, int comm) throws IOException, RoutingException
|
||||||
|
{
|
||||||
|
if (this.isContentFound)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (incoming instanceof ContentMessage)
|
||||||
|
{
|
||||||
|
/* The reply received is a content message with the required content, take it in */
|
||||||
|
ContentMessage msg = (ContentMessage) incoming;
|
||||||
|
|
||||||
|
/* Add the origin node to our routing table */
|
||||||
|
this.localNode.getRoutingTable().insert(msg.getOrigin());
|
||||||
|
|
||||||
|
/* Get the Content and check if it satisfies the required parameters */
|
||||||
|
JKademliaStorageEntry content = msg.getContent();
|
||||||
|
this.contentFound = content;
|
||||||
|
this.isContentFound = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* The reply received is a NodeReplyMessage with nodes closest to the content needed */
|
||||||
|
NodeReplyMessage msg = (NodeReplyMessage) incoming;
|
||||||
|
|
||||||
|
/* Add the origin node to our routing table */
|
||||||
|
Node origin = msg.getOrigin();
|
||||||
|
this.localNode.getRoutingTable().insert(origin);
|
||||||
|
|
||||||
|
/* Set that we've completed ASKing the origin node */
|
||||||
|
this.nodes.put(origin, ASKED);
|
||||||
|
|
||||||
|
/* Remove this msg from messagesTransiting since it's completed now */
|
||||||
|
this.messagesTransiting.remove(comm);
|
||||||
|
|
||||||
|
/* Add the received nodes to the routeLengthChecker */
|
||||||
|
this.routeLengthChecker.addNodes(msg.getNodes(), origin);
|
||||||
|
|
||||||
|
/* Add the received nodes to our nodes list to query */
|
||||||
|
this.addNodes(msg.getNodes());
|
||||||
|
this.askNodesorFinish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A node does not respond or a packet was lost, we set this node as failed
|
||||||
|
*
|
||||||
|
* @param comm
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public synchronized void timeout(int comm) throws IOException
|
||||||
|
{
|
||||||
|
/* Get the node associated with this communication */
|
||||||
|
Node n = this.messagesTransiting.get(new Integer(comm));
|
||||||
|
|
||||||
|
if (n == null)
|
||||||
|
{
|
||||||
|
throw new UnknownMessageException("Unknown comm: " + comm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mark this node as failed and inform the routing table that it's unresponsive */
|
||||||
|
this.nodes.put(n, FAILED);
|
||||||
|
this.localNode.getRoutingTable().setUnresponsiveContact(n);
|
||||||
|
this.messagesTransiting.remove(comm);
|
||||||
|
|
||||||
|
this.askNodesorFinish();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Whether the content was found or not.
|
||||||
|
*/
|
||||||
|
public boolean isContentFound()
|
||||||
|
{
|
||||||
|
return this.isContentFound;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The list of all content found during the lookup operation
|
||||||
|
*
|
||||||
|
* @throws ContentNotFoundException
|
||||||
|
*/
|
||||||
|
public JKademliaStorageEntry getContentFound() throws ContentNotFoundException
|
||||||
|
{
|
||||||
|
if (this.isContentFound)
|
||||||
|
{
|
||||||
|
return this.contentFound;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new ContentNotFoundException("No Value was found for the given key.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return How many hops it took in order to get to the content.
|
||||||
|
*/
|
||||||
|
public int routeLength()
|
||||||
|
{
|
||||||
|
return this.routeLengthChecker.getRouteLength();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,99 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.operation;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KadConfiguration;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KadServer;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KademliaNode;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.dht.KademliaDHT;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.dht.KademliaStorageEntryMetadata;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.exceptions.ContentNotFoundException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.message.Message;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.message.StoreContentMessage;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.Node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh/Restore the data on this node by sending the data to the K-Closest nodes to the data
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @since 20140306
|
||||||
|
*/
|
||||||
|
public class ContentRefreshOperation implements Operation
|
||||||
|
{
|
||||||
|
|
||||||
|
private final KadServer server;
|
||||||
|
private final KademliaNode localNode;
|
||||||
|
private final KademliaDHT dht;
|
||||||
|
private final KadConfiguration config;
|
||||||
|
|
||||||
|
public ContentRefreshOperation(KadServer server, KademliaNode localNode, KademliaDHT dht, KadConfiguration config)
|
||||||
|
{
|
||||||
|
this.server = server;
|
||||||
|
this.localNode = localNode;
|
||||||
|
this.dht = dht;
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For each content stored on this DHT, distribute it to the K closest nodes
|
||||||
|
Also delete the content if this node is no longer one of the K closest nodes
|
||||||
|
|
||||||
|
We assume that our JKademliaRoutingTable is updated, and we can get the K closest nodes from that table
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void execute() throws IOException
|
||||||
|
{
|
||||||
|
/* Get a list of all storage entries for content */
|
||||||
|
List<KademliaStorageEntryMetadata> entries = this.dht.getStorageEntries();
|
||||||
|
|
||||||
|
/* If a content was last republished before this time, then we need to republish it */
|
||||||
|
final long minRepublishTime = (System.currentTimeMillis() / 1000L) - this.config.restoreInterval();
|
||||||
|
|
||||||
|
/* For each storage entry, distribute it */
|
||||||
|
for (KademliaStorageEntryMetadata e : entries)
|
||||||
|
{
|
||||||
|
/* Check last update time of this entry and only distribute it if it has been last updated > 1 hour ago */
|
||||||
|
if (e.lastRepublished() > minRepublishTime)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Set that this content is now republished */
|
||||||
|
e.updateLastRepublished();
|
||||||
|
|
||||||
|
/* Get the K closest nodes to this entries */
|
||||||
|
List<Node> closestNodes = this.localNode.getRoutingTable().findClosest(e.getKey(), this.config.k());
|
||||||
|
|
||||||
|
/* Create the message */
|
||||||
|
Message msg = new StoreContentMessage(this.localNode.getNode(), dht.get(e));
|
||||||
|
|
||||||
|
/*Store the message on all of the K-Nodes*/
|
||||||
|
for (Node n : closestNodes)
|
||||||
|
{
|
||||||
|
/*We don't need to again store the content locally, it's already here*/
|
||||||
|
if (!n.equals(this.localNode.getNode()))
|
||||||
|
{
|
||||||
|
/* Send a contentstore operation to the K-Closest nodes */
|
||||||
|
this.server.sendMessage(n, msg, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Delete any content on this node that this node is not one of the K-Closest nodes to */
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!closestNodes.contains(this.localNode.getNode()))
|
||||||
|
{
|
||||||
|
this.dht.remove(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (ContentNotFoundException cnfe)
|
||||||
|
{
|
||||||
|
/* It would be weird if the content is not found here */
|
||||||
|
System.err.println("ContentRefreshOperation: Removing content from local node, content not found... Message: " + cnfe.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.operation;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KadConfiguration;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KadServer;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KademliaNode;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.dht.KademliaDHT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An operation that handles refreshing the entire Kademlia Systems including buckets and content
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @since 20140306
|
||||||
|
*/
|
||||||
|
public class KadRefreshOperation implements Operation
|
||||||
|
{
|
||||||
|
|
||||||
|
private final KadServer server;
|
||||||
|
private final KademliaNode localNode;
|
||||||
|
private final KademliaDHT dht;
|
||||||
|
private final KadConfiguration config;
|
||||||
|
|
||||||
|
public KadRefreshOperation(KadServer server, KademliaNode localNode, KademliaDHT dht, KadConfiguration config)
|
||||||
|
{
|
||||||
|
this.server = server;
|
||||||
|
this.localNode = localNode;
|
||||||
|
this.dht = dht;
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute() throws IOException
|
||||||
|
{
|
||||||
|
/* Run our BucketRefreshOperation to refresh buckets */
|
||||||
|
new BucketRefreshOperation(this.server, this.localNode, this.config).execute();
|
||||||
|
|
||||||
|
/* After buckets have been refreshed, we refresh content */
|
||||||
|
new ContentRefreshOperation(this.server, this.localNode, this.dht, this.config).execute();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,323 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.operation;
|
||||||
|
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.message.Receiver;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KadConfiguration;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KadServer;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KademliaNode;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.exceptions.RoutingException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.message.Message;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.message.NodeLookupMessage;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.message.NodeReplyMessage;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.KeyComparator;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.Node;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.KademliaId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the K closest nodes to a specified identifier
|
||||||
|
* The algorithm terminates when it has gotten responses from the K closest nodes it has seen.
|
||||||
|
* Nodes that fail to respond are removed from consideration
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @created 20140219
|
||||||
|
*/
|
||||||
|
public class NodeLookupOperation implements Operation, Receiver
|
||||||
|
{
|
||||||
|
|
||||||
|
/* Constants */
|
||||||
|
private static final String UNASKED = "UnAsked";
|
||||||
|
private static final String AWAITING = "Awaiting";
|
||||||
|
private static final String ASKED = "Asked";
|
||||||
|
private static final String FAILED = "Failed";
|
||||||
|
|
||||||
|
private final KadServer server;
|
||||||
|
private final KademliaNode localNode;
|
||||||
|
private final KadConfiguration config;
|
||||||
|
|
||||||
|
private final Message lookupMessage; // Message sent to each peer
|
||||||
|
private final Map<Node, String> nodes;
|
||||||
|
|
||||||
|
/* Tracks messages in transit and awaiting reply */
|
||||||
|
private final Map<Integer, Node> messagesTransiting;
|
||||||
|
|
||||||
|
/* Used to sort nodes */
|
||||||
|
private final Comparator comparator;
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
messagesTransiting = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param server KadServer used for communication
|
||||||
|
* @param localNode The local node making the communication
|
||||||
|
* @param lookupId The ID for which to find nodes close to
|
||||||
|
* @param config
|
||||||
|
*/
|
||||||
|
public NodeLookupOperation(KadServer server, KademliaNode localNode, KademliaId lookupId, KadConfiguration config)
|
||||||
|
{
|
||||||
|
this.server = server;
|
||||||
|
this.localNode = localNode;
|
||||||
|
this.config = config;
|
||||||
|
|
||||||
|
this.lookupMessage = new NodeLookupMessage(localNode.getNode(), lookupId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We initialize a TreeMap to store nodes.
|
||||||
|
* This map will be sorted by which nodes are closest to the lookupId
|
||||||
|
*/
|
||||||
|
this.comparator = new KeyComparator(lookupId);
|
||||||
|
this.nodes = new TreeMap(this.comparator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws IOException
|
||||||
|
* @throws RoutingException
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public synchronized void execute() throws IOException, RoutingException
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
/* Set the local node as already asked */
|
||||||
|
nodes.put(this.localNode.getNode(), ASKED);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We add all nodes here instead of the K-Closest because there may be the case that the K-Closest are offline
|
||||||
|
* - The operation takes care of looking at the K-Closest.
|
||||||
|
*/
|
||||||
|
this.addNodes(this.localNode.getRoutingTable().getAllNodes());
|
||||||
|
|
||||||
|
/* If we haven't finished as yet, wait for a maximum of config.operationTimeout() time */
|
||||||
|
int totalTimeWaited = 0;
|
||||||
|
int timeInterval = 10; // We re-check every n milliseconds
|
||||||
|
while (totalTimeWaited < this.config.operationTimeout())
|
||||||
|
{
|
||||||
|
if (!this.askNodesorFinish())
|
||||||
|
{
|
||||||
|
wait(timeInterval);
|
||||||
|
totalTimeWaited += timeInterval;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Now after we've finished, we would have an idea of offline nodes, lets update our routing table */
|
||||||
|
this.localNode.getRoutingTable().setUnresponsiveContacts(this.getFailedNodes());
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (InterruptedException e)
|
||||||
|
{
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Node> getClosestNodes()
|
||||||
|
{
|
||||||
|
return this.closestNodes(ASKED);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add nodes from this list to the set of nodes to lookup
|
||||||
|
*
|
||||||
|
* @param list The list from which to add nodes
|
||||||
|
*/
|
||||||
|
public void addNodes(List<Node> list)
|
||||||
|
{
|
||||||
|
for (Node o : list)
|
||||||
|
{
|
||||||
|
/* If this node is not in the list, add the node */
|
||||||
|
if (!nodes.containsKey(o))
|
||||||
|
{
|
||||||
|
nodes.put(o, UNASKED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asks some of the K closest nodes seen but not yet queried.
|
||||||
|
* Assures that no more than DefaultConfiguration.CONCURRENCY messages are in transit at a time
|
||||||
|
*
|
||||||
|
* This method should be called every time a reply is received or a timeout occurs.
|
||||||
|
*
|
||||||
|
* If all K closest nodes have been asked and there are no messages in transit,
|
||||||
|
* the algorithm is finished.
|
||||||
|
*
|
||||||
|
* @return <code>true</code> if finished OR <code>false</code> otherwise
|
||||||
|
*/
|
||||||
|
private boolean askNodesorFinish() throws IOException
|
||||||
|
{
|
||||||
|
/* If >= CONCURRENCY nodes are in transit, don't do anything */
|
||||||
|
if (this.config.maxConcurrentMessagesTransiting() <= this.messagesTransiting.size())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Get unqueried nodes among the K closest seen that have not FAILED */
|
||||||
|
List<Node> unasked = this.closestNodesNotFailed(UNASKED);
|
||||||
|
|
||||||
|
if (unasked.isEmpty() && this.messagesTransiting.isEmpty())
|
||||||
|
{
|
||||||
|
/* We have no unasked nodes nor any messages in transit, we're finished! */
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send messages to nodes in the list;
|
||||||
|
* making sure than no more than CONCURRENCY messsages are in transit
|
||||||
|
*/
|
||||||
|
for (int i = 0; (this.messagesTransiting.size() < this.config.maxConcurrentMessagesTransiting()) && (i < unasked.size()); i++)
|
||||||
|
{
|
||||||
|
Node n = (Node) unasked.get(i);
|
||||||
|
|
||||||
|
int comm = server.sendMessage(n, lookupMessage, this);
|
||||||
|
|
||||||
|
this.nodes.put(n, AWAITING);
|
||||||
|
this.messagesTransiting.put(comm, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* We're not finished as yet, return false */
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param status The status of the nodes to return
|
||||||
|
*
|
||||||
|
* @return The K closest nodes to the target lookupId given that have the specified status
|
||||||
|
*/
|
||||||
|
private List<Node> closestNodes(String status)
|
||||||
|
{
|
||||||
|
List<Node> closestNodes = new ArrayList<>(this.config.k());
|
||||||
|
int remainingSpaces = this.config.k();
|
||||||
|
|
||||||
|
for (Map.Entry e : this.nodes.entrySet())
|
||||||
|
{
|
||||||
|
if (status.equals(e.getValue()))
|
||||||
|
{
|
||||||
|
/* We got one with the required status, now add it */
|
||||||
|
closestNodes.add((Node) e.getKey());
|
||||||
|
if (--remainingSpaces == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return closestNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find The K closest nodes to the target lookupId given that have not FAILED.
|
||||||
|
* From those K, get those that have the specified status
|
||||||
|
*
|
||||||
|
* @param status The status of the nodes to return
|
||||||
|
*
|
||||||
|
* @return A List of the closest nodes
|
||||||
|
*/
|
||||||
|
private List<Node> closestNodesNotFailed(String status)
|
||||||
|
{
|
||||||
|
List<Node> closestNodes = new ArrayList<>(this.config.k());
|
||||||
|
int remainingSpaces = this.config.k();
|
||||||
|
|
||||||
|
for (Map.Entry<Node, String> e : this.nodes.entrySet())
|
||||||
|
{
|
||||||
|
if (!FAILED.equals(e.getValue()))
|
||||||
|
{
|
||||||
|
if (status.equals(e.getValue()))
|
||||||
|
{
|
||||||
|
/* We got one with the required status, now add it */
|
||||||
|
closestNodes.add(e.getKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (--remainingSpaces == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return closestNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receive and handle the incoming NodeReplyMessage
|
||||||
|
*
|
||||||
|
* @param comm
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public synchronized void receive(Message incoming, int comm) throws IOException
|
||||||
|
{
|
||||||
|
if (!(incoming instanceof NodeReplyMessage))
|
||||||
|
{
|
||||||
|
/* Not sure why we get a message of a different type here... @todo Figure it out. */
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/* We receive a NodeReplyMessage with a set of nodes, read this message */
|
||||||
|
NodeReplyMessage msg = (NodeReplyMessage) incoming;
|
||||||
|
|
||||||
|
/* Add the origin node to our routing table */
|
||||||
|
Node origin = msg.getOrigin();
|
||||||
|
this.localNode.getRoutingTable().insert(origin);
|
||||||
|
|
||||||
|
/* Set that we've completed ASKing the origin node */
|
||||||
|
this.nodes.put(origin, ASKED);
|
||||||
|
|
||||||
|
/* Remove this msg from messagesTransiting since it's completed now */
|
||||||
|
this.messagesTransiting.remove(comm);
|
||||||
|
|
||||||
|
/* Add the received nodes to our nodes list to query */
|
||||||
|
this.addNodes(msg.getNodes());
|
||||||
|
this.askNodesorFinish();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A node does not respond or a packet was lost, we set this node as failed
|
||||||
|
*
|
||||||
|
* @param comm
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public synchronized void timeout(int comm) throws IOException
|
||||||
|
{
|
||||||
|
/* Get the node associated with this communication */
|
||||||
|
Node n = this.messagesTransiting.get(comm);
|
||||||
|
|
||||||
|
if (n == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mark this node as failed and inform the routing table that it is unresponsive */
|
||||||
|
this.nodes.put(n, FAILED);
|
||||||
|
this.localNode.getRoutingTable().setUnresponsiveContact(n);
|
||||||
|
this.messagesTransiting.remove(comm);
|
||||||
|
|
||||||
|
this.askNodesorFinish();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Node> getFailedNodes()
|
||||||
|
{
|
||||||
|
List<Node> failedNodes = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Map.Entry<Node, String> e : this.nodes.entrySet())
|
||||||
|
{
|
||||||
|
if (e.getValue().equals(FAILED))
|
||||||
|
{
|
||||||
|
failedNodes.add(e.getKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return failedNodes;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.operation;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.exceptions.RoutingException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An operation in the Kademlia routing protocol
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @created 20140218
|
||||||
|
*/
|
||||||
|
public interface Operation
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts an operation and returns when the operation is finished
|
||||||
|
*
|
||||||
|
* @throws RoutingException
|
||||||
|
*/
|
||||||
|
public void execute() throws IOException, RoutingException;
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Implementation of the Kademlia Ping operation,
|
||||||
|
* This is on hold at the moment since I'm not sure if we'll use ping given the improvements mentioned in the paper.
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @since 20140218
|
||||||
|
*/
|
||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.operation;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KadServer;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.exceptions.RoutingException;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.Node;
|
||||||
|
|
||||||
|
public class PingOperation implements Operation
|
||||||
|
{
|
||||||
|
|
||||||
|
private final KadServer server;
|
||||||
|
private final Node localNode;
|
||||||
|
private final Node toPing;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param server The Kademlia server used to send & receive messages
|
||||||
|
* @param local The local node
|
||||||
|
* @param toPing The node to send the ping message to
|
||||||
|
*/
|
||||||
|
public PingOperation(KadServer server, Node local, Node toPing)
|
||||||
|
{
|
||||||
|
this.server = server;
|
||||||
|
this.localNode = local;
|
||||||
|
this.toPing = toPing;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute() throws IOException, RoutingException
|
||||||
|
{
|
||||||
|
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.operation;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KadConfiguration;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KadServer;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KademliaNode;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.dht.JKademliaStorageEntry;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.dht.KademliaDHT;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.message.Message;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.message.StoreContentMessage;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.Node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operation that stores a DHT Content onto the K closest nodes to the content Key
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @since 20140224
|
||||||
|
*/
|
||||||
|
public class StoreOperation implements Operation
|
||||||
|
{
|
||||||
|
|
||||||
|
private final KadServer server;
|
||||||
|
private final KademliaNode localNode;
|
||||||
|
private final JKademliaStorageEntry storageEntry;
|
||||||
|
private final KademliaDHT localDht;
|
||||||
|
private final KadConfiguration config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param server
|
||||||
|
* @param localNode
|
||||||
|
* @param storageEntry The content to be stored on the DHT
|
||||||
|
* @param localDht The local DHT
|
||||||
|
* @param config
|
||||||
|
*/
|
||||||
|
public StoreOperation(KadServer server, KademliaNode localNode, JKademliaStorageEntry storageEntry, KademliaDHT localDht, KadConfiguration config)
|
||||||
|
{
|
||||||
|
this.server = server;
|
||||||
|
this.localNode = localNode;
|
||||||
|
this.storageEntry = storageEntry;
|
||||||
|
this.localDht = localDht;
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void execute() throws IOException
|
||||||
|
{
|
||||||
|
/* Get the nodes on which we need to store the content */
|
||||||
|
NodeLookupOperation ndlo = new NodeLookupOperation(this.server, this.localNode, this.storageEntry.getContentMetadata().getKey(), this.config);
|
||||||
|
ndlo.execute();
|
||||||
|
List<Node> nodes = ndlo.getClosestNodes();
|
||||||
|
|
||||||
|
/* Create the message */
|
||||||
|
Message msg = new StoreContentMessage(this.localNode.getNode(), this.storageEntry);
|
||||||
|
|
||||||
|
/*Store the message on all of the K-Nodes*/
|
||||||
|
for (Node n : nodes)
|
||||||
|
{
|
||||||
|
if (n.equals(this.localNode.getNode()))
|
||||||
|
{
|
||||||
|
/* Store the content locally */
|
||||||
|
this.localDht.store(this.storageEntry);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @todo Create a receiver that receives a store acknowledgement message to count how many nodes a content have been stored at
|
||||||
|
*/
|
||||||
|
this.server.sendMessage(n, msg, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The number of nodes that have stored this content
|
||||||
|
*
|
||||||
|
* @todo Implement this method
|
||||||
|
*/
|
||||||
|
public int numNodesStoredAt()
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,118 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.routing;
|
||||||
|
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.Node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps information about contacts of the Node; Contacts are stored in the Buckets in the Routing Table.
|
||||||
|
*
|
||||||
|
* Contacts are used instead of nodes because more information is needed than just the node information.
|
||||||
|
* - Information such as
|
||||||
|
* -- Last seen time
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @since 20140425
|
||||||
|
* @updated 20140426
|
||||||
|
*/
|
||||||
|
public class Contact implements Comparable<Contact>
|
||||||
|
{
|
||||||
|
|
||||||
|
private final Node n;
|
||||||
|
private long lastSeen;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stale as described by Kademlia paper page 64
|
||||||
|
* When a contact fails to respond, if the replacement cache is empty and there is no replacement for the contact,
|
||||||
|
* just mark it as stale.
|
||||||
|
*
|
||||||
|
* Now when a new contact is added, if the contact is stale, it is removed.
|
||||||
|
*/
|
||||||
|
private int staleCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a contact object
|
||||||
|
*
|
||||||
|
* @param n The node associated with this contact
|
||||||
|
*/
|
||||||
|
public Contact(Node n)
|
||||||
|
{
|
||||||
|
this.n = n;
|
||||||
|
this.lastSeen = System.currentTimeMillis() / 1000L;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Node getNode()
|
||||||
|
{
|
||||||
|
return this.n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a Node sees a contact a gain, the Node will want to update that it's seen recently,
|
||||||
|
* this method updates the last seen timestamp for this contact.
|
||||||
|
*/
|
||||||
|
public void setSeenNow()
|
||||||
|
{
|
||||||
|
this.lastSeen = System.currentTimeMillis() / 1000L;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When last was this contact seen?
|
||||||
|
*
|
||||||
|
* @return long The last time this contact was seen.
|
||||||
|
*/
|
||||||
|
public long lastSeen()
|
||||||
|
{
|
||||||
|
return this.lastSeen;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object c)
|
||||||
|
{
|
||||||
|
if (c instanceof Contact)
|
||||||
|
{
|
||||||
|
return ((Contact) c).getNode().equals(this.getNode());
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments the amount of times this count has failed to respond to a request.
|
||||||
|
*/
|
||||||
|
public void incrementStaleCount()
|
||||||
|
{
|
||||||
|
staleCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Integer Stale count
|
||||||
|
*/
|
||||||
|
public int staleCount()
|
||||||
|
{
|
||||||
|
return this.staleCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the stale count of the contact if it's recently seen
|
||||||
|
*/
|
||||||
|
public void resetStaleCount()
|
||||||
|
{
|
||||||
|
this.staleCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(Contact o)
|
||||||
|
{
|
||||||
|
if (this.getNode().equals(o.getNode()))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (this.lastSeen() > o.lastSeen()) ? 1 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode()
|
||||||
|
{
|
||||||
|
return this.getNode().hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.routing;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Comparator to compare 2 contacts by their last seen time
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @since 20140426
|
||||||
|
*/
|
||||||
|
public class ContactLastSeenComparator implements Comparator<Contact>
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare two contacts to determine their order in the Bucket,
|
||||||
|
* Contacts are ordered by their last seen timestamp.
|
||||||
|
*
|
||||||
|
* @param c1 Contact 1
|
||||||
|
* @param c2 Contact 2
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int compare(Contact c1, Contact c2)
|
||||||
|
{
|
||||||
|
if (c1.getNode().equals(c2.getNode()))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* We may have 2 different contacts with same last seen values so we can't return 0 here */
|
||||||
|
return c1.lastSeen() > c2.lastSeen() ? 1 : -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,275 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.routing;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KadConfiguration;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.Node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A bucket in the Kademlia routing table
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @created 20140215
|
||||||
|
*/
|
||||||
|
public class JKademliaBucket implements KademliaBucket
|
||||||
|
{
|
||||||
|
|
||||||
|
/* How deep is this bucket in the Routing Table */
|
||||||
|
private final int depth;
|
||||||
|
|
||||||
|
/* Contacts stored in this routing table */
|
||||||
|
private final TreeSet<Contact> contacts;
|
||||||
|
|
||||||
|
/* A set of last seen contacts that can replace any current contact that is unresponsive */
|
||||||
|
private final TreeSet<Contact> replacementCache;
|
||||||
|
|
||||||
|
private final KadConfiguration config;
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
contacts = new TreeSet<>();
|
||||||
|
replacementCache = new TreeSet<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param depth How deep in the routing tree is this bucket
|
||||||
|
* @param config
|
||||||
|
*/
|
||||||
|
public JKademliaBucket(int depth, KadConfiguration config)
|
||||||
|
{
|
||||||
|
this.depth = depth;
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void insert(Contact c)
|
||||||
|
{
|
||||||
|
if (this.contacts.contains(c))
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* If the contact is already in the bucket, lets update that we've seen it
|
||||||
|
* We need to remove and re-add the contact to get the Sorted Set to update sort order
|
||||||
|
*/
|
||||||
|
Contact tmp = this.removeFromContacts(c.getNode());
|
||||||
|
tmp.setSeenNow();
|
||||||
|
tmp.resetStaleCount();
|
||||||
|
this.contacts.add(tmp);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* If the bucket is filled, so put the contacts in the replacement cache */
|
||||||
|
if (contacts.size() >= this.config.k())
|
||||||
|
{
|
||||||
|
/* If the cache is empty, we check if any contacts are stale and replace the stalest one */
|
||||||
|
Contact stalest = null;
|
||||||
|
for (Contact tmp : this.contacts)
|
||||||
|
{
|
||||||
|
if (tmp.staleCount() >= this.config.stale())
|
||||||
|
{
|
||||||
|
/* Contact is stale */
|
||||||
|
if (stalest == null)
|
||||||
|
{
|
||||||
|
stalest = tmp;
|
||||||
|
}
|
||||||
|
else if (tmp.staleCount() > stalest.staleCount())
|
||||||
|
{
|
||||||
|
stalest = tmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If we have a stale contact, remove it and add the new contact to the bucket */
|
||||||
|
if (stalest != null)
|
||||||
|
{
|
||||||
|
this.contacts.remove(stalest);
|
||||||
|
this.contacts.add(c);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* No stale contact, lets insert this into replacement cache */
|
||||||
|
this.insertIntoReplacementCache(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.contacts.add(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void insert(Node n)
|
||||||
|
{
|
||||||
|
this.insert(new Contact(n));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized boolean containsContact(Contact c)
|
||||||
|
{
|
||||||
|
return this.contacts.contains(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized boolean containsNode(Node n)
|
||||||
|
{
|
||||||
|
return this.containsContact(new Contact(n));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized boolean removeContact(Contact c)
|
||||||
|
{
|
||||||
|
/* If the contact does not exist, then we failed to remove it */
|
||||||
|
if (!this.contacts.contains(c))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Contact exist, lets remove it only if our replacement cache has a replacement */
|
||||||
|
if (!this.replacementCache.isEmpty())
|
||||||
|
{
|
||||||
|
/* Replace the contact with one from the replacement cache */
|
||||||
|
this.contacts.remove(c);
|
||||||
|
Contact replacement = this.replacementCache.first();
|
||||||
|
this.contacts.add(replacement);
|
||||||
|
this.replacementCache.remove(replacement);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* There is no replacement, just increment the contact's stale count */
|
||||||
|
this.getFromContacts(c.getNode()).incrementStaleCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized Contact getFromContacts(Node n)
|
||||||
|
{
|
||||||
|
for (Contact c : this.contacts)
|
||||||
|
{
|
||||||
|
if (c.getNode().equals(n))
|
||||||
|
{
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* This contact does not exist */
|
||||||
|
throw new NoSuchElementException("The contact does not exist in the contacts list.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized Contact removeFromContacts(Node n)
|
||||||
|
{
|
||||||
|
for (Contact c : this.contacts)
|
||||||
|
{
|
||||||
|
if (c.getNode().equals(n))
|
||||||
|
{
|
||||||
|
this.contacts.remove(c);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* We got here means this element does not exist */
|
||||||
|
throw new NoSuchElementException("Node does not exist in the replacement cache. ");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized boolean removeNode(Node n)
|
||||||
|
{
|
||||||
|
return this.removeContact(new Contact(n));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized int numContacts()
|
||||||
|
{
|
||||||
|
return this.contacts.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized int getDepth()
|
||||||
|
{
|
||||||
|
return this.depth;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized List<Contact> getContacts()
|
||||||
|
{
|
||||||
|
final ArrayList<Contact> ret = new ArrayList<>();
|
||||||
|
|
||||||
|
/* If we have no contacts, return the blank arraylist */
|
||||||
|
if (this.contacts.isEmpty())
|
||||||
|
{
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* We have contacts, lets copy put them into the arraylist and return */
|
||||||
|
for (Contact c : this.contacts)
|
||||||
|
{
|
||||||
|
ret.add(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the bucket is filled, we keep extra contacts in the replacement cache.
|
||||||
|
*/
|
||||||
|
private synchronized void insertIntoReplacementCache(Contact c)
|
||||||
|
{
|
||||||
|
/* Just return if this contact is already in our replacement cache */
|
||||||
|
if (this.replacementCache.contains(c))
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* If the contact is already in the bucket, lets update that we've seen it
|
||||||
|
* We need to remove and re-add the contact to get the Sorted Set to update sort order
|
||||||
|
*/
|
||||||
|
Contact tmp = this.removeFromReplacementCache(c.getNode());
|
||||||
|
tmp.setSeenNow();
|
||||||
|
this.replacementCache.add(tmp);
|
||||||
|
}
|
||||||
|
else if (this.replacementCache.size() > this.config.k())
|
||||||
|
{
|
||||||
|
/* if our cache is filled, we remove the least recently seen contact */
|
||||||
|
this.replacementCache.remove(this.replacementCache.last());
|
||||||
|
this.replacementCache.add(c);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.replacementCache.add(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized Contact removeFromReplacementCache(Node n)
|
||||||
|
{
|
||||||
|
for (Contact c : this.replacementCache)
|
||||||
|
{
|
||||||
|
if (c.getNode().equals(n))
|
||||||
|
{
|
||||||
|
this.replacementCache.remove(c);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* We got here means this element does not exist */
|
||||||
|
throw new NoSuchElementException("Node does not exist in the replacement cache. ");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized String toString()
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder("Bucket at depth: ");
|
||||||
|
sb.append(this.depth);
|
||||||
|
sb.append("\n Nodes: \n");
|
||||||
|
for (Contact n : this.contacts)
|
||||||
|
{
|
||||||
|
sb.append("Node: ");
|
||||||
|
sb.append(n.getNode().getNodeId().toString());
|
||||||
|
sb.append(" (stale: ");
|
||||||
|
sb.append(n.staleCount());
|
||||||
|
sb.append(")");
|
||||||
|
sb.append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,238 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.routing;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KadConfiguration;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.KeyComparator;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.Node;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.KademliaId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of a Kademlia routing table
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @created 20140215
|
||||||
|
*/
|
||||||
|
public class JKademliaRoutingTable implements KademliaRoutingTable
|
||||||
|
{
|
||||||
|
|
||||||
|
private final Node localNode; // The current node
|
||||||
|
private transient KademliaBucket[] buckets;
|
||||||
|
|
||||||
|
private transient KadConfiguration config;
|
||||||
|
|
||||||
|
public JKademliaRoutingTable(Node localNode, KadConfiguration config)
|
||||||
|
{
|
||||||
|
this.localNode = localNode;
|
||||||
|
this.config = config;
|
||||||
|
|
||||||
|
/* Initialize all of the buckets to a specific depth */
|
||||||
|
this.initialize();
|
||||||
|
|
||||||
|
/* Insert the local node */
|
||||||
|
this.insert(localNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the JKademliaRoutingTable to it's default state
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public final void initialize()
|
||||||
|
{
|
||||||
|
this.buckets = new KademliaBucket[KademliaId.ID_LENGTH];
|
||||||
|
for (int i = 0; i < KademliaId.ID_LENGTH; i++)
|
||||||
|
{
|
||||||
|
buckets[i] = new JKademliaBucket(i, this.config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setConfiguration(KadConfiguration config)
|
||||||
|
{
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a contact to the routing table based on how far it is from the LocalNode.
|
||||||
|
*
|
||||||
|
* @param c The contact to add
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public synchronized final void insert(Contact c)
|
||||||
|
{
|
||||||
|
this.buckets[this.getBucketId(c.getNode().getNodeId())].insert(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a node to the routing table based on how far it is from the LocalNode.
|
||||||
|
*
|
||||||
|
* @param n The node to add
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public synchronized final void insert(Node n)
|
||||||
|
{
|
||||||
|
this.buckets[this.getBucketId(n.getNodeId())].insert(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the bucket ID in which a given node should be placed; the bucketId is computed based on how far the node is away from the Local Node.
|
||||||
|
*
|
||||||
|
* @param nid The NodeId for which we want to find which bucket it belong to
|
||||||
|
*
|
||||||
|
* @return Integer The bucket ID in which the given node should be placed.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public final int getBucketId(KademliaId nid)
|
||||||
|
{
|
||||||
|
int bId = this.localNode.getNodeId().getDistance(nid) - 1;
|
||||||
|
|
||||||
|
/* If we are trying to insert a node into it's own routing table, then the bucket ID will be -1, so let's just keep it in bucket 0 */
|
||||||
|
return bId < 0 ? 0 : bId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the closest set of contacts to a given NodeId
|
||||||
|
*
|
||||||
|
* @param target The NodeId to find contacts close to
|
||||||
|
* @param numNodesRequired The number of contacts to find
|
||||||
|
*
|
||||||
|
* @return List A List of contacts closest to target
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public synchronized final List<Node> findClosest(KademliaId target, int numNodesRequired)
|
||||||
|
{
|
||||||
|
TreeSet<Node> sortedSet = new TreeSet<>(new KeyComparator(target));
|
||||||
|
sortedSet.addAll(this.getAllNodes());
|
||||||
|
|
||||||
|
List<Node> closest = new ArrayList<>(numNodesRequired);
|
||||||
|
|
||||||
|
/* Now we have the sorted set, lets get the top numRequired */
|
||||||
|
int count = 0;
|
||||||
|
for (Node n : sortedSet)
|
||||||
|
{
|
||||||
|
closest.add(n);
|
||||||
|
if (++count == numNodesRequired)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return closest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return List A List of all Nodes in this JKademliaRoutingTable
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public synchronized final List<Node> getAllNodes()
|
||||||
|
{
|
||||||
|
List<Node> nodes = new ArrayList<>();
|
||||||
|
|
||||||
|
for (KademliaBucket b : this.buckets)
|
||||||
|
{
|
||||||
|
for (Contact c : b.getContacts())
|
||||||
|
{
|
||||||
|
nodes.add(c.getNode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return List A List of all Nodes in this JKademliaRoutingTable
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public final List<Contact> getAllContacts()
|
||||||
|
{
|
||||||
|
List<Contact> contacts = new ArrayList<>();
|
||||||
|
|
||||||
|
for (KademliaBucket b : this.buckets)
|
||||||
|
{
|
||||||
|
contacts.addAll(b.getContacts());
|
||||||
|
}
|
||||||
|
|
||||||
|
return contacts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Bucket[] The buckets in this Kad Instance
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public final KademliaBucket[] getBuckets()
|
||||||
|
{
|
||||||
|
return this.buckets;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the KadBuckets of this routing table, mainly used when retrieving saved state
|
||||||
|
*
|
||||||
|
* @param buckets
|
||||||
|
*/
|
||||||
|
public final void setBuckets(KademliaBucket[] buckets)
|
||||||
|
{
|
||||||
|
this.buckets = buckets;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method used by operations to notify the routing table of any contacts that have been unresponsive.
|
||||||
|
*
|
||||||
|
* @param contacts The set of unresponsive contacts
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void setUnresponsiveContacts(List<Node> contacts)
|
||||||
|
{
|
||||||
|
if (contacts.isEmpty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (Node n : contacts)
|
||||||
|
{
|
||||||
|
this.setUnresponsiveContact(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method used by operations to notify the routing table of any contacts that have been unresponsive.
|
||||||
|
*
|
||||||
|
* @param n
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public synchronized void setUnresponsiveContact(Node n)
|
||||||
|
{
|
||||||
|
int bucketId = this.getBucketId(n.getNodeId());
|
||||||
|
|
||||||
|
/* Remove the contact from the bucket */
|
||||||
|
this.buckets[bucketId].removeNode(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized final String toString()
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder("\nPrinting Routing Table Started ***************** \n");
|
||||||
|
int totalContacts = 0;
|
||||||
|
for (KademliaBucket b : this.buckets)
|
||||||
|
{
|
||||||
|
if (b.numContacts() > 0)
|
||||||
|
{
|
||||||
|
totalContacts += b.numContacts();
|
||||||
|
sb.append("# nodes in Bucket with depth ");
|
||||||
|
sb.append(b.getDepth());
|
||||||
|
sb.append(": ");
|
||||||
|
sb.append(b.numContacts());
|
||||||
|
sb.append("\n");
|
||||||
|
sb.append(b.toString());
|
||||||
|
sb.append("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.append("\nTotal Contacts: ");
|
||||||
|
sb.append(totalContacts);
|
||||||
|
sb.append("\n\n");
|
||||||
|
|
||||||
|
sb.append("Printing Routing Table Ended ******************** ");
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,87 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.routing;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.Node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A bucket used to store Contacts in the routing table.
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @created 20140215
|
||||||
|
*/
|
||||||
|
public interface KademliaBucket
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a contact to the bucket
|
||||||
|
*
|
||||||
|
* @param c the new contact
|
||||||
|
*/
|
||||||
|
public void insert(Contact c);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new contact and insert it into the bucket.
|
||||||
|
*
|
||||||
|
* @param n The node to create the contact from
|
||||||
|
*/
|
||||||
|
public void insert(Node n);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if this bucket contain a contact
|
||||||
|
*
|
||||||
|
* @param c The contact to check for
|
||||||
|
*
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
public boolean containsContact(Contact c);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if this bucket contain a node
|
||||||
|
*
|
||||||
|
* @param n The node to check for
|
||||||
|
*
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
public boolean containsNode(Node n);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a contact from this bucket.
|
||||||
|
*
|
||||||
|
* If there are replacement contacts in the replacement cache,
|
||||||
|
* select the last seen one and put it into the bucket while removing the required contact.
|
||||||
|
*
|
||||||
|
* If there are no contacts in the replacement cache, then we just mark the contact requested to be removed as stale.
|
||||||
|
* Marking as stale would actually be incrementing the stale count of the contact.
|
||||||
|
*
|
||||||
|
* @param c The contact to remove
|
||||||
|
*
|
||||||
|
* @return Boolean whether the removal was successful.
|
||||||
|
*/
|
||||||
|
public boolean removeContact(Contact c);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the contact object related to a node from this bucket
|
||||||
|
*
|
||||||
|
* @param n The node of the contact to remove
|
||||||
|
*
|
||||||
|
* @return Boolean whether the removal was successful.
|
||||||
|
*/
|
||||||
|
public boolean removeNode(Node n);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts the number of contacts in this bucket.
|
||||||
|
*
|
||||||
|
* @return Integer The number of contacts in this bucket
|
||||||
|
*/
|
||||||
|
public int numContacts();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Integer The depth of this bucket in the RoutingTable
|
||||||
|
*/
|
||||||
|
public int getDepth();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return An Iterable structure with all contacts in this bucket
|
||||||
|
*/
|
||||||
|
public List<Contact> getContacts();
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.routing;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KadConfiguration;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.Node;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.KademliaId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specification for Kademlia's Routing Table
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @since 20140501
|
||||||
|
*/
|
||||||
|
public interface KademliaRoutingTable
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the RoutingTable to it's default state
|
||||||
|
*/
|
||||||
|
public void initialize();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the configuration file for this routing table
|
||||||
|
*
|
||||||
|
* @param config
|
||||||
|
*/
|
||||||
|
public void setConfiguration(KadConfiguration config);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a contact to the routing table based on how far it is from the LocalNode.
|
||||||
|
*
|
||||||
|
* @param c The contact to add
|
||||||
|
*/
|
||||||
|
public void insert(Contact c);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a node to the routing table based on how far it is from the LocalNode.
|
||||||
|
*
|
||||||
|
* @param n The node to add
|
||||||
|
*/
|
||||||
|
public void insert(Node n);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the bucket ID in which a given node should be placed; the bucketId is computed based on how far the node is away from the Local Node.
|
||||||
|
*
|
||||||
|
* @param nid The NodeId for which we want to find which bucket it belong to
|
||||||
|
*
|
||||||
|
* @return Integer The bucket ID in which the given node should be placed.
|
||||||
|
*/
|
||||||
|
public int getBucketId(KademliaId nid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the closest set of contacts to a given NodeId
|
||||||
|
*
|
||||||
|
* @param target The NodeId to find contacts close to
|
||||||
|
* @param numNodesRequired The number of contacts to find
|
||||||
|
*
|
||||||
|
* @return List A List of contacts closest to target
|
||||||
|
*/
|
||||||
|
public List<Node> findClosest(KademliaId target, int numNodesRequired);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return List A List of all Nodes in this RoutingTable
|
||||||
|
*/
|
||||||
|
public List getAllNodes();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return List A List of all Nodes in this RoutingTable
|
||||||
|
*/
|
||||||
|
public List getAllContacts();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Bucket[] The buckets in this Kad Instance
|
||||||
|
*/
|
||||||
|
public KademliaBucket[] getBuckets();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method used by operations to notify the routing table of any contacts that have been unresponsive.
|
||||||
|
*
|
||||||
|
* @param contacts The set of unresponsive contacts
|
||||||
|
*/
|
||||||
|
public void setUnresponsiveContacts(List<Node> contacts);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method used by operations to notify the routing table of any contacts that have been unresponsive.
|
||||||
|
*
|
||||||
|
* @param n
|
||||||
|
*/
|
||||||
|
public void setUnresponsiveContact(Node n);
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,100 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.util;
|
||||||
|
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class that is used to calculate the hash of strings.
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @since 20140405
|
||||||
|
*/
|
||||||
|
public class HashCalculator
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the SHA-1 Hash.
|
||||||
|
*
|
||||||
|
* @param toHash The string to hash
|
||||||
|
*
|
||||||
|
* @return byte[20] The hashed string
|
||||||
|
*
|
||||||
|
* @throws NoSuchAlgorithmException
|
||||||
|
*/
|
||||||
|
public static byte[] sha1Hash(String toHash) throws NoSuchAlgorithmException
|
||||||
|
{
|
||||||
|
/* Create a MessageDigest */
|
||||||
|
MessageDigest md = MessageDigest.getInstance("SHA-1");
|
||||||
|
|
||||||
|
/* Add password bytes to digest */
|
||||||
|
md.update(toHash.getBytes());
|
||||||
|
|
||||||
|
/* Get the hashed bytes */
|
||||||
|
return md.digest();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the SHA-1 Hash using a Salt.
|
||||||
|
*
|
||||||
|
* @param toHash The string to hash
|
||||||
|
* @param salt A salt used to blind the hash
|
||||||
|
*
|
||||||
|
* @return byte[20] The hashed string
|
||||||
|
*
|
||||||
|
* @throws NoSuchAlgorithmException
|
||||||
|
*/
|
||||||
|
public static byte[] sha1Hash(String toHash, String salt) throws NoSuchAlgorithmException
|
||||||
|
{
|
||||||
|
/* Create a MessageDigest */
|
||||||
|
MessageDigest md = MessageDigest.getInstance("SHA-1");
|
||||||
|
|
||||||
|
/* Add password bytes to digest */
|
||||||
|
md.update(toHash.getBytes());
|
||||||
|
|
||||||
|
/* Get the hashed bytes */
|
||||||
|
return md.digest(salt.getBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the MD5 Hash.
|
||||||
|
*
|
||||||
|
* @param toHash The string to hash
|
||||||
|
*
|
||||||
|
* @return byte[16] The hashed string
|
||||||
|
*
|
||||||
|
* @throws NoSuchAlgorithmException
|
||||||
|
*/
|
||||||
|
public static byte[] md5Hash(String toHash) throws NoSuchAlgorithmException
|
||||||
|
{
|
||||||
|
/* Create a MessageDigest */
|
||||||
|
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||||
|
|
||||||
|
/* Add password bytes to digest */
|
||||||
|
md.update(toHash.getBytes());
|
||||||
|
|
||||||
|
/* Get the hashed bytes */
|
||||||
|
return md.digest();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the MD5 Hash using a salt.
|
||||||
|
*
|
||||||
|
* @param toHash The string to hash
|
||||||
|
* @param salt A salt used to blind the hash
|
||||||
|
*
|
||||||
|
* @return byte[16] The hashed string
|
||||||
|
*
|
||||||
|
* @throws NoSuchAlgorithmException
|
||||||
|
*/
|
||||||
|
public static byte[] md5Hash(String toHash, String salt) throws NoSuchAlgorithmException
|
||||||
|
{
|
||||||
|
/* Create a MessageDigest */
|
||||||
|
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||||
|
|
||||||
|
/* Add password bytes to digest */
|
||||||
|
md.update(toHash.getBytes());
|
||||||
|
|
||||||
|
/* Get the hashed bytes */
|
||||||
|
return md.digest(salt.getBytes());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,92 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.util;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.node.Node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class that helps compute the route length taken to complete an operation.
|
||||||
|
*
|
||||||
|
* Only used for routing operations - mainly the NodeLookup and ContentLookup Operations.
|
||||||
|
*
|
||||||
|
* Idea:
|
||||||
|
* - Add the original set of nodes with route length 0;
|
||||||
|
* - When we get a node reply with a set of nodes, we add those nodes and set the route length to their sender route length + 1
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @since 20140510
|
||||||
|
*/
|
||||||
|
public class RouteLengthChecker
|
||||||
|
{
|
||||||
|
|
||||||
|
/* Store the nodes and their route length (RL) */
|
||||||
|
private final HashMap<Node, Integer> nodes;
|
||||||
|
|
||||||
|
/* Lets cache the max route length instead of having to go and search for it later */
|
||||||
|
private int maxRouteLength;
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
this.nodes = new HashMap<>();
|
||||||
|
this.maxRouteLength = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the initial nodes in the routing operation
|
||||||
|
*
|
||||||
|
* @param initialNodes The set of initial nodes
|
||||||
|
*/
|
||||||
|
public void addInitialNodes(Collection<Node> initialNodes)
|
||||||
|
{
|
||||||
|
for (Node n : initialNodes)
|
||||||
|
{
|
||||||
|
this.nodes.put(n, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add any nodes that we get from a node reply.
|
||||||
|
*
|
||||||
|
* The route length of these nodes will be their sender + 1;
|
||||||
|
*
|
||||||
|
* @param inputSet The set of nodes we receive
|
||||||
|
* @param sender The node who send the set
|
||||||
|
*/
|
||||||
|
public void addNodes(Collection<Node> inputSet, Node sender)
|
||||||
|
{
|
||||||
|
if (!this.nodes.containsKey(sender))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Get the route length of the input set - sender RL + 1 */
|
||||||
|
int inputSetRL = this.nodes.get(sender) + 1;
|
||||||
|
|
||||||
|
if (inputSetRL > this.maxRouteLength)
|
||||||
|
{
|
||||||
|
this.maxRouteLength = inputSetRL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add the nodes to our set */
|
||||||
|
for (Node n : inputSet)
|
||||||
|
{
|
||||||
|
/* We only add if the node is not already there... */
|
||||||
|
if (!this.nodes.containsKey(n))
|
||||||
|
{
|
||||||
|
this.nodes.put(n, inputSetRL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the route length of the operation!
|
||||||
|
*
|
||||||
|
* It will be the max route length of all the nodes here.
|
||||||
|
*
|
||||||
|
* @return The route length
|
||||||
|
*/
|
||||||
|
public int getRouteLength()
|
||||||
|
{
|
||||||
|
return this.maxRouteLength;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,95 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.util.serializer;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
import com.google.gson.stream.JsonReader;
|
||||||
|
import com.google.gson.stream.JsonWriter;
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.util.List;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.dht.DHT;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.dht.KademliaDHT;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.dht.KademliaStorageEntryMetadata;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A KadSerializer that serializes DHT to JSON format
|
||||||
|
* The generic serializer is not working for DHT
|
||||||
|
*
|
||||||
|
* Why a DHT specific serializer?
|
||||||
|
* The DHT structure:
|
||||||
|
* - DHT
|
||||||
|
* -- StorageEntriesManager
|
||||||
|
* --- Map<NodeId, List<StorageEntry>>
|
||||||
|
* ---- NodeId:KeyBytes
|
||||||
|
* ---- List<StorageEntry>
|
||||||
|
* ----- StorageEntry: Key, OwnerId, Type, Hash
|
||||||
|
*
|
||||||
|
* The above structure seems to be causing some problem for Gson, especially at the Map part.
|
||||||
|
*
|
||||||
|
* Solution
|
||||||
|
* - Make the StorageEntriesManager transient
|
||||||
|
* - Simply store all StorageEntry in the serialized object
|
||||||
|
* - When reloading, re-add all StorageEntry to the DHT
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
*
|
||||||
|
* @since 20140310
|
||||||
|
*/
|
||||||
|
public class JsonDHTSerializer implements KadSerializer<KademliaDHT>
|
||||||
|
{
|
||||||
|
|
||||||
|
private final Gson gson;
|
||||||
|
private final Type storageEntriesCollectionType;
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
gson = new Gson();
|
||||||
|
|
||||||
|
storageEntriesCollectionType = new TypeToken<List<KademliaStorageEntryMetadata>>()
|
||||||
|
{
|
||||||
|
}.getType();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(KademliaDHT data, DataOutputStream out) throws IOException
|
||||||
|
{
|
||||||
|
try (JsonWriter writer = new JsonWriter(new OutputStreamWriter(out)))
|
||||||
|
{
|
||||||
|
writer.beginArray();
|
||||||
|
|
||||||
|
/* Write the basic DHT */
|
||||||
|
gson.toJson(data, DHT.class, writer);
|
||||||
|
|
||||||
|
/* Now Store the Entries */
|
||||||
|
gson.toJson(data.getStorageEntries(), this.storageEntriesCollectionType, writer);
|
||||||
|
|
||||||
|
writer.endArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KademliaDHT read(DataInputStream in) throws IOException, ClassNotFoundException
|
||||||
|
{
|
||||||
|
try (DataInputStream din = new DataInputStream(in);
|
||||||
|
JsonReader reader = new JsonReader(new InputStreamReader(in)))
|
||||||
|
{
|
||||||
|
reader.beginArray();
|
||||||
|
|
||||||
|
/* Read the basic DHT */
|
||||||
|
DHT dht = gson.fromJson(reader, DHT.class);
|
||||||
|
dht.initialize();
|
||||||
|
|
||||||
|
/* Now get the entries and add them back to the DHT */
|
||||||
|
List<KademliaStorageEntryMetadata> entries = gson.fromJson(reader, this.storageEntriesCollectionType);
|
||||||
|
dht.putStorageEntries(entries);
|
||||||
|
|
||||||
|
reader.endArray();
|
||||||
|
return dht;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,112 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.util.serializer;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
import com.google.gson.stream.JsonReader;
|
||||||
|
import com.google.gson.stream.JsonWriter;
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.routing.JKademliaRoutingTable;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.util.List;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.KadConfiguration;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.routing.Contact;
|
||||||
|
import io.github.chronosx88.dhtBootstrap.kademlia.routing.KademliaRoutingTable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A KadSerializer that serializes routing tables to JSON format
|
||||||
|
The generic serializer is not working for routing tables
|
||||||
|
|
||||||
|
Why a JKademliaRoutingTable specific serializer?
|
||||||
|
The routing table structure:
|
||||||
|
- JKademliaRoutingTable
|
||||||
|
-- Buckets[]
|
||||||
|
--- Map<NodeId, Node>
|
||||||
|
* ---- NodeId:KeyBytes
|
||||||
|
* ---- Node: NodeId, InetAddress, Port
|
||||||
|
*
|
||||||
|
* The above structure seems to be causing some problem for Gson,
|
||||||
|
* especially at the Map part.
|
||||||
|
*
|
||||||
|
* Solution
|
||||||
|
- Make the Buckets[] transient
|
||||||
|
- Simply store all Nodes in the serialized object
|
||||||
|
- When reloading, re-add all nodes to the JKademliaRoutingTable
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
*
|
||||||
|
* @since 20140310
|
||||||
|
*/
|
||||||
|
public class JsonRoutingTableSerializer implements KadSerializer<KademliaRoutingTable>
|
||||||
|
{
|
||||||
|
|
||||||
|
private final Gson gson;
|
||||||
|
|
||||||
|
Type contactCollectionType = new TypeToken<List<Contact>>()
|
||||||
|
{
|
||||||
|
}.getType();
|
||||||
|
|
||||||
|
private final KadConfiguration config;
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
gson = new Gson();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the class
|
||||||
|
*
|
||||||
|
* @param config
|
||||||
|
*/
|
||||||
|
public JsonRoutingTableSerializer(KadConfiguration config)
|
||||||
|
{
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(KademliaRoutingTable data, DataOutputStream out) throws IOException
|
||||||
|
{
|
||||||
|
try (JsonWriter writer = new JsonWriter(new OutputStreamWriter(out)))
|
||||||
|
{
|
||||||
|
writer.beginArray();
|
||||||
|
|
||||||
|
/* Write the basic JKademliaRoutingTable */
|
||||||
|
gson.toJson(data, JKademliaRoutingTable.class, writer);
|
||||||
|
|
||||||
|
/* Now Store the Contacts */
|
||||||
|
gson.toJson(data.getAllContacts(), contactCollectionType, writer);
|
||||||
|
|
||||||
|
writer.endArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KademliaRoutingTable read(DataInputStream in) throws IOException, ClassNotFoundException
|
||||||
|
{
|
||||||
|
try (DataInputStream din = new DataInputStream(in);
|
||||||
|
JsonReader reader = new JsonReader(new InputStreamReader(in)))
|
||||||
|
{
|
||||||
|
reader.beginArray();
|
||||||
|
|
||||||
|
/* Read the basic JKademliaRoutingTable */
|
||||||
|
KademliaRoutingTable tbl = gson.fromJson(reader, KademliaRoutingTable.class);
|
||||||
|
tbl.setConfiguration(config);
|
||||||
|
|
||||||
|
/* Now get the Contacts and add them back to the JKademliaRoutingTable */
|
||||||
|
List<Contact> contacts = gson.fromJson(reader, contactCollectionType);
|
||||||
|
tbl.initialize();
|
||||||
|
|
||||||
|
for (Contact c : contacts)
|
||||||
|
{
|
||||||
|
tbl.insert(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.endArray();
|
||||||
|
/* Read and return the Content*/
|
||||||
|
return tbl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.util.serializer;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.stream.JsonReader;
|
||||||
|
import com.google.gson.stream.JsonWriter;
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A KadSerializer that serializes content to JSON format
|
||||||
|
*
|
||||||
|
* @param <T> The type of content to serialize
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
*
|
||||||
|
* @since 20140225
|
||||||
|
*/
|
||||||
|
public class JsonSerializer<T> implements KadSerializer<T>
|
||||||
|
{
|
||||||
|
|
||||||
|
private final Gson gson;
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
gson = new Gson();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(T data, DataOutputStream out) throws IOException
|
||||||
|
{
|
||||||
|
try (JsonWriter writer = new JsonWriter(new OutputStreamWriter(out)))
|
||||||
|
{
|
||||||
|
writer.beginArray();
|
||||||
|
|
||||||
|
/* Store the content type */
|
||||||
|
gson.toJson(data.getClass().getName(), String.class, writer);
|
||||||
|
|
||||||
|
/* Now Store the content */
|
||||||
|
gson.toJson(data, data.getClass(), writer);
|
||||||
|
|
||||||
|
writer.endArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T read(DataInputStream in) throws IOException, ClassNotFoundException
|
||||||
|
{
|
||||||
|
try (DataInputStream din = new DataInputStream(in);
|
||||||
|
JsonReader reader = new JsonReader(new InputStreamReader(in)))
|
||||||
|
{
|
||||||
|
reader.beginArray();
|
||||||
|
|
||||||
|
/* Read the class name */
|
||||||
|
String className = gson.fromJson(reader, String.class);
|
||||||
|
|
||||||
|
/* Read and return the Content*/
|
||||||
|
T ret = gson.fromJson(reader, Class.forName(className));
|
||||||
|
|
||||||
|
reader.endArray();
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
package io.github.chronosx88.dhtBootstrap.kademlia.util.serializer;
|
||||||
|
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Serializer is used to transform data to and from a specified form.
|
||||||
|
*
|
||||||
|
* Here we define the structure of any Serializer used in Kademlia
|
||||||
|
*
|
||||||
|
* @author Joshua Kissoon
|
||||||
|
* @param <T> The type of content being serialized
|
||||||
|
*
|
||||||
|
* @since 20140225
|
||||||
|
*/
|
||||||
|
public interface KadSerializer<T>
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a KadContent to a DataOutput stream
|
||||||
|
*
|
||||||
|
* @param data The data to write
|
||||||
|
* @param out The output Stream to write to
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public void write(T data, DataOutputStream out) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read data of type T from a DataInput Stream
|
||||||
|
*
|
||||||
|
* @param in The InputStream to read the data from
|
||||||
|
*
|
||||||
|
* @return T Data of type T
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
* @throws ClassNotFoundException
|
||||||
|
*/
|
||||||
|
public T read(DataInputStream in) throws IOException, ClassNotFoundException;
|
||||||
|
}
|
3
src/main/resources/META-INF/MANIFEST.MF
Normal file
3
src/main/resources/META-INF/MANIFEST.MF
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Manifest-Version: 1.0
|
||||||
|
Main-Class: io.github.chronosx88.dhtBootstrap.Main
|
||||||
|
|
Loading…
Reference in New Issue
Block a user