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