From 52004544471bfd5cd666b36a9176fe30e2a86b39 Mon Sep 17 00:00:00 2001 From: ChronosX88 Date: Thu, 28 Feb 2019 15:21:49 +0400 Subject: [PATCH] Init commit --- .gitignore | 13 + .../Kademlia_Bootstrap_Node_main_jar.xml | 9 + .idea/encodings.xml | 4 + .idea/gradle.xml | 17 + .idea/misc.xml | 7 + .idea/vcs.xml | 6 + build.gradle | 23 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 56172 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 +++++++ gradlew.bat | 84 ++++ settings.gradle | 2 + .../github/chronosx88/dhtBootstrap/Main.java | 18 + .../kademlia/DefaultConfiguration.java | 101 +++++ .../dhtBootstrap/kademlia/JKademliaNode.java | 428 ++++++++++++++++++ .../kademlia/KadConfiguration.java | 63 +++ .../dhtBootstrap/kademlia/KadServer.java | 356 +++++++++++++++ .../kademlia/KadStatistician.java | 87 ++++ .../dhtBootstrap/kademlia/KademliaNode.java | 154 +++++++ .../dhtBootstrap/kademlia/Statistician.java | 182 ++++++++ .../dhtBootstrap/kademlia/dht/DHT.java | 265 +++++++++++ .../kademlia/dht/GetParameter.java | 117 +++++ .../kademlia/dht/JKademliaStorageEntry.java | 59 +++ .../dhtBootstrap/kademlia/dht/KadContent.java | 65 +++ .../kademlia/dht/KademliaDHT.java | 122 +++++ .../kademlia/dht/KademliaStorageEntry.java | 32 ++ .../dht/KademliaStorageEntryMetadata.java | 59 +++ .../kademlia/dht/StorageEntryMetadata.java | 152 +++++++ .../kademlia/dht/StoredContentManager.java | 202 +++++++++ .../exceptions/ContentExistException.java | 21 + .../exceptions/ContentNotFoundException.java | 21 + .../exceptions/KadServerDownException.java | 21 + .../kademlia/exceptions/RoutingException.java | 23 + .../exceptions/UnknownMessageException.java | 21 + .../kademlia/message/AcknowledgeMessage.java | 59 +++ .../kademlia/message/ConnectMessage.java | 58 +++ .../kademlia/message/ConnectReceiver.java | 58 +++ .../message/ContentLookupMessage.java | 80 ++++ .../message/ContentLookupReceiver.java | 69 +++ .../kademlia/message/ContentMessage.java | 85 ++++ .../message/KademliaMessageFactory.java | 37 ++ .../kademlia/message/Message.java | 14 + .../kademlia/message/MessageFactory.java | 76 ++++ .../kademlia/message/NodeLookupMessage.java | 75 +++ .../kademlia/message/NodeLookupReceiver.java | 72 +++ .../kademlia/message/NodeReplyMessage.java | 94 ++++ .../kademlia/message/Receiver.java | 33 ++ .../kademlia/message/SimpleMessage.java | 72 +++ .../kademlia/message/SimpleReceiver.java | 25 + .../kademlia/message/StoreContentMessage.java | 84 ++++ .../message/StoreContentReceiver.java | 57 +++ .../kademlia/message/Streamable.java | 42 ++ .../kademlia/node/KademliaId.java | 264 +++++++++++ .../kademlia/node/KeyComparator.java | 44 ++ .../dhtBootstrap/kademlia/node/Node.java | 134 ++++++ .../operation/BucketRefreshOperation.java | 66 +++ .../kademlia/operation/ConnectOperation.java | 140 ++++++ .../operation/ContentLookupOperation.java | 342 ++++++++++++++ .../operation/ContentRefreshOperation.java | 99 ++++ .../operation/KadRefreshOperation.java | 40 ++ .../operation/NodeLookupOperation.java | 323 +++++++++++++ .../kademlia/operation/Operation.java | 21 + .../kademlia/operation/PingOperation.java | 39 ++ .../kademlia/operation/StoreOperation.java | 83 ++++ .../kademlia/routing/Contact.java | 118 +++++ .../routing/ContactLastSeenComparator.java | 34 ++ .../kademlia/routing/JKademliaBucket.java | 275 +++++++++++ .../routing/JKademliaRoutingTable.java | 238 ++++++++++ .../kademlia/routing/KademliaBucket.java | 87 ++++ .../routing/KademliaRoutingTable.java | 91 ++++ .../kademlia/util/HashCalculator.java | 100 ++++ .../kademlia/util/RouteLengthChecker.java | 92 ++++ .../util/serializer/JsonDHTSerializer.java | 95 ++++ .../JsonRoutingTableSerializer.java | 112 +++++ .../util/serializer/JsonSerializer.java | 67 +++ .../util/serializer/KadSerializer.java | 41 ++ src/main/resources/META-INF/MANIFEST.MF | 3 + 77 files changed, 6950 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/artifacts/Kademlia_Bootstrap_Node_main_jar.xml create mode 100644 .idea/encodings.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/vcs.xml create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/Main.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/DefaultConfiguration.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/JKademliaNode.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/KadConfiguration.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/KadServer.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/KadStatistician.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/KademliaNode.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/Statistician.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/DHT.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/GetParameter.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/JKademliaStorageEntry.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/KadContent.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/KademliaDHT.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/KademliaStorageEntry.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/KademliaStorageEntryMetadata.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/StorageEntryMetadata.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/StoredContentManager.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/exceptions/ContentExistException.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/exceptions/ContentNotFoundException.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/exceptions/KadServerDownException.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/exceptions/RoutingException.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/exceptions/UnknownMessageException.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/AcknowledgeMessage.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/ConnectMessage.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/ConnectReceiver.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/ContentLookupMessage.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/ContentLookupReceiver.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/ContentMessage.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/KademliaMessageFactory.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/Message.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/MessageFactory.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/NodeLookupMessage.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/NodeLookupReceiver.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/NodeReplyMessage.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/Receiver.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/SimpleMessage.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/SimpleReceiver.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/StoreContentMessage.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/StoreContentReceiver.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/Streamable.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/node/KademliaId.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/node/KeyComparator.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/node/Node.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/BucketRefreshOperation.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/ConnectOperation.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/ContentLookupOperation.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/ContentRefreshOperation.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/KadRefreshOperation.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/NodeLookupOperation.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/Operation.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/PingOperation.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/StoreOperation.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/routing/Contact.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/routing/ContactLastSeenComparator.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/routing/JKademliaBucket.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/routing/JKademliaRoutingTable.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/routing/KademliaBucket.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/routing/KademliaRoutingTable.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/util/HashCalculator.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/util/RouteLengthChecker.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/util/serializer/JsonDHTSerializer.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/util/serializer/JsonRoutingTableSerializer.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/util/serializer/JsonSerializer.java create mode 100644 src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/util/serializer/KadSerializer.java create mode 100644 src/main/resources/META-INF/MANIFEST.MF diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..38518ea --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.idea/artifacts/Kademlia_Bootstrap_Node_main_jar.xml b/.idea/artifacts/Kademlia_Bootstrap_Node_main_jar.xml new file mode 100644 index 0000000..542932f --- /dev/null +++ b/.idea/artifacts/Kademlia_Bootstrap_Node_main_jar.xml @@ -0,0 +1,9 @@ + + + $PROJECT_DIR$/out/artifacts/Kademlia_Bootstrap_Node_main_jar + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..15a15b2 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..310e0f2 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..bc8d0a3 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..863b157 --- /dev/null +++ b/build.gradle @@ -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' + ) + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..28861d273a5d270fd8f65dd74570c17c9c507736 GIT binary patch literal 56172 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

ZM+x{iQDVZRnLYbdXrQjU&=u%hsN4|smH&B~F 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 b+=;!jU|P+J`>$CfFsoU|bwOU-ceCsYH7qU$ literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3a03f4e --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -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" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..e95643d --- /dev/null +++ b/gradlew.bat @@ -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 diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..3e6e515 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'Kademlia-Bootstrap-Node' + diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/Main.java b/src/main/java/io/github/chronosx88/dhtBootstrap/Main.java new file mode 100644 index 0000000..5861030 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/Main.java @@ -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(); + } + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/DefaultConfiguration.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/DefaultConfiguration.java new file mode 100644 index 0000000..7af62af --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/DefaultConfiguration.java @@ -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; + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/JKademliaNode.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/JKademliaNode.java new file mode 100644 index 0000000..fe96356 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/JKademliaNode.java @@ -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 or 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().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().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().write(this, dout); + + /** + * @section Save the node state + */ + dout = new DataOutputStream(new FileOutputStream(getStateStorageFolderName(this.ownerId, this.config) + File.separator + "node.kns")); + new JsonSerializer().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(); + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/KadConfiguration.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/KadConfiguration.java new file mode 100644 index 0000000..954d0cd --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/KadConfiguration.java @@ -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(); +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/KadServer.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/KadServer.java new file mode 100644 index 0000000..3568ddc --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/KadServer.java @@ -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 receivers; + private final Timer timer; // Schedule future tasks + private final Map 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 cancel() + * method inherited from TimerTask. In this case the caller is + * responsible for removing the task from the tasks 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; + } + +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/KadStatistician.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/KadStatistician.java new file mode 100644 index 0000000..50113e0 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/KadStatistician.java @@ -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(); +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/KademliaNode.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/KademliaNode.java new file mode 100644 index 0000000..0cbe61d --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/KademliaNode.java @@ -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(); +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/Statistician.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/Statistician.java new file mode 100644 index 0000000..25cd9ac --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/Statistician.java @@ -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(); + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/DHT.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/DHT.java new file mode 100644 index 0000000..3b34a4a --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/DHT.java @@ -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 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 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 getStorageEntries() + { + return contentManager.getAllEntries(); + } + + @Override + public void putStorageEntries(List 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(); + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/GetParameter.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/GetParameter.java new file mode 100644 index 0000000..0ca5f7b --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/GetParameter.java @@ -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 + "]"; + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/JKademliaStorageEntry.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/JKademliaStorageEntry.java new file mode 100644 index 0000000..9fa82ba --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/JKademliaStorageEntry.java @@ -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(); + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/KadContent.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/KadContent.java new file mode 100644 index 0000000..835f2d9 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/KadContent.java @@ -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); +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/KademliaDHT.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/KademliaDHT.java new file mode 100644 index 0000000..14a0622 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/KademliaDHT.java @@ -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 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 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 ientries); + +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/KademliaStorageEntry.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/KademliaStorageEntry.java new file mode 100644 index 0000000..f36d516 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/KademliaStorageEntry.java @@ -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(); +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/KademliaStorageEntryMetadata.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/KademliaStorageEntryMetadata.java new file mode 100644 index 0000000..425b15f --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/KademliaStorageEntryMetadata.java @@ -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(); +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/StorageEntryMetadata.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/StorageEntryMetadata.java new file mode 100644 index 0000000..e165f85 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/StorageEntryMetadata.java @@ -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(); + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/StoredContentManager.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/StoredContentManager.java new file mode 100644 index 0000000..1df26ce --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/dht/StoredContentManager.java @@ -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> 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 getAllEntries() + { + List entriesRet = new ArrayList<>(); + + for (List 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 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(); + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/exceptions/ContentExistException.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/exceptions/ContentExistException.java new file mode 100644 index 0000000..7a20d82 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/exceptions/ContentExistException.java @@ -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); + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/exceptions/ContentNotFoundException.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/exceptions/ContentNotFoundException.java new file mode 100644 index 0000000..0ea6b6c --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/exceptions/ContentNotFoundException.java @@ -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); + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/exceptions/KadServerDownException.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/exceptions/KadServerDownException.java new file mode 100644 index 0000000..db0bd47 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/exceptions/KadServerDownException.java @@ -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); + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/exceptions/RoutingException.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/exceptions/RoutingException.java new file mode 100644 index 0000000..0604cc5 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/exceptions/RoutingException.java @@ -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); + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/exceptions/UnknownMessageException.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/exceptions/UnknownMessageException.java new file mode 100644 index 0000000..f8f4d61 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/exceptions/UnknownMessageException.java @@ -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); + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/AcknowledgeMessage.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/AcknowledgeMessage.java new file mode 100644 index 0000000..ad77816 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/AcknowledgeMessage.java @@ -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() + "]"; + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/ConnectMessage.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/ConnectMessage.java new file mode 100644 index 0000000..79569ef --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/ConnectMessage.java @@ -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() + "]"; + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/ConnectReceiver.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/ConnectReceiver.java new file mode 100644 index 0000000..b38166d --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/ConnectReceiver.java @@ -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 + { + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/ContentLookupMessage.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/ContentLookupMessage.java new file mode 100644 index 0000000..300260c --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/ContentLookupMessage.java @@ -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().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().read(in); + } + catch (ClassNotFoundException e) + { + e.printStackTrace(); + } + } + + @Override + public byte code() + { + return CODE; + } + +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/ContentLookupReceiver.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/ContentLookupReceiver.java new file mode 100644 index 0000000..4a96ea7 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/ContentLookupReceiver.java @@ -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) + { + + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/ContentMessage.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/ContentMessage.java new file mode 100644 index 0000000..87f0a36 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/ContentMessage.java @@ -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().write(content, out); + } + + @Override + public final void fromStream(DataInputStream in) throws IOException + { + this.origin = new Node(in); + + try + { + this.content = new JsonSerializer().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 + "]"; + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/KademliaMessageFactory.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/KademliaMessageFactory.java new file mode 100644 index 0000000..bd88f03 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/KademliaMessageFactory.java @@ -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); +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/Message.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/Message.java new file mode 100644 index 0000000..a8e6dcf --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/Message.java @@ -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 byte type there can + * be at most 256 different message types. + * + * @return byte A unique code representing the message type + * */ + public byte code(); +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/MessageFactory.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/MessageFactory.java new file mode 100644 index 0000000..eb457fa --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/MessageFactory.java @@ -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(); + } + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/NodeLookupMessage.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/NodeLookupMessage.java new file mode 100644 index 0000000..e69f512 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/NodeLookupMessage.java @@ -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 + "]"; + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/NodeLookupReceiver.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/NodeLookupReceiver.java new file mode 100644 index 0000000..541a960 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/NodeLookupReceiver.java @@ -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 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 + { + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/NodeReplyMessage.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/NodeReplyMessage.java new file mode 100644 index 0000000..a1132ba --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/NodeReplyMessage.java @@ -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 nodes; + + public NodeReplyMessage(Node origin, List 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 getNodes() + { + return this.nodes; + } + + @Override + public String toString() + { + return "NodeReplyMessage[origin NodeId=" + origin.getNodeId() + "]"; + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/Receiver.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/Receiver.java new file mode 100644 index 0000000..4b3c107 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/Receiver.java @@ -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 MessageServer.TIMEOUT seconds for the + * message with communication id comm, 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; +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/SimpleMessage.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/SimpleMessage.java new file mode 100644 index 0000000..fee31b2 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/SimpleMessage.java @@ -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; + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/SimpleReceiver.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/SimpleReceiver.java new file mode 100644 index 0000000..1a4b8e0 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/SimpleReceiver.java @@ -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."); + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/StoreContentMessage.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/StoreContentMessage.java new file mode 100644 index 0000000..ad0b9e4 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/StoreContentMessage.java @@ -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().write(content, out); + } + + @Override + public final void fromStream(DataInputStream in) throws IOException + { + this.origin = new Node(in); + try + { + this.content = new JsonSerializer().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 + "]"; + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/StoreContentReceiver.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/StoreContentReceiver.java new file mode 100644 index 0000000..812748d --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/StoreContentReceiver.java @@ -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. + */ + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/Streamable.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/Streamable.java new file mode 100644 index 0000000..3ca0e5e --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/message/Streamable.java @@ -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. + *

+ * 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: + *

+ * Streamable(DataInput in) throws IOException; + * */ +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; +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/node/KademliaId.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/node/KademliaId.java new file mode 100644 index 0000000..1932f44 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/node/KademliaId.java @@ -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(); + } + +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/node/KeyComparator.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/node/KeyComparator.java new file mode 100644 index 0000000..132ccf2 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/node/KeyComparator.java @@ -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 +{ + + 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 Node + * 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()); + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/node/Node.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/node/Node.java new file mode 100644 index 0000000..05166a4 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/node/Node.java @@ -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(); + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/BucketRefreshOperation.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/BucketRefreshOperation.java new file mode 100644 index 0000000..f54bbc1 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/BucketRefreshOperation.java @@ -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(); + } + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/ConnectOperation.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/ConnectOperation.java new file mode 100644 index 0000000..261d2f8 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/ConnectOperation.java @@ -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(); + } + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/ContentLookupOperation.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/ContentLookupOperation.java new file mode 100644 index 0000000..17e7051 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/ContentLookupOperation.java @@ -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 nodes; + + /* Tracks messages in transit and awaiting reply */ + private final Map 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 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 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 true if finished OR false 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 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 closestNodesNotFailed(Byte status) + { + List 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(); + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/ContentRefreshOperation.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/ContentRefreshOperation.java new file mode 100644 index 0000000..4e283fb --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/ContentRefreshOperation.java @@ -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 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 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()); + } + } + + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/KadRefreshOperation.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/KadRefreshOperation.java new file mode 100644 index 0000000..978c585 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/KadRefreshOperation.java @@ -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(); + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/NodeLookupOperation.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/NodeLookupOperation.java new file mode 100644 index 0000000..293d09f --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/NodeLookupOperation.java @@ -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 nodes; + + /* Tracks messages in transit and awaiting reply */ + private final Map 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 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 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 true if finished OR false 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 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 closestNodes(String status) + { + List 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 closestNodesNotFailed(String status) + { + List 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(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 getFailedNodes() + { + List failedNodes = new ArrayList<>(); + + for (Map.Entry e : this.nodes.entrySet()) + { + if (e.getValue().equals(FAILED)) + { + failedNodes.add(e.getKey()); + } + } + + return failedNodes; + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/Operation.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/Operation.java new file mode 100644 index 0000000..15abe20 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/Operation.java @@ -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; +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/PingOperation.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/PingOperation.java new file mode 100644 index 0000000..502a69d --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/PingOperation.java @@ -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. + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/StoreOperation.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/StoreOperation.java new file mode 100644 index 0000000..2cde66f --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/operation/StoreOperation.java @@ -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 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; + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/routing/Contact.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/routing/Contact.java new file mode 100644 index 0000000..1d9d061 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/routing/Contact.java @@ -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 +{ + + 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(); + } + +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/routing/ContactLastSeenComparator.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/routing/ContactLastSeenComparator.java new file mode 100644 index 0000000..2ba13b5 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/routing/ContactLastSeenComparator.java @@ -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 +{ + + /** + * 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; + } + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/routing/JKademliaBucket.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/routing/JKademliaBucket.java new file mode 100644 index 0000000..190d354 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/routing/JKademliaBucket.java @@ -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 contacts; + + /* A set of last seen contacts that can replace any current contact that is unresponsive */ + private final TreeSet 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 getContacts() + { + final ArrayList 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(); + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/routing/JKademliaRoutingTable.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/routing/JKademliaRoutingTable.java new file mode 100644 index 0000000..c6eeacc --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/routing/JKademliaRoutingTable.java @@ -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 findClosest(KademliaId target, int numNodesRequired) + { + TreeSet sortedSet = new TreeSet<>(new KeyComparator(target)); + sortedSet.addAll(this.getAllNodes()); + + List 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 getAllNodes() + { + List 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 getAllContacts() + { + List 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 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(); + } + +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/routing/KademliaBucket.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/routing/KademliaBucket.java new file mode 100644 index 0000000..d078fc1 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/routing/KademliaBucket.java @@ -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 getContacts(); +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/routing/KademliaRoutingTable.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/routing/KademliaRoutingTable.java new file mode 100644 index 0000000..af48a26 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/routing/KademliaRoutingTable.java @@ -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 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 contacts); + + /** + * Method used by operations to notify the routing table of any contacts that have been unresponsive. + * + * @param n + */ + public void setUnresponsiveContact(Node n); + +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/util/HashCalculator.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/util/HashCalculator.java new file mode 100644 index 0000000..91127a9 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/util/HashCalculator.java @@ -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()); + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/util/RouteLengthChecker.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/util/RouteLengthChecker.java new file mode 100644 index 0000000..e2a2878 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/util/RouteLengthChecker.java @@ -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 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 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 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; + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/util/serializer/JsonDHTSerializer.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/util/serializer/JsonDHTSerializer.java new file mode 100644 index 0000000..d8b83bf --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/util/serializer/JsonDHTSerializer.java @@ -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:KeyBytes + * ---- List + * ----- 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 +{ + + private final Gson gson; + private final Type storageEntriesCollectionType; + + + { + gson = new Gson(); + + storageEntriesCollectionType = new TypeToken>() + { + }.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 entries = gson.fromJson(reader, this.storageEntriesCollectionType); + dht.putStorageEntries(entries); + + reader.endArray(); + return dht; + } + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/util/serializer/JsonRoutingTableSerializer.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/util/serializer/JsonRoutingTableSerializer.java new file mode 100644 index 0000000..2f54a00 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/util/serializer/JsonRoutingTableSerializer.java @@ -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: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 +{ + + private final Gson gson; + + Type contactCollectionType = new TypeToken>() + { + }.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 contacts = gson.fromJson(reader, contactCollectionType); + tbl.initialize(); + + for (Contact c : contacts) + { + tbl.insert(c); + } + + reader.endArray(); + /* Read and return the Content*/ + return tbl; + } + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/util/serializer/JsonSerializer.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/util/serializer/JsonSerializer.java new file mode 100644 index 0000000..8226b81 --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/util/serializer/JsonSerializer.java @@ -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 The type of content to serialize + * + * @author Joshua Kissoon + * + * @since 20140225 + */ +public class JsonSerializer implements KadSerializer +{ + + 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; + } + } +} diff --git a/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/util/serializer/KadSerializer.java b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/util/serializer/KadSerializer.java new file mode 100644 index 0000000..8d09afa --- /dev/null +++ b/src/main/java/io/github/chronosx88/dhtBootstrap/kademlia/util/serializer/KadSerializer.java @@ -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 The type of content being serialized + * + * @since 20140225 + */ +public interface KadSerializer +{ + + /** + * 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; +} diff --git a/src/main/resources/META-INF/MANIFEST.MF b/src/main/resources/META-INF/MANIFEST.MF new file mode 100644 index 0000000..95eb89b --- /dev/null +++ b/src/main/resources/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: io.github.chronosx88.dhtBootstrap.Main +