package kademlia.core; 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.ArrayList; import java.util.List; import java.util.NoSuchElementException; import java.util.Timer; import java.util.TimerTask; import kademlia.dht.DHT; import kademlia.dht.KadContent; import kademlia.exceptions.RoutingException; import kademlia.message.MessageFactory; import kademlia.node.Node; import kademlia.node.NodeId; import kademlia.operation.ConnectOperation; import kademlia.operation.ContentLookupOperation; import kademlia.operation.Operation; import kademlia.operation.KadRefreshOperation; import kademlia.operation.StoreOperation; import kademlia.serializer.JsonSerializer; /** * The main Kademlia network management class * * @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 * @todo Handle compressing data * @todo Allow optional storing of content locally using the put method * @todo Instead of using a StoreContentMessage to send a store RPC and a ContentMessage to receive a FIND rpc, make them 1 message with different operation type * @todo If we're trying to send a message to this node, just cancel the sending process and handle the message right here * @todo Keep this node in it's own routing table - it helps for ContentRefresh operation - easy to check whether this node is one of the k-nodes for a content * @todo Move DHT.getContentStorageFolderName to the Configuration class * */ public class Kademlia { /* Kademlia Attributes */ private final String ownerId; /* Objects to be used */ private final Node localNode; private final KadServer server; private final DHT dht; private final Timer timer; private final int udpPort; /* Factories */ private final MessageFactory messageFactory; /** * 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 * * @throws IOException If an error occurred while reading id or local map * from disk or a network error occurred while * attempting to connect to the network * */ public Kademlia(String ownerId, Node localNode, int udpPort, DHT dht) throws IOException { this.ownerId = ownerId; this.udpPort = udpPort; this.localNode = localNode; this.dht = dht; this.messageFactory = new MessageFactory(localNode, this.dht); this.server = new KadServer(udpPort, this.messageFactory, this.localNode); this.timer = new Timer(true); /* Schedule Recurring RestoreOperation */ timer.schedule( new TimerTask() { @Override public void run() { try { /* Runs a DHT RefreshOperation */ Kademlia.this.refresh(); } catch (IOException e) { System.err.println("Refresh Operation Failed; Message: " + e.getMessage()); } } }, // Delay // Interval Configuration.RESTORE_INTERVAL, Configuration.RESTORE_INTERVAL ); } public Kademlia(String ownerId, NodeId defaultId, int udpPort) throws IOException { this(ownerId, new Node(defaultId, InetAddress.getLocalHost(), udpPort), udpPort, new DHT()); } /** * Load Stored state * * @param ownerId The ID of the owner for the stored state * * @return A Kademlia instance loaded from a stored state in a file * * @throws java.io.FileNotFoundException * * @todo Boot up this Kademlia instance from a saved file state */ public static void loadFromFile(String ownerId) throws FileNotFoundException, IOException, ClassNotFoundException { /* Setup the file in which we store the state */ DataInputStream din = new DataInputStream(new FileInputStream(getStateStorageFolderName() + File.separator + ownerId + ".kns")); /* Read the UDP Port that this app is running on */ Integer rPort = new JsonSerializer().read(din); /* Read the node state */ // Node rN = new JsonSerializer().read(din); /* Read the DHT */ DHT rDht = new JsonSerializer().read(din); //return new Kademlia(ownerId, rN, rPort, rDht); } /** * @return Node The local node for this system */ public Node getNode() { return this.localNode; } /** * @return The KadServer used to send/receive messages */ public KadServer getServer() { return this.server; } /** * 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 final void connect(Node n) throws IOException, RoutingException { Operation op = new ConnectOperation(this.server, this.localNode, n); op.execute(); } /** * 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 java.io.IOException * */ public int put(KadContent content) throws IOException { StoreOperation sop = new StoreOperation(server, localNode, content); sop.execute(); /* Return how many nodes the content was stored on */ return sop.numNodesStoredAt(); } /** * Get some content stored on the DHT * The content returned is a JSON String in byte format; this string is parsed into a class * * @param param The parameters used to search for the content * @param numResultsReq How many results are required from different nodes * * @return DHTContent The content * * @throws java.io.IOException */ public List get(GetParameter param, int numResultsReq) throws NoSuchElementException, IOException { List contentFound; if (this.dht.contains(param)) { /* If the content exist in our own DHT, then return it. */ System.out.println("Found content locally"); contentFound = new ArrayList<>(); contentFound.add(this.dht.get(param)); } else { /* Seems like it doesn't exist in our DHT, get it from other Nodes */ System.out.println("Looking for content on foreign nodes"); ContentLookupOperation clo = new ContentLookupOperation(server, localNode, param, numResultsReq); clo.execute(); contentFound = clo.getContentFound(); } return contentFound; } /** * Allow the user of the System to call refresh even out of the normal Kad refresh timing * * @throws java.io.IOException */ public void refresh() throws IOException { new KadRefreshOperation(this.server, this.localNode, this.dht).execute(); } /** * @return String The ID of the owner of this local network */ public String getOwnerId() { return this.ownerId; } /** * Here we handle properly shutting down the Kademlia instance * * @throws java.io.FileNotFoundException */ public void shutdown() throws FileNotFoundException, IOException, ClassNotFoundException { /* Shut down the server */ this.server.shutdown(); /* Save this Kademlia instance's state if required */ if (Configuration.SAVE_STATE_ON_SHUTDOWN) { /* Save the system state */ this.saveKadState(); } /* Now we store the content locally in a file */ } /** * Saves the node state to a text file * * @throws java.io.FileNotFoundException */ private void saveKadState() throws FileNotFoundException, IOException, ClassNotFoundException { /* Setup the file in which we store the state */ DataOutputStream dout; dout = new DataOutputStream(new FileOutputStream(getStateStorageFolderName() + File.separator + this.ownerId + ".kns")); System.out.println("Saving state"); /* Save the UDP Port that this app is running on */ new JsonSerializer().write(this.udpPort, dout); /* Save the node state */ dout = new DataOutputStream(new FileOutputStream(getStateStorageFolderName() + File.separator + this.ownerId + ".kns")); new JsonSerializer().write(this.localNode, dout); /* Save the DHT */ // dout = new DataOutputStream(new FileOutputStream(getStateStorageFolderName() + File.separator + this.ownerId + ".kns")); //new JsonSerializer().write(this.dht, dout); // System.out.println(dht.getStorageEntries()); // // DataInputStream din = new DataInputStream(new FileInputStream(getStateStorageFolderName() + File.separator + ownerId + ".kns")); // DHT dddht = new JsonSerializer().read(din); // System.out.println(); // System.out.println(); // System.out.println(); // System.out.println(); // System.out.println(dddht); System.out.println("FInished saving state"); } /** * 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 storagePath = System.getProperty("user.home") + File.separator + Configuration.LOCAL_FOLDER; File mainStorageFolder = new File(storagePath); /* Create the main storage folder if it doesn't exist */ if (!mainStorageFolder.isDirectory()) { mainStorageFolder.mkdir(); } File contentStorageFolder = new File(mainStorageFolder + File.separator + "nodes"); /* Create the content folder if it doesn't exist */ if (!contentStorageFolder.isDirectory()) { contentStorageFolder.mkdir(); } return mainStorageFolder + File.separator + "nodes"; } /** * 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.localNode.getRoutingTable()); sb.append("\n"); sb.append("\n\n\n"); return sb.toString(); } }