package de.tum.in.www1.jReto.connectivity;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.LinkedBlockingQueue;
import de.tum.in.www1.jReto.module.api.Connection;
import de.tum.in.www1.jReto.packet.Packet;
import de.tum.in.www1.jReto.packet.PacketType;
import de.tum.in.www1.jReto.routing.Node;
/**
* PacketConnections are used to send and receive packets. Packets used by Reto can be any data of any fixed length,
* but the first four bytes always contain the packet's type.
*
* A PacketConnection encapsulates the idea of a logical connection. It adds an additional indirection between a Connection as provided by
* the Reto API and underlying connections. This allows the PacketConnection to switch between different underlying connections, and even exist without
* an underlying connection. This is the basis of features such as automatic reconnect, and offering automatic connection upgrades.
*
* Packets can be sent by calling the write() method. If data is currently written, or no underlying connection is available, the packet is buffered
* until it can be sent.
*
* A PacketConnection can have multiple delegates. Amongst other events, the PacketConnection delegates the handling of packets to multiple delegates that implement
* the PacketHandler protocol. The PacketHandlers may specify the packet types they are able to handler; the packet connection will call the according handler's
* handlePacket method.
*
* The PacketConnection itself is agnostic of the different packet types; the introduction of new packet types and handlers does not require any changes
* in the PacketConnection class. This allows Reto to split up different functionality of Connections into different classes,
* e.g. the TransferManager and ReliabilityManager, which are both PacketHandlers.
*/
public class PacketConnection {
/**
* The delegate protocol used with the PacketConnection.
* It allows delegates to specify which types of packets they wish to handle.
*/
public static interface Handler {
/** Called when the underlying connection closed. */
void onUnderlyingConnectionClose(PacketConnection connection);
/** Called when the underlying connection is about to be switched to a different one. */
void onWillSwapUnderlyingConnection(PacketConnection connection);
/** Called when confirmation that the underlying connection did connect is received. */
void onUnderlyingConnectionConnected(PacketConnection connection);
/** Called whenever all packets that were queued have been sent, i.e. the connection is ready for more data if available. */
void onNoPacketsLeft(PacketConnection connection);
/** An array of packet types that are handled by this PacketHandler. */
Set<PacketType> getHandledPacketTypes();
/** Called when a packet is received that should be handled */
void handlePacket(ByteBuffer data, PacketType type);
}
/** The underlying connection used by this PacketConnection */
private de.tum.in.www1.jReto.module.api.Connection underlyingConnection;
/** The PacketConnection's delegates. */
private Set<Handler> delegates = new HashSet<>();
/**
* The PacketConnection's delegates, organized by the packet type they handle.
*/
private Map<PacketType, Handler> packetHandlers = new HashMap<>();
/** The connection's identifier. This identifier is used to associate new incoming underlying connections with exising packet connections. */
private final UUID connectionIdentifier;
/** This connection's destinations. */
private final Set<Node> destinations;
/** Buffer for unsent packets. */
private Queue<Packet> unsentPackets;
/** Whether a packet is currently being sent. */
private boolean isSendingPacket = false;
/** Whether a connection is currently being established. */
private boolean isEstablishingConnection = false;
/** Handles events from the underlying connection and calls appropriate methods. */
private final de.tum.in.www1.jReto.module.api.Connection.Handler underlyingConnectionHandler = new de.tum.in.www1.jReto.module.api.Connection.Handler() {
@Override
public void onDataReceived(de.tum.in.www1.jReto.module.api.Connection connection,
ByteBuffer data) {
if (connection != PacketConnection.this.underlyingConnection) {
System.out.println("Received data from unused underlying connection.");
return;
}
PacketType packetType = PacketType.fromData(data);
Handler handler = PacketConnection.this.packetHandlers.get(packetType);
if (handler == null) {
System.err.println("Warning: There is no handler for packets of type "+ packetType+ ". Registered handlers: "+ PacketConnection.this.packetHandlers);
return;
}
handler.handlePacket(data, packetType);
}
@Override
public void onDataSent(de.tum.in.www1.jReto.module.api.Connection connection) {
if (connection != PacketConnection.this.underlyingConnection) {
System.out.println("Received onDataSent from unused underlying connection.");
return;
}
PacketConnection.this.isSendingPacket = false;
PacketConnection.this.write();
}
@Override
public void onConnect(de.tum.in.www1.jReto.module.api.Connection connection) {
if (connection != PacketConnection.this.underlyingConnection) {
System.out.println("Received onConnect from unused underlying connection.");
return;
}
PacketConnection.this.onConnect();
}
@Override
public void onClose(de.tum.in.www1.jReto.module.api.Connection connection) {
if (connection != PacketConnection.this.underlyingConnection) {
System.out.println("Previous underlying connection closed.");
connection.setHandler(null);
return;
}
PacketConnection.this.delegates.forEach(delegate -> delegate.onUnderlyingConnectionClose(PacketConnection.this));
}
};
/**
* Initializes a new PacketConnection.
*
* @param connection An underlying connection to use with this packet connection. May be nil and set later.
* @param connectionIdentifier This connection's identifier.
* @param destinations The connection's destinations.
*/
public PacketConnection(de.tum.in.www1.jReto.module.api.Connection underlyingConnection, UUID connectionIdentifier, Set<Node> destinations) {
this.unsentPackets = new LinkedBlockingQueue<Packet>();
this.underlyingConnection = underlyingConnection;
this.connectionIdentifier = connectionIdentifier;
this.destinations = destinations;
if (this.underlyingConnection != null) {
this.underlyingConnection.setHandler(this.underlyingConnectionHandler);
if (this.underlyingConnection.isConnected()) this.onConnect();
}
}
private void onConnect() {
this.delegates.forEach(delegate -> delegate.onUnderlyingConnectionConnected(this));
this.write();
}
/** Add a delegate */
public void addDelegate(Handler delegate) {
this.delegates.add(delegate);
for (PacketType type : delegate.getHandledPacketTypes()) {
this.packetHandlers.put(type, delegate);
}
}
/** Remove a delegate*/
public void removeDelegate(Handler delegate) {
this.delegates.remove(delegate);
// TODO remove packet handlers
}
public Connection getUnderlyingConnection() {
return this.underlyingConnection;
}
public Set<Node> getDestinations() {
return this.destinations;
}
public boolean getIsEstablishingConnection() {
return this.isEstablishingConnection;
}
public void setIsEstablishingConnection(boolean isEstablishingConnection) {
this.isEstablishingConnection = isEstablishingConnection;
}
/**
* Swaps this PacketConnection's underlying connection to a new one. The new connection can also be nil; in that case the packet connection will be disconnected.
*/
public void swapUnderlyingConnection(de.tum.in.www1.jReto.module.api.Connection underlyingConnection) {
if (this.underlyingConnection == underlyingConnection) return;
if (this.underlyingConnection != null) {
this.delegates.forEach(delegate -> delegate.onWillSwapUnderlyingConnection(this));
}
Connection previousConnection = this.underlyingConnection;
this.underlyingConnection = underlyingConnection;
if (this.underlyingConnection != null) this.underlyingConnection.setHandler(this.underlyingConnectionHandler);
if (previousConnection != null && previousConnection.isConnected()) previousConnection.close();
this.isSendingPacket = false;
this.unsentPackets.clear();
if (this.underlyingConnection != null && this.underlyingConnection.isConnected()) this.onConnect();
}
public UUID getConnectionIdentifier() {
return this.connectionIdentifier;
}
public boolean getIsConnected() {
return this.underlyingConnection != null && this.underlyingConnection.isConnected();
}
/**
* Closes the underlying connection
*/
public void disconnectUnderlyingConnection() {
this.underlyingConnection.close();
}
/**
* Writes a packet. The packet will be buffered and sent later if the connection is currently disconnected.
*/
public void writePacket(Packet packet) {
this.unsentPackets.add(packet);
this.write();
}
/**
* Attempts to write any buffered packets, or notifies it's delegates that all packes have been written.
*/
public void write() {
if (this.isSendingPacket)
return;
if (this.underlyingConnection == null
|| !this.underlyingConnection.isConnected())
return;
Packet nextPacket = null;
if (this.unsentPackets.size() != 0) {
nextPacket = this.unsentPackets.poll();
}
if (nextPacket != null) {
this.isSendingPacket = true;
this.underlyingConnection.writeData(nextPacket.serialize());
} else {
this.delegates.forEach(delegate -> delegate.onNoPacketsLeft(this));
}
}
}