package de.tum.in.www1.jReto; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.Executor; import de.tum.in.www1.jReto.connectivity.PacketConnection; import de.tum.in.www1.jReto.connectivity.ReliablitiyManager; import de.tum.in.www1.jReto.connectivity.packet.ManagedConnectionHandshake; import de.tum.in.www1.jReto.module.api.Module; import de.tum.in.www1.jReto.routing.DefaultRouter; import de.tum.in.www1.jReto.routing.Node; import de.tum.in.www1.jReto.routing.Router; import de.tum.in.www1.jReto.routing.SinglePacketHelper; /** * @author jasamer * * A LocalPeer advertises the local peer in the network and browses for other peers. * * It requires one or more Modules to accomplish this. Two Modules that come with Reto are the WlanModule and the RemoteP2P module. * * The LocalPeer can also be used to establish multicast connections to multiple other peers. */ public class LocalPeer { /** Used to notify about discovered peers. */ public static interface PeerDiscoveryHandler { void onPeerDiscovered(RemotePeer peer); } /** Used to notify about removed peers. */ public static interface PeerRemovalHandler { void onPeerRemoved(RemotePeer peer); } /** Used to notify about incoming connections. */ public static interface IncomingConnectionHandler { void onConnection(RemotePeer peer, Connection connection); } private PeerDiscoveryHandler peerDiscoveryHandler; private PeerRemovalHandler peerRemovalHandler; private IncomingConnectionHandler incomingConnectionHandler; /** The Executor used to execute all networking operations and callbacks */ private final Executor executor; /** All know peers by their Node counterpart (provided by the Router). */ private final Map<Node, de.tum.in.www1.jReto.RemotePeer> knownPeers; /** The Router instance used by the LocalPeer */ private DefaultRouter router; /** This peer's unique identifier. If not specified in the constructor, it has a random value. */ private UUID localPeerIdentifier; /** All connections that were established by this local peer. */ private Map<UUID, PacketConnection> establishedConnections = new HashMap<>(); /** All connections that were established to this local peer. */ private Map<UUID, PacketConnection> incomingConnections = new HashMap<>(); /** Whether browsing and advertisement was started. */ private boolean isStarted = false; private ReliablitiyManager.PacketConnectionManager packetConnectionManager = new ReliablitiyManager.PacketConnectionManager() { @Override public void notifyConnectionClose(PacketConnection connection) { LocalPeer.this.removeConnection(connection); } @Override public void establishUnderlyingConnection(PacketConnection connection) { LocalPeer.this.reconnect(connection); } }; /** * Constructs a new LocalPeer object. A random identifier will be used for the LocalPeer. * Note that a LocalPeer is not functional without modules. You can add modules later with the addModule method. * * @param executor The executor used to run all networking code with. The executor can be used to specifiy the thread that should be used. */ public LocalPeer(Executor executor) { this(UUID.randomUUID(), new ArrayList<Module>(), executor); } /** * Constructs a new LocalPeer object. A random identifier will be used for the LocalPeer. * * @param modules A collection of modules used for the underlying networking functionality. For example: @see WlanModule, @see RemoteP2PModule. * @param executor The executor used to run all networking code with. The executor can be used to specifiy the thread that should be used. */ public LocalPeer(Collection<Module> modules, Executor executor) { this(UUID.randomUUID(), modules, executor); } /** * Constructs a new LocalPeer object. * * @param localPeerIdentifier The identifier used for the peer * @param modules A collection of modules used for the underlying networking functionality. For example: @see WlanModule, @see RemoteP2PModule. * @param executor The executor used to run all networking code with. The executor can be used to specifiy the thread that should be used. */ public LocalPeer(UUID localPeerIdentifier, Collection<Module> modules, Executor executor) { this(localPeerIdentifier, modules, executor, new Router.BroadcastDelaySettings(5, 0.5)); } /** * Constructs a new LocalPeer object. * * @param localPeerIdentifier The identifier used for the peer * @param modules A collection of modules used for the underlying networking functionality. For example: @see WlanModule, @see RemoteP2PModule. * @param executor The executor used to run all networking code with. The executor can be used to specifiy the thread that should be used. * @param broadcastDelaySettings Settings for the timing of routing information broadcast. */ public LocalPeer(UUID localPeerIdentifier, Collection<Module> modules, Executor executor, Router.BroadcastDelaySettings broadcastDelaySettings) { this.localPeerIdentifier = localPeerIdentifier; this.executor = executor; this.knownPeers = new HashMap<Node, de.tum.in.www1.jReto.RemotePeer>(); this.router = new DefaultRouter(localPeerIdentifier, executor, modules, broadcastDelaySettings); this.router.setHandler(new Router.RouterHandler() { @Override public void onRouteImproved(Router router, Node node) { LocalPeer.this.reconnectConnections(node); } @Override public void onNodeLost(Router router, Node node) { LocalPeer.this.removeNode(node); } @Override public void onNodeFound(Router router, Node node) { LocalPeer.this.addNode(node); } @Override public void onConnection(Router router, Node node, de.tum.in.www1.jReto.module.api.Connection connection) { LocalPeer.this.handleConnection(node, connection); } }); } /** This peer's unique identifier. If not specified in the constructor, it has a random value. */ public UUID getUniqueIdentifier() { return LocalPeer.this.localPeerIdentifier; } /** The set of peers currently reachable */ public Collection<RemotePeer> getPeers() { return this.knownPeers.values(); } public PeerDiscoveryHandler getPeerDiscoveryHandler() { return this.peerDiscoveryHandler; } public void setPeerDiscoveryHandler(PeerDiscoveryHandler peerDiscoveryHandler) { this.peerDiscoveryHandler = peerDiscoveryHandler; } public PeerRemovalHandler getPeerRemovalHandler() { return this.peerRemovalHandler; } public void setPeerRemovalHandler(PeerRemovalHandler peerRemovalHandler) { this.peerRemovalHandler = peerRemovalHandler; } public IncomingConnectionHandler getIncomingConnectionHandler() { return this.incomingConnectionHandler; } public void setIncomingConnectionHandler(IncomingConnectionHandler incomingConnectionHandler) { this.incomingConnectionHandler = incomingConnectionHandler; } /** * Starts the local peer (i.e. it will advertise itself and browse for other peers). * You need to set the incomingConnectionHandler property of the discovered RemotePeers, otherwise you will not be able to handle incoming connections. * * @param peerDiscoveryHandler Called when a peer is discovered. You can use a lambda expression of the form "peer -> doSomething(peer)" as the parameter. * @param peerRemovalHandler Called when a peer is lost/removed. You can use a lambda expression of the form "peer -> doSomething(peer)" as the parameter. */ public void start(PeerDiscoveryHandler peerDiscoveryHandler, PeerRemovalHandler peerRemovalHandler) { this.start(peerDiscoveryHandler, peerRemovalHandler, null); } /** * Starts the local peer (i.e. it will advertise itself and browse for other peers). * Incoming connections will be reported via the incomingConnectionHandler, unless the incomingConnectionHandler property of the RemotePeer that established the connection is set. * * @param peerDiscoveryHandler Called when a peer is discovered. You can use a lambda expression of the form "peer -> doSomething(peer)" as the parameter. * @param peerRemovalHandler Called when a peer is lost/removed. You can use a lambda expression of the form "peer -> doSomething(peer)" as the parameter. * @param incomingConnectionHandler Called when a connection is established by a RemotePeer to the LocalPeer, and the RemotePeer's incomingConnectionHandler property is not set. You can, again, use a lambda expression as above. * No connection is passed to the handler; you can obtain the Connection by calling one of the acceptConnection methods on the RemotePeer. */ public void start(PeerDiscoveryHandler peerDiscoveryHandler, PeerRemovalHandler peerRemovalHandler, IncomingConnectionHandler incomingConnectionHandler) { if (this.isStarted) { System.err.println("You attempted to start a LocalPeer that is already started. Nothing will happen."); return; } this.isStarted = true; this.setPeerDiscoveryHandler(peerDiscoveryHandler); this.setPeerRemovalHandler(peerRemovalHandler); this.setIncomingConnectionHandler(incomingConnectionHandler); this.router.start(); } /** * Stops the LocalPeer. It will no longer be advertised and will not browse for other peers. * */ public void stop() { if (!this.isStarted) { System.err.println("You attempted to stop a LocalPeer that is not started. Nothing will happen."); return; } this.isStarted = false; this.router.stop(); } /** * Establishes a multicast connection to a set of peers. * @param destinations The RemotePeers to establish the connection with. * @return A Connection object. It can be used to send data immediately (the transfers will be started once the connection was successfully established). * */ public Connection connect(Set<RemotePeer> destinations) { Set<Node> destinationNodes = new HashSet<>(); for (RemotePeer peer : destinations) destinationNodes.add(peer.getNode()); UUID connectionIdentifier = UUID.randomUUID(); PacketConnection packetConnection = new PacketConnection(null, connectionIdentifier, destinationNodes); this.establishedConnections.put(connectionIdentifier, packetConnection); final Connection transferConnection = new Connection(packetConnection, this.localPeerIdentifier, this.executor, true, this.packetConnectionManager); transferConnection.attemptReconnect(); return transferConnection; } /** * Adds a module to the LocalPeer. * * @param module The module that should be added. * */ public void addModule(Module module) { this.router.addModule(module); } /** * Removes a module from the LocalPeer. * * @param module The module that should be removed. * */ public void removeModule(Module module) { this.router.removeModule(module); } /** Provides an existing RemotePeer for a given Node, or creates a new one if it is not known. */ private de.tum.in.www1.jReto.RemotePeer providePeerForNode(Node node) { de.tum.in.www1.jReto.RemotePeer peer = this.knownPeers.get(node); if (peer == null) { peer = new RemotePeer(this, node); knownPeers.put(node, peer); } return peer; } /** Adds a Node */ private void addNode(Node node) { if (this.knownPeers.containsKey(node)) return; final RemotePeer peer = this.providePeerForNode(node); this.peerDiscoveryHandler.onPeerDiscovered(peer); } /** Removes a Node */ private void removeNode(final Node node) { RemotePeer removedPeer = this.knownPeers.get(node); this.knownPeers.remove(node); this.peerRemovalHandler.onPeerRemoved(removedPeer); } /** * Handles an incoming connection. * * @param node The node which established the connection * @param connection The connection that was established * */ private void handleConnection(final Node node, final de.tum.in.www1.jReto.module.api.Connection connection) { SinglePacketHelper.read(connection, new SinglePacketHelper.OnPacketHandler() { @Override public void onPacket(ByteBuffer data) { ManagedConnectionHandshake handshake = ManagedConnectionHandshake.deserialize(data); if (handshake == null) { System.err.println("Received invalid packet, closing connection."); connection.close(); return; } LocalPeer.this.handleConnection(node, connection, handshake.connectionIdentifier); } }, new SinglePacketHelper.OnFailHandler() { @Override public void onFail() { System.err.println("Failed to receive ManagedConnectionHandshake."); } }); } /** Handles an incoming connection with a know connection identifier. * Called when ManagedConnectionHandshake was received, i.e. when all necessary information is available to deal with this connection. * If the corresponding PacketConnection already exists, its underlying connection is swapped. Otherwise, a new Connection is created. * * @param node The node which established the connection * @param connection The connection that was established * @param connectionIdentifier The identifier of the connection * */ private void handleConnection(Node node, de.tum.in.www1.jReto.module.api.Connection connection, UUID connectionIdentifier) { boolean needsToReportPeer = this.knownPeers.get(node) == null; RemotePeer peer = this.providePeerForNode(node); if (needsToReportPeer) this.peerDiscoveryHandler.onPeerDiscovered(peer); PacketConnection packetConnection = this.incomingConnections.get(connectionIdentifier); if (packetConnection != null) { packetConnection.swapUnderlyingConnection(connection); } else { packetConnection = new PacketConnection(connection, connectionIdentifier, new HashSet<>(Arrays.asList(peer.getNode()))); this.incomingConnections.put(connectionIdentifier, packetConnection); Connection transferConnection = new Connection(packetConnection, this.localPeerIdentifier, this.executor, false, this.packetConnectionManager); if (peer.getIncomingConnectionHandler() != null) { peer.getIncomingConnectionHandler().onConnection(peer, transferConnection); } else if (this.getIncomingConnectionHandler() != null) { this.getIncomingConnectionHandler().onConnection(peer, transferConnection); } else { System.err.println("An incoming connection was received, but onConnection is not set. Set it either in your LocalPeer instance ("+this+"), or in the RemotePeer which established the connection ("+peer+")."); } } } /** * Establishes a new underlying connection for a given packet connection using the Router. * */ private void establishUnderlyingConnection(final PacketConnection packetConnection) { if (packetConnection.getIsEstablishingConnection()) { return; } packetConnection.setIsEstablishingConnection(true); this.router.establishMulticastConnection( packetConnection.getDestinations(), new Router.OnConnectionHandler() { @Override public void onConnect(final de.tum.in.www1.jReto.module.api.Connection connection) { SinglePacketHelper.write(connection, new ManagedConnectionHandshake(packetConnection.getConnectionIdentifier()), new SinglePacketHelper.OnSuccessHandler() { @Override public void onSuccess() { packetConnection.swapUnderlyingConnection(connection); packetConnection.setIsEstablishingConnection(false); } }, new SinglePacketHelper.OnFailHandler() { @Override public void onFail() { packetConnection.setIsEstablishingConnection(false); System.err.println("Failed to send ManagedConnectionHandshake"); } }); } }, new Router.OnFailHandler() { @Override public void onFail() { packetConnection.setIsEstablishingConnection(false); System.err.println("Could not establish a connection to: "+packetConnection.getDestinations()+". Will retry soon."); } }); } /** Reconnects all connections that were established to a certain node. */ private void reconnectConnections(Node node) { for (PacketConnection packetConnection : this.establishedConnections.values()) this.reconnect(packetConnection); } private void removeConnection(PacketConnection connection) { this.establishedConnections.remove(connection.getConnectionIdentifier()); this.incomingConnections.remove(connection.getConnectionIdentifier()); } private void reconnect(PacketConnection connection) { this.establishUnderlyingConnection(connection); } public void setOnConnection(IncomingConnectionHandler handler) { this.incomingConnectionHandler = handler; } public IncomingConnectionHandler getOnConnection() { return this.incomingConnectionHandler; } }