Initial commit

This commit is contained in:
Ulf Gebhardt 2018-02-17 19:09:15 +01:00
parent 88587e0af3
commit 6e35a68c47
24 changed files with 3163 additions and 0 deletions

73
build.xml Normal file
View File

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- You may freely edit this file. See commented blocks below for -->
<!-- some examples of how to customize the build. -->
<!-- (If you delete it and reopen the project it will be recreated.) -->
<!-- By default, only the Clean and Build commands use this build script. -->
<!-- Commands such as Run, Debug, and Test only use this build script if -->
<!-- the Compile on Save feature is turned off for the project. -->
<!-- You can turn off the Compile on Save (or Deploy on Save) setting -->
<!-- in the project's Project Properties dialog box.-->
<project name="Kademlia" default="default" basedir=".">
<description>Builds, tests, and runs the project Kademlia.</description>
<import file="nbproject/build-impl.xml"/>
<!--
There exist several targets which are by default empty and which can be
used for execution of your tasks. These targets are usually executed
before and after some main targets. They are:
-pre-init: called before initialization of project properties
-post-init: called after initialization of project properties
-pre-compile: called before javac compilation
-post-compile: called after javac compilation
-pre-compile-single: called before javac compilation of single file
-post-compile-single: called after javac compilation of single file
-pre-compile-test: called before javac compilation of JUnit tests
-post-compile-test: called after javac compilation of JUnit tests
-pre-compile-test-single: called before javac compilation of single JUnit test
-post-compile-test-single: called after javac compilation of single JUunit test
-pre-jar: called before JAR building
-post-jar: called after JAR building
-post-clean: called after cleaning build products
(Targets beginning with '-' are not intended to be called on their own.)
Example of inserting an obfuscator after compilation could look like this:
<target name="-post-compile">
<obfuscate>
<fileset dir="${build.classes.dir}"/>
</obfuscate>
</target>
For list of available properties check the imported
nbproject/build-impl.xml file.
Another way to customize the build is by overriding existing main targets.
The targets of interest are:
-init-macrodef-javac: defines macro for javac compilation
-init-macrodef-junit: defines macro for junit execution
-init-macrodef-debug: defines macro for class debugging
-init-macrodef-java: defines macro for class execution
-do-jar: JAR building
run: execution of project
-javadoc-build: Javadoc generation
test-report: JUnit report generation
An example of overriding the target for project execution could look like this:
<target name="run" depends="Kademlia-impl.jar">
<exec dir="bin" executable="launcher.exe">
<arg file="${dist.jar}"/>
</exec>
</target>
Notice that the overridden target depends on the jar target and not only on
the compile target as the regular run target does. Again, for a list of available
properties which you can use, check the target you are overriding in the
nbproject/build-impl.xml file.
-->
</project>

1
build/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*/

7
logging.properties Normal file
View File

@ -0,0 +1,7 @@
handlers=java.util.logging.ConsoleHandler
.level=FINEST
java.util.logging.SimpleFormatter.format=[%1$tF %1$tr] %3$s %4$s: %5$s %n
java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter
java.util.logging.ConsoleHandler.level = FINEST

3
manifest.mf Normal file
View File

@ -0,0 +1,3 @@
Manifest-Version: 1.0
X-COMMENT: Main-Class will be added automatically by build

1
nbproject/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/private

1403
nbproject/build-impl.xml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,8 @@
build.xml.data.CRC32=4d688e4d
build.xml.script.CRC32=c3cd04bd
build.xml.stylesheet.CRC32=8064a381@1.78.1.48
# This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml.
# Do not edit this file. You may delete it but then the IDE will never regenerate such files for you.
nbproject/build-impl.xml.data.CRC32=4d688e4d
nbproject/build-impl.xml.script.CRC32=269076b4
nbproject/build-impl.xml.stylesheet.CRC32=830a3534@1.80.1.48

View File

@ -0,0 +1,84 @@
annotation.processing.enabled=true
annotation.processing.enabled.in.editor=false
annotation.processing.processors.list=
annotation.processing.run.all.processors=true
annotation.processing.source.output=${build.generated.sources.dir}/ap-source-output
application.title=Kademlia
application.vendor=rylon
build.classes.dir=${build.dir}/classes
build.classes.excludes=**/*.java,**/*.form
# This directory is removed when the project is cleaned:
build.dir=build
build.generated.dir=${build.dir}/generated
build.generated.sources.dir=${build.dir}/generated-sources
# Only compile against the classpath explicitly listed here:
build.sysclasspath=ignore
build.test.classes.dir=${build.dir}/test/classes
build.test.results.dir=${build.dir}/test/results
# Uncomment to specify the preferred debugger connection transport:
#debug.transport=dt_socket
debug.classpath=\
${run.classpath}
debug.test.classpath=\
${run.test.classpath}
# This directory is removed when the project is cleaned:
dist.dir=dist
dist.jar=${dist.dir}/Kademlia.jar
dist.javadoc.dir=${dist.dir}/javadoc
endorsed.classpath=
excludes=
file.reference.11-src=src
includes=**
jar.archive.disabled=${jnlp.enabled}
jar.compress=false
jar.index=${jnlp.enabled}
javac.classpath=
# Space-separated list of extra javac options
javac.compilerargs=
javac.deprecation=false
javac.external.vm=false
javac.processorpath=\
${javac.classpath}
javac.source=1.8
javac.target=1.8
javac.test.classpath=\
${javac.classpath}:\
${build.classes.dir}:\
${libs.junit.classpath}:\
${libs.junit_4.classpath}
javac.test.processorpath=\
${javac.test.classpath}
javadoc.additionalparam=
javadoc.author=false
javadoc.encoding=${source.encoding}
javadoc.noindex=false
javadoc.nonavbar=false
javadoc.notree=false
javadoc.private=false
javadoc.splitindex=true
javadoc.use=true
javadoc.version=false
javadoc.windowtitle=
jnlp.codebase.type=no.codebase
jnlp.descriptor=application
jnlp.enabled=false
jnlp.mixed.code=defaut
jnlp.offline-allowed=false
jnlp.signed=false
main.class=CLI
manifest.file=manifest.mf
meta.inf.dir=${src.dir}/META-INF
mkdist.disabled=false
platform.active=default_platform
run.classpath=\
${javac.classpath}:\
${build.classes.dir}
# Space-separated list of JVM arguments used when running the project
# (you may also define separate properties like run-sys-prop.name=value instead of -Dname=value
# or test-sys-prop.name=value to set system properties for unit tests):
run.jvmargs=
run.test.classpath=\
${javac.test.classpath}:\
${build.test.classes.dir}
source.encoding=UTF-8
src.dir=${file.reference.11-src}

13
nbproject/project.xml Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://www.netbeans.org/ns/project/1">
<type>org.netbeans.modules.java.j2seproject</type>
<configuration>
<data xmlns="http://www.netbeans.org/ns/j2se-project/3">
<name>Kademlia</name>
<source-roots>
<root id="src.dir"/>
</source-roots>
<test-roots/>
</data>
</configuration>
</project>

78
src/CLI.java Normal file
View File

@ -0,0 +1,78 @@
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.nio.MappedByteBuffer;
import java.util.logging.LogManager;
import node.FileIdentifier;
import node.Identifier;
import node.Node;
import node.NodeIdentifier;
public class CLI {
public static void main(String[] args) throws IOException {
System.setProperty("java.util.logging.config.file",
"logging.properties");
try {
LogManager.getLogManager().readConfiguration();
} catch (Exception e) {
e.printStackTrace();
}
Node node = new Node();
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String s;
while ((s = in.readLine()) != null && s.length() != 0) {
String[] splitted = s.split(" ");
String cmd = splitted[0];
switch (cmd) {
//status
case "status":
for (NodeIdentifier id : node.getNeighbors()) {
System.out.println(id);
}
break;
//lookup fileID
case "lookup":
String fileID = splitted[1];
// TODO not implemented
// Zum testen:
FileIdentifier fileIDToFind = new FileIdentifier(1, fileID.getBytes());
node.findValue(fileIDToFind);
break;
//request fileID
case "request":
String fileID3 = splitted[1];
FileIdentifier fileIDToFind2 = new FileIdentifier(1, fileID3.getBytes());
node.sendDataReq(fileIDToFind2);
break;
//leave
case "leave":
node.leave();
break;
//store fileID data
case "store":
String fileID2 = splitted[1];
String data = splitted[2];
// TODO not implemented
// Zum testen:
FileIdentifier fileIDToStore = new FileIdentifier(1,fileID2.getBytes());
node.store(fileIDToStore);
node.storeData(fileIDToStore,data);
break;
default:
System.out.println("Unknown command.");
break;
}
}
}
}

141
src/message/Ack.java Normal file
View File

@ -0,0 +1,141 @@
package message;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.util.logging.Level;
import java.util.logging.Logger;
import node.Identifier;
import node.NodeIdentifier;
import util.BufferUtil;
public class Ack {
private final static Logger LOGGER = Logger.getLogger(Ack.class.getName());
/**
* timeout in seconds
*/
private static final int TIMEOUT = 1000;
/**
* Maximum number of retries
*/
private static final int MAX_RETRIES = 3;
private Identifier rpcId;
private NodeIdentifier receiver;
private ByteBuffer buffer;
private int numRetries = 0;
private TimeoutThread timeout;
private Thread thread;
// The channel to re-send the message on
private DatagramChannel channel;
private MessageCallback callback;
public Ack(Identifier id, NodeIdentifier receiver, DatagramChannel channel,
ByteBuffer buffer, MessageCallback cb) {
this.rpcId = id;
this.receiver = receiver;
this.channel = channel;
this.buffer = BufferUtil.clone(buffer);
this.callback = cb;
startThread();
}
private void startThread() {
LOGGER.log(Level.FINEST, "Starting timeout thread for RPC " + rpcId);
timeout = new TimeoutThread();
thread = new Thread(timeout);
thread.start();
}
public Identifier getID() {
return rpcId;
}
public boolean check(NodeIdentifier fromID) {
return fromID.equals(receiver);
}
public ByteBuffer getBuf() {
return buffer;
}
public void setBuf(ByteBuffer buf) {
this.buffer = buf;
}
public void setReceived() {
// Stop thread
try {
if (thread != null) {
timeout.terminate();
thread.join();
}
} catch (InterruptedException e) {
}
}
private class TimeoutThread implements Runnable {
private volatile boolean notReceived = true;
// When do we stop expecting the ack
private long timeToStop = System.currentTimeMillis() + TIMEOUT;
@Override
public void run() {
while (notReceived && System.currentTimeMillis() < timeToStop) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// Timeout hit!
if (notReceived) {
if (numRetries < MAX_RETRIES) {
try {
LOGGER.log(
Level.FINE,
"Didn't receive RPC Ack {0} by now. Resending... ",
new Object[] { rpcId });
LOGGER.log(Level.INFO, receiver.getAddress().toString());
channel.send(buffer, receiver.getAddress());
} catch (IOException e) {
e.printStackTrace();
}
startThread();
numRetries++;
} else {
LOGGER.log(Level.INFO, "Absent RPC ack {0}.",
new Object[] { rpcId });
if (callback != null) {
callback.onTimeout();
}
}
} else {
// Message has been received in time
if (callback != null) {
callback.onReceive();
}
}
}
public void terminate() {
notReceived = false;
}
}
}

View File

@ -0,0 +1,22 @@
package message;
/**
* A callback to create asynchronous events that get triggered when a message
* (ack/answer) is received.
*
* @author jln
*
*/
public interface MessageCallback {
/**
* Called when the awaited message arrives.
*/
public void onReceive();
/**
* Called when the awaited message doesn't arrive (even after possible
* retries).
*/
public void onTimeout();
}

View File

@ -0,0 +1,19 @@
package message;
public class MessageType {
public final static byte FIND_NODE = 0;
public final static byte NODES = 1;
public final static byte PING = 11;
public final static byte PONG = 12;
public final static byte LEAVE = 2;
public final static byte FIND_VALUE = 4;
public final static byte STORE = 5;
public final static byte DATA = 6;
public final static byte DATA_REQ = 7;
public final static byte VALUE_NODES = 8;
public final static byte FOUND_VALUE = 9;
public final static byte ACK = 10;
}

View File

@ -0,0 +1,36 @@
package node;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class ChunkIdentifier extends Identifier {
private String chunkID;
private FileIdentifier fileID;
public ChunkIdentifier(int size, byte[] bytes, FileIdentifier fileID, String chunkID) {
super(size, bytes);
this.fileID = fileID;
//calculate SHA-256 Hash of chunckID
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(chunkID.getBytes());
this.chunkID = md.digest().toString();
} catch (NoSuchAlgorithmException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public String getChunkID() {
return this.chunkID;
}
public FileIdentifier getFileID(){
return this.fileID;
}
}

View File

@ -0,0 +1,28 @@
package node;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class FileIdentifier extends Identifier {
private String fileID;
public FileIdentifier(int size, byte[] fileID) {
super(size, fileID);
/*//calculate SHA-256 Hash of key
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(fileID.getBytes());
this.fileID = md.digest().toString();
} catch (NoSuchAlgorithmException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}*/
}
public String getKey() {
return this.fileID;
}
}

111
src/node/Identifier.java Normal file
View File

@ -0,0 +1,111 @@
package node;
import java.math.BigInteger;
import java.util.BitSet;
import java.util.Random;
/**
* A Kademlia identifier. Can be used for identifying files as well as nodes
* (but for nodes check {@see NodeIdentifier}).
*
* @author jln
*
*/
public class Identifier {
private static Random random = new Random(System.currentTimeMillis());
protected BitSet bits;
private int size;
public Identifier(int size, byte[] bytes) {
this.size = size;
this.bits = BitSet.valueOf(bytes);
}
private Identifier(int size, BitSet bits) {
this.size = size;
this.bits = bits;
}
/**
* Creates an ID exactly "in the middle" of the ID space. (If the ID space
* is 8 bit wide, this returns an ID valued 128).
*
* @param size
* the size of the id space
* @return an Identifier
*/
public static Identifier getStaticIdentifier(int size) {
BitSet middle = new BitSet(size);
middle.set(size - 1);
return new Identifier(size, middle);
}
/**
* Creates a random ID for the given id space size.
*
* @param size
* the size of the id space
* @return a random Identifier
*/
public static Identifier getRandomIdentifier(int size) {
BitSet bits = new BitSet(size);
for (int i = 0; i < size; i++) {
double threshold = random.nextGaussian();
if (threshold > 0) {
bits.set(i);
}
}
return new Identifier(size, bits);
}
public BigInteger distanceTo(Identifier otherID) {
BitSet distance = (BitSet) bits.clone();
distance.xor(otherID.bits);
return new BigInteger(1, distance.toByteArray());
}
/**
* Returns whether the bit at the given position is set or not. The MSB is
* at position 0.
*
* @param index
* the index to check
* @return true if the bit is set
*/
public boolean isBitSetAt(int index) {
BigInteger intValue = new BigInteger(1, bits.toByteArray());
int numOfTrimmedZeros = size - intValue.bitLength();
if (index < numOfTrimmedZeros) {
return false;
}
return bits.get(bits.length() - (index + numOfTrimmedZeros) - 1);
}
public byte[] getBytes() {
return bits.toByteArray();
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Identifier)) {
return false;
} else {
return bits.equals(((Identifier) o).bits);
}
}
@Override
public int hashCode() {
return toString().hashCode();
}
public String toString() {
return new BigInteger(1, bits.toByteArray()).toString();
}
}

476
src/node/Node.java Normal file
View File

@ -0,0 +1,476 @@
package node;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import message.Ack;
import message.MessageCallback;
import message.MessageType;
import routingtable.IRoutingTable;
import routingtable.RoutingTableImpl;
public class Node {
private final static Logger LOGGER = Logger.getLogger(Node.class.getName());
/**
* Size of ID space (has to be a multiple of 8)
*/
public static final int ID_BITS = 8;
/**
* The bucket size
*/
public static final int BUCKET_SIZE = 2;
/**
* The first node is always spawned on port 50000
*/
private static final int INITIAL_PORT = 50000;
private static final Identifier INITIAL_ID = Identifier
.getStaticIdentifier(ID_BITS);
private static final int BUFFER_SIZE = 512;
private static final int CHUNK_SIZE = 4;
/**
* The size of an IP address (in bytes)
*/
public static final int SIZE_IP_ADDRESS = 8;
private InetSocketAddress address;
private DatagramChannel channel;
public NodeIdentifier lastlookup = null;
private Map<Identifier, List<Ack>> rpcs = new HashMap<Identifier, List<Ack>>();
private Map<Identifier, Identifier> values = new HashMap<Identifier, Identifier>();
private Identifier searchID = null;
private Thread thread;
private UDPHandler udpListen;
private Identifier nodeID = Identifier.getRandomIdentifier(ID_BITS);
private IRoutingTable routingTable = new RoutingTableImpl(BUCKET_SIZE, this);
private Map<FileIdentifier, String> data = new HashMap<FileIdentifier, String>();;
public Node() {
System.setProperty("java.net.preferIPv4Stack", "true");
try {
channel = DatagramChannel.open();
try {
address = new InetSocketAddress("localhost", INITIAL_PORT);
channel.socket().bind(address);
this.nodeID = INITIAL_ID;
} catch (SocketException e) {
// The initial port is already bound -> let the system pick a
// port
channel.socket().bind(new InetSocketAddress("localhost", 0));
address = (InetSocketAddress) channel.getLocalAddress();
}
channel.configureBlocking(false);
udpListen = new UDPHandler(this);
thread = new Thread(udpListen);
thread.start();
LOGGER.log(Level.INFO, "{0}: Initialized node {1} on {2}",
new Object[] { this.nodeID, getName(), address.toString() });
if (address.getPort() != INITIAL_PORT) {
// The port of this node is not the "INITIAL_PORT" (so it's not
// the first node in the network). So we try to join the network
// via the first node.
NodeIdentifier viaNode = new NodeIdentifier(ID_BITS,
INITIAL_ID.getBytes(), new InetSocketAddress(
"127.0.0.1", INITIAL_PORT));
joinNetworkVia(viaNode);
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void joinNetworkVia(NodeIdentifier viaNode) {
LOGGER.log(Level.INFO, "Trying to join network via node {0}",
new Object[] { viaNode });
routingTable.insert(viaNode);
sendFindNode(viaNode, this.nodeID);
}
/**
* Creates and returns new ID (usually used as a RPC ID). This makes sure
* the ID is not yet used (in this node).
*
* @return an ID
*/
private Identifier createRPCID() {
Identifier rpcID = Identifier.getRandomIdentifier(ID_BITS);
while (rpcs.containsKey(rpcID)) {
rpcID = Identifier.getRandomIdentifier(ID_BITS);
}
return rpcID;
}
void sendFindNode(NodeIdentifier receiver, Identifier idToFind) {
boolean successful = send(receiver, MessageType.FIND_NODE,
idToFind.getBytes(), true, null);
if (successful) {
LOGGER.log(Level.INFO, "Sending [FIND_NODE {0}] to node {1}",
new Object[] { idToFind, receiver });
}
}
void sendFindValue(NodeIdentifier receiver, Identifier idToFind) {
// need to save the fileID because we need it for future searches
this.searchID = idToFind;
LOGGER.log(Level.INFO, "Sending [FIND_VALUE {0}] to node {1}",new Object[] { idToFind, receiver });
boolean successful = send(receiver, MessageType.FIND_VALUE,
idToFind.getBytes(), true, null);
if (successful) {
LOGGER.log(Level.INFO, "Sent [FIND_VALUE {0}] to node {1}",
new Object[] { idToFind, receiver });
}
}
void sendFoundValue(NodeIdentifier receiver, Identifier idToFind,
Identifier rpcID) {
boolean successful = send(receiver, MessageType.FOUND_VALUE, rpcID,
values.get(idToFind).getBytes(), false, null);
if (successful) {
LOGGER.log(Level.INFO, "Sending [FOUND_VALUE {0} -> {1}] to node {2}",
new Object[] { idToFind, values.get(idToFind), receiver });
}
}
/**
* Gets all nodes of this nodes routing table, that are close to a given
* node/fileID and sends that list to a specific node.
*
* @param receiver
* The node to receive the list of nodes
* @param idToFind
* The ID to find close nodes of
* @param rpcID
* An RPC ID (because this is always an answer to a FIND_NODE
* RPC)
* @param nodeType
* If true, we search a specific node, else a fileID
*/
void sendClosestNodesTo(NodeIdentifier receiver, Identifier idToFind,
Identifier rpcID, boolean nodeType) {
byte msgtype = 0;
if (nodeType) {
msgtype = MessageType.NODES;
} else {
msgtype = MessageType.VALUE_NODES;
}
Set<NodeIdentifier> closeNodes = routingTable
.getClosestNodesTo(idToFind);
int numNodes = closeNodes.size();
ByteBuffer nodes = ByteBuffer.allocate(numNodes * (ID_BITS / 8)
+ numNodes * SIZE_IP_ADDRESS);
for (NodeIdentifier idToSend : closeNodes) {
// Don't send the node to itself
if (!receiver.equals(idToSend)) {
nodes.put(idToSend.getTripleAsBytes());
}
}
boolean successful = send(receiver, msgtype, rpcID, nodes.array(),
false, null);
if (successful) {
LOGGER.log(
Level.INFO,
"Sending {0} nodes to to node {1} [FIND_NODE {2}] (rpcID={3})",
new Object[] { closeNodes.size(), receiver, idToFind, rpcID });
}
}
public void sendStore(NodeIdentifier receiver, Identifier fileID) {
boolean successful = send(receiver, MessageType.STORE,
fileID.getBytes(), true, null);
if (successful) {
LOGGER.log(Level.INFO, "Sending [STORE {0}] to node {1}",
new Object[] { fileID, receiver });
}
}
public void sendAck(NodeIdentifier receiver, Identifier rpcID) {
send(receiver, MessageType.ACK, rpcID, null, false, null);
}
public void sendDataReq(FileIdentifier fileID){
//TODO
if(lastlookup == null){
new Exception("lookup first!").printStackTrace();
return;}
//String id = "128";
//NodeIdentifier receiver = new NodeIdentifier(8, id.getBytes(), new InetSocketAddress("localhost", INITIAL_PORT));
send(lastlookup, MessageType.DATA_REQ, fileID.getBytes(), true, null);
}
public void sendData(NodeIdentifier receiver, Identifier fileID) {
String data = this.data.get(fileID);
if(data == null){
//TODO We dont have that data. -> DOES NOT WORK PROPERLY!
new Exception().printStackTrace();
return;
}
int CHUNK_COUNT = data.length()/CHUNK_SIZE;
for(int i = 0; i<CHUNK_COUNT; i++){
String chunk = fileID.toString() + "-" +
CHUNK_COUNT + "-" +
i + "-" +
data.substring(i*CHUNK_SIZE, (i+1)*CHUNK_SIZE);
send(receiver, MessageType.DATA, chunk.getBytes(), true, null);
}
}
public void sendPing(NodeIdentifier receiver, MessageCallback cb) {
boolean successful = send(receiver, MessageType.PING, null, true, cb);
if (successful) {
LOGGER.log(Level.INFO, "Sending [PING] to node {0}",
new Object[] { receiver });
}
}
void sendPong(NodeIdentifier receiver, Identifier rpcID) {
boolean successful = send(receiver, MessageType.PONG, rpcID, null,
false, null);
if (successful) {
LOGGER.log(Level.INFO, "Sending [PONG] to {0} (rpcID={1})",
new Object[] { receiver, rpcID });
}
}
/**
* Send a message to a given ID (with a given RPC ID). You usually want to
* use this method when you know the RPC ID beforehand (e.g. if this is an
* ack or answer to a prior message).
*
* @param to
* the ID to send to
* @param messageType
* the message type
* @param data
* the data to send
* @param reliable
* flag, whether this has to be acked or not
* @param cb
* A callback that is executed when this message gets acked (or
* answered). This obviously is only of interest when the
* reliable flag is true
* @return true if the message was sent successfully
*/
private boolean send(NodeIdentifier to, byte messageType, byte[] data,
boolean reliable, MessageCallback cb) {
return send(to, messageType, createRPCID(), data, reliable, cb);
}
/**
* Send a message to a given ID (with a given RPC ID). You usually want to
* use this method when you know the RPC ID beforehand (e.g. if this is an
* ack or answer to a prior message).
*
* @param to
* the ID to send to
* @param messageType
* the message type
* @param rpcID
* the RPC ID of this message (if you don't know this use
* {@link #send(NodeIdentifier, byte, byte[], boolean, MessageCallback)}
* and a new random ID will be created)
* @param data
* the data to send
* @param reliable
* flag, whether this has to be acked or not
* @param cb
* A callback that is executed when this message gets acked (or
* answered). This obviously is only of interest when the
* reliable flag is true
* @return true if the message was sent successfully
*/
private boolean send(NodeIdentifier to, byte messageType, Identifier rpcID,
byte[] data, boolean reliable, MessageCallback cb) {
boolean successful = true;
ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
buffer.put(messageType);
buffer.put(this.nodeID.getBytes());
buffer.put(rpcID.getBytes());
if (data != null) {
buffer.put(data);
}
LOGGER.log(Level.INFO, "Sending Data to:{0} with MT:"+messageType+" data:"+buffer.toString(), to.getAddress().toString());
buffer.flip();
try {
channel.send(buffer, to.getAddress());
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Failed to write to channel. To:"+to.getAddress().toString()+" buffer: "+buffer.toString(), e);
successful = false;
} finally {
// Even if an exception occurred this should be reliable
if (reliable) {
Ack newAck = new Ack(rpcID, to, channel, buffer, cb);
if (rpcs.containsKey(rpcID)) {
rpcs.get(rpcID).add(newAck);
} else {
rpcs.put(rpcID, new ArrayList<Ack>());
rpcs.get(rpcID).add(newAck);
}
}
}
return successful;
}
public String getName() {
return nodeID.toString();
}
public boolean hasAcks() {
return !rpcs.isEmpty();
}
public DatagramChannel getChannel() {
return channel;
}
public void updateBuckets(NodeIdentifier id) {
routingTable.insert(id);
}
public Identifier getID() {
return nodeID;
}
public Set<NodeIdentifier> getNeighbors() {
return routingTable.getEntries();
}
public void storePair(Identifier key, Identifier nodeid) {
values.put(key, nodeid);
}
public void store(Identifier key) {
storePair(key,this.nodeID);
Set<NodeIdentifier> nodes = routingTable.getClosestNodesTo(key);
for (NodeIdentifier node : nodes) {
sendStore(node, key);
}
}
public void findValue(Identifier key) {
Set<NodeIdentifier> nodes = routingTable.getClosestNodesTo(key);
for (NodeIdentifier node : nodes) {
sendFindValue(node, key);
}
}
public boolean hasKey(Identifier key) {
return values.containsKey(key);
}
public Identifier getSearchID() {
return this.searchID;
}
public boolean receivedRPC(NodeIdentifier fromID, Identifier rpcID) {
List<Ack> rpcsFromID = rpcs.get(rpcID);
boolean removedAck = false;
// wohl unschön, hier auf != null zu prüfen, da Fehler wo anders ist.
if (rpcsFromID != null) {
for (Ack ack : rpcsFromID) {
if (ack.check(fromID)) {
ack.setReceived();
rpcsFromID.remove(ack);
removedAck = true;
LOGGER.log(Level.FINEST, "Received RPC ack " + rpcID);
break;
}
}
}
if (!removedAck) {
LOGGER.log(Level.WARNING,
"Received RPC ack {0}, but didn't expect that",
new Object[] { rpcID });
}
return removedAck;
}
public void leave() {
for (NodeIdentifier n : getNeighbors()) {
sendLeave(n);
}
System.exit(0);
}
private boolean sendLeave(NodeIdentifier n) {
return send(n, MessageType.LEAVE, null, false, null);
}
public void storeData(FileIdentifier id, String data) {
this.data.put(id, data);
LOGGER.log(Level.INFO, "Stored Data [{0}] as [{1}])",
new Object[] { data, id});
}
public void sendFile(NodeIdentifier nodeID, File file) {
}
}

View File

@ -0,0 +1,35 @@
package node;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import util.BufferUtil;
/**
* Same as a {@link Identifier}, but this also stores an IP address.
*
* @author jln
*
*/
public class NodeIdentifier extends Identifier {
private InetSocketAddress address;
public NodeIdentifier(int size, byte[] bytes, InetSocketAddress address) {
super(size, bytes);
this.address = address;
}
public byte[] getTripleAsBytes() {
ByteBuffer result = ByteBuffer.allocate(Node.SIZE_IP_ADDRESS
+ (Node.ID_BITS / 8));
result.put(BufferUtil.addrToBytes(address));
result.put(bits.toByteArray());
return result.array();
}
public InetSocketAddress getAddress() {
return address;
}
}

353
src/node/UDPHandler.java Normal file
View File

@ -0,0 +1,353 @@
package node;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import message.MessageType;
public class UDPHandler implements Runnable {
private final static Logger LOGGER = Logger.getLogger(UDPHandler.class
.getName());
public static final int BUF_SIZE = 512;
private volatile boolean running = true;
private ByteBuffer buffer = ByteBuffer.allocate(BUF_SIZE);
private Node node;
HashMap<FileIdentifier, HashMap<Integer,String> > chunklist = new HashMap<FileIdentifier, HashMap<Integer, String> >();
public UDPHandler(Node node) {
this.node = node;
}
/**
* Takes the buffer of this UDPHandler as is and tries to read an IP address
* (4 bytes and 1 int) from it. If there is no/incomplete or wrong data,
* this will fail.
*
* @return the address that has been read
*/
private InetSocketAddress getIPFromBuffer() {
StringBuilder theAddr = new StringBuilder();
// Read 4 Bytes and 1 Integer = 1 IP address
for (int i = 0; i < 4; i++) {
theAddr.append(buffer.get());
if (i < 3) {
theAddr.append(".");
}
}
int port = buffer.getInt();
return new InetSocketAddress(theAddr.toString(), port);
}
private Identifier getIDFromBuffer() {
int numBytes = Node.ID_BITS / 8;
byte[] result = new byte[numBytes];
for (int i = 0; i < numBytes; i++) {
result[i] = buffer.get();
}
return new Identifier(Node.ID_BITS, result);
}
/**
* Reads a triple <IP address, port, id> from the channel and returns a
* {@link node.NodeIdentifier}.
*
* @return the read node ID
*/
private NodeIdentifier getNodeTripleFromBuffer() {
InetSocketAddress address = getIPFromBuffer();
int numBytes = Node.ID_BITS / 8;
byte[] result = new byte[numBytes];
for (int i = 0; i < numBytes; i++) {
result[i] = buffer.get();
}
LOGGER.log(Level.INFO,"Read Buffer id: "+Node.ID_BITS+" result: "+result+" addr: "+address);
return new NodeIdentifier(Node.ID_BITS, result, address);
}
public void run() {
InetSocketAddress from = null;
// Run until it gets killed, and all my Acks have been answered
while (running || node.hasAcks()) {
try {
// Flag that indicates whether the routing table should be
// updated with the node we just received a message from. This
// needs to be done, because some messages trigger a direct
// answer. For example we send a PING to a node. That node
// answers with a PONG. Because we received a message from that
// node we will update our routing table and see that we already
// know this node. So we will PING that node...
boolean updateRT = true;
// The address of the node that sent this message
from = (InetSocketAddress) node.getChannel().receive(buffer);
// channel.receive() is non-blocking. So we need to check if
// something actually has been written to the buffer
if (buffer.remaining() != BUF_SIZE) {
buffer.flip();
byte messageType = buffer.get();
NodeIdentifier fromID = new NodeIdentifier(Node.ID_BITS,
getIDFromBuffer().getBytes(), from);
Identifier rpcID = getIDFromBuffer();
switch (messageType) {
case MessageType.FIND_NODE:
receiveFindNode(fromID, rpcID);
break;
case MessageType.NODES:
receiveNodes(fromID, rpcID);
break;
case MessageType.PING:
updateRT = false;
receivePing(fromID, rpcID);
break;
case MessageType.PONG:
updateRT = false;
receivePong(fromID, rpcID);
break;
case MessageType.LEAVE:
// We don't have to do anything here because, after this
// switch block we call node.updateBuckets(...) which
// will try to ping the node we received this leave
// message from. That node will not answered because it
// directly shut down after sending the leave message.
// So the node will be removed from this routing table.
LOGGER.log(Level.INFO, "Received leave from {0}",
new Object[] { from.toString() });
break;
case MessageType.FIND_VALUE:
receiveFindValue(fromID, rpcID);
break;
case MessageType.VALUE_NODES:
receiveValueNodes(fromID, rpcID);
break;
case MessageType.FOUND_VALUE:
receiveFoundValue(fromID, rpcID);
break;
case MessageType.STORE:
receiveStore(fromID, rpcID);
break;
case MessageType.DATA:
receiveData(fromID, rpcID);
LOGGER.log(Level.INFO, "Received DATA from {0}",
new Object[] { from.toString() });
break;
case MessageType.DATA_REQ:
receiveDataReq(fromID,rpcID);
LOGGER.log(Level.INFO, "Received DATA_REQ from {0}",
new Object[] { from.toString() });
break;
case MessageType.ACK:
receiveAck(fromID, rpcID);
break;
default:
LOGGER.log(Level.INFO,
"Received unknown command from {0}: [{1}]{2}",
new Object[] { from.toString(), messageType,
new String(buffer.array()) });
}
if (updateRT) {
node.updateBuckets(new NodeIdentifier(Node.ID_BITS,
fromID.getBytes(), from));
}
} else {
// If nothing has been read/received wait and read/receive
// again
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
buffer.clear();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void receiveAck(NodeIdentifier fromID, Identifier rpcID) {
// This should be the either answer to a prior STORE or FOUND_VALUE ->
// mark this RPC ID as received
node.receivedRPC(fromID, rpcID);
}
private void receiveFoundValue(NodeIdentifier fromID, Identifier rpcID) {
Identifier idToFind = getIDFromBuffer();
node.lastlookup = fromID;
// TODO Auto-generated method stub
// Node kontaktieren, damit Datei gesendet werden kann.
// This should be the answer to a prior FIND_VALUE -> mark this RPC ID
// as received
node.receivedRPC(fromID, rpcID);
LOGGER.log(Level.INFO, "Received [FOUND VALUE on Node {0}] from Node {1}",
new Object[] { idToFind, fromID });
}
private void receiveValueNodes(NodeIdentifier fromID, Identifier rpcID) {
int numReceived = 0;
// This is just for the log message
StringBuilder nodes = new StringBuilder();
while (buffer.hasRemaining()) {
NodeIdentifier newID = getNodeTripleFromBuffer();
node.sendFindValue(newID, node.getSearchID());
nodes.append(newID).append(", ");
numReceived++;
}
// This should be the answer to a prior FIND_VALUE -> mark this RPC ID
// as
// received
node.receivedRPC(fromID, rpcID);
LOGGER.log(Level.INFO,
"Received {0} [VALUE NODES] [{1}] from Node {2})",
new Object[] { numReceived, nodes.toString(), fromID });
}
private void receiveData(NodeIdentifier fromID, Identifier rpcID) {
String data = new String(buffer.array());
String parts[] = data.split("-");
String fileID = parts[0];
int chunkCount = Integer.parseInt(parts[1]);
int chunkID = Integer.parseInt(parts[2]);
String chunkContent = parts[3];
LOGGER.log(Level.INFO,"recieved Chunk file: "+fileID+" count: "+chunkCount+" id: "+chunkID);
FileIdentifier fid = new FileIdentifier(1,fileID.getBytes());
if(chunklist.get(fid) == null){
chunklist.put(fid, new HashMap<Integer,String>());
}
if(chunklist.get(fid).get(chunkID) == null){
chunklist.get(fid).put(chunkID, chunkContent);
}
if(chunklist.get(fid).size() >= chunkCount){
LOGGER.log(Level.INFO,"FILE complete file: "+fileID+" count: "+chunkCount+" id: "+chunkID);
String file = "";
for(int i=0; i<chunklist.get(fid).size();i++){
file += chunklist.get(fid).get(i);
}
node.store(fid);
node.storeData(fid, file);
chunklist.remove(fid);
LOGGER.log(Level.INFO,"FILE DATA: "+file);
}
node.sendAck(fromID, rpcID);
LOGGER.log(Level.INFO, "Received [DATA] [{0}] from Node {1})",
new Object[] { data.toString(), fromID });
}
private void receiveDataReq(NodeIdentifier fromID, Identifier rpcID) {
Identifier fid = getIDFromBuffer();
//FileIdentifier fid = new FileIdentifier(1, buffer.array());
node.sendData(fromID, fid);
node.sendAck(fromID, rpcID);
}
private void receivePong(NodeIdentifier fromID, Identifier rpcID) {
LOGGER.log(Level.INFO, "Received [PONG] from {0}",
new Object[] { fromID });
// This should be the answer to a prior PING -> mark this RPC ID as
// received
node.receivedRPC(fromID, rpcID);
}
private void receivePing(NodeIdentifier fromID, Identifier rpcID) {
LOGGER.log(Level.INFO, "Received [PING] from {0}",
new Object[] { fromID });
node.sendPong(fromID, rpcID);
}
private void receiveNodes(NodeIdentifier fromID, Identifier rpcID) {
int numReceived = 0;
// This is just for the log message
StringBuilder nodes = new StringBuilder();
while (buffer.hasRemaining()) {
NodeIdentifier newID = getNodeTripleFromBuffer();
node.updateBuckets(newID);
nodes.append(newID).append(", ");
numReceived++;
}
// This should be the answer to a prior FIND_NODE -> mark this RPC ID as
// received
node.receivedRPC(fromID, rpcID);
LOGGER.log(Level.INFO, "Received {0} [NODES] [{1}] from Node {2})",
new Object[] { numReceived, nodes.toString(), fromID });
}
private void receiveFindNode(NodeIdentifier fromID, Identifier rpc_id) {
Identifier idToFind = getIDFromBuffer();
LOGGER.log(Level.INFO, "Received [FIND_NODE {0}] from Node {1}",
new Object[] { idToFind, fromID });
node.sendClosestNodesTo(fromID, idToFind, rpc_id, true);
}
private void receiveStore(NodeIdentifier fromID, Identifier rpcID) {
Identifier fileID = getIDFromBuffer();
LOGGER.log(Level.INFO, "Received [STORE {0}] from Node {1}",
new Object[] { fileID, fromID });
node.storePair(fileID, fromID);
node.sendAck(fromID, rpcID);
}
private void receiveFindValue(NodeIdentifier fromID, Identifier rpcID) {
Identifier fileID = getIDFromBuffer();
LOGGER.log(Level.INFO, "Received [FIND VALUE {0}] from Node {1}",
new Object[] { fileID, fromID });
if (node.hasKey(fileID)) {
node.sendFoundValue(fromID, fileID, rpcID);
} else {
node.sendClosestNodesTo(fromID, fileID, rpcID, false);
}
}
public void terminate() {
running = false;
}
}

View File

@ -0,0 +1,145 @@
package routingtable;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import message.MessageCallback;
import node.Node;
import node.NodeIdentifier;
public class Bucket {
private final static Logger LOGGER = Logger.getLogger(Bucket.class
.getName());
private Bucket left;
private Bucket right;
private List<NodeIdentifier> entries;
private int bucketSize;
private int level;
private Node node;
public Bucket(int bucketSize, int level, Node node) {
this.bucketSize = bucketSize;
this.level = level;
this.node = node;
entries = new ArrayList<NodeIdentifier>();
}
/**
* Returns the nodes of this very bucket.
*
* @return
*/
public List<NodeIdentifier> getNodes() {
return entries;
}
public boolean contains(NodeIdentifier id) {
if (!isLeaf()) {
return left.contains(id) || right.contains(id);
}
return entries.contains(id);
}
/**
* Tries to update the given node.
*
* @param id
* @return true if the node is still available, else false
*/
public void update(final NodeIdentifier id) {
if (!isLeaf()) {
if (id.isBitSetAt(level)) {
left.update(id);
} else {
right.update(id);
}
} else {
node.sendPing(id, new MessageCallback() {
@Override
public void onReceive() {
LOGGER.log(Level.INFO,
"Node answered in time, moving to top of list.");
entries.remove(id);
entries.add(0, id);
}
@Override
public void onTimeout() {
LOGGER.log(Level.INFO, "Node didnt answer in time.");
// TODO: this should be propagated to the "upper" Routing
// Table, not just to this specific bucket
entries.remove(id);
}
});
}
}
public void insert(NodeIdentifier newId) {
insert(newId, "");
}
public void insert(NodeIdentifier newId, String path) {
if (isLeaf()) {
if (entries.size() < bucketSize) {
LOGGER.log(Level.INFO,
"Added node {0} to RT [{1}] on level {2}",
new Object[] { newId, path, level });
entries.add(newId);
} else {
LOGGER.log(Level.INFO, "Split on level " + level
+ " while adding " + newId);
LOGGER.log(Level.INFO,
"Distributing present nodes to lower buckets");
Bucket newLeft = new Bucket(bucketSize, level + 1, node);
Bucket newRight = new Bucket(bucketSize, level + 1, node);
// Add the new entry and in the following loop distribute all
// existing entries to left/right
entries.add(newId);
for (NodeIdentifier id : entries) {
if (id.isBitSetAt(level)) {
newLeft.insert(id, path + "1");
} else {
newRight.insert(id, path + "0");
}
}
this.entries = null;
this.left = newLeft;
this.right = newRight;
}
} else {
if (newId.isBitSetAt(level)) {
left.insert(newId, path + "1");
} else {
right.insert(newId, path + "0");
}
}
}
private boolean isLeaf() {
return left == null && right == null;
}
public void remove(NodeIdentifier node) {
if (isLeaf()) {
entries.remove(node);
} else {
if (node.isBitSetAt(level)) {
left.remove(node);
} else {
right.remove(node);
}
}
}
}

View File

@ -0,0 +1,19 @@
package routingtable;
import java.util.Set;
import node.Identifier;
import node.NodeIdentifier;
public interface IRoutingTable {
public void insert(NodeIdentifier id);
public Set<NodeIdentifier> getClosestNodesTo(Identifier id);
public boolean contains(NodeIdentifier node);
public void remove(NodeIdentifier node);
public Set<NodeIdentifier> getEntries();
}

View File

@ -0,0 +1,79 @@
package routingtable;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import node.Identifier;
import node.Node;
import node.NodeIdentifier;
public class RoutingTableImpl implements IRoutingTable {
private Set<NodeIdentifier> entries = new HashSet<NodeIdentifier>();
private Bucket root;
private int bucketSize;
public RoutingTableImpl(int bucketSize, Node node) {
this.bucketSize = bucketSize;
this.root = new Bucket(bucketSize, 0, node);
}
@Override
public void insert(NodeIdentifier id) {
if (root.contains(id)) {
root.update(id);
} else {
entries.add(id);
root.insert(id);
}
}
@Override
public Set<NodeIdentifier> getClosestNodesTo(final Identifier id) {
Set<NodeIdentifier> result = new HashSet<NodeIdentifier>();
if (entries.size() <= bucketSize) {
result.addAll(entries);
} else {
List<NodeIdentifier> temp = new ArrayList<NodeIdentifier>(entries);
Collections.sort(temp, new Comparator<NodeIdentifier>() {
@Override
public int compare(NodeIdentifier o1, NodeIdentifier o2) {
BigInteger dist1 = id.distanceTo(o1);
BigInteger dist2 = id.distanceTo(o2);
return dist1.compareTo(dist2);
}
});
for (int i = 0; i < bucketSize; i++) {
result.add(temp.get(i));
}
result = new HashSet<NodeIdentifier>(temp.subList(0,
Node.BUCKET_SIZE));
}
return result;
}
@Override
public boolean contains(NodeIdentifier node) {
return root.contains(node);
}
@Override
public void remove(NodeIdentifier node) {
}
@Override
public Set<NodeIdentifier> getEntries() {
return entries;
}
}

28
src/util/BufferUtil.java Normal file
View File

@ -0,0 +1,28 @@
package util;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
public class BufferUtil {
public static ByteBuffer clone(ByteBuffer original) {
ByteBuffer clone = ByteBuffer.allocate(original.capacity());
int oldPosition = original.position();
original.rewind();// copy from the beginning
clone.put(original);
// original.rewind();
original.position(oldPosition);
clone.flip();
return clone;
}
public static byte[] addrToBytes(InetSocketAddress addr) {
ByteBuffer buffer = ByteBuffer.allocate(8);
for (String part : addr.getHostString().split("\\.")) {
buffer.put(Byte.valueOf(part));
}
buffer.putInt(addr.getPort());
return buffer.array();
}
}

BIN
u5-1.pdf Normal file

Binary file not shown.