All Downloads are FREE. Search and download functionalities are using the official Maven repository.

convex.net.Connection Maven / Gradle / Ivy

There is a newer version: 0.7.15
Show newest version
package convex.net;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.ByteChannel;
import java.nio.channels.CancelledKeyException;
import java.nio.channels.Channel;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import convex.core.Constants;
import convex.core.Result;
import convex.core.data.ACell;
import convex.core.data.AccountKey;
import convex.core.data.AVector;
import convex.core.data.Address;
import convex.core.data.Format;
import convex.core.data.Hash;
import convex.core.data.IRefFunction;
import convex.core.data.SignedData;
import convex.core.data.Vectors;
import convex.core.data.prim.CVMLong;
import convex.core.exceptions.BadFormatException;
import convex.core.store.AStore;
import convex.core.store.Stores;
import convex.core.transactions.ATransaction;
import convex.core.util.Counters;
import convex.core.util.Utils;
import convex.net.message.Message;

/**
 * 

* Class representing a low-level Connection between network participants. *

* *

* Sent messages are sent asynchronously via the shared client selector. *

* *

* Received messages are read by the shared client selector, converted into * Message instances, and passed to a Consumer for handling. *

* *

* A Connection "owns" the ByteChannel associated with this Peer connection *

*/ @SuppressWarnings("unused") public class Connection { final ByteChannel channel; /** * Counter for IDs of all messages sent from this JVM */ private static long idCounter = 0; /** * Store to use for this connection. Required for responding to incoming * messages. */ private final AStore store; /** * If trusted, the Account Key of the remote peer. */ private AccountKey trustedPeerKey; private static final Logger log = LoggerFactory.getLogger(Connection.class.getName()); /** * Pre-allocated direct buffer for message sending TODO: is one per connection * OK? Users should synchronise on this briefly while building message. */ private final ByteBuffer frameBuf = ByteBuffer.allocateDirect(Format.LIMIT_ENCODING_LENGTH + 20); private final MessageReceiver receiver; private final MessageSender sender; private Connection(ByteChannel clientChannel, Consumer receiveAction, AStore store, AccountKey trustedPeerKey) { this.channel = clientChannel; receiver = new MessageReceiver(receiveAction, this); sender = new MessageSender(clientChannel); this.store = store; this.trustedPeerKey = trustedPeerKey; } /** * Create a PeerConnection using an existing channel. Does not perform any * connection initialisation: channel should already be connected. * * @param channel Byte channel to wrap * @param receiveAction Consumer to be called when a Message is received * @param store Store to use when receiving messages. * @param trustedPeerKey Trusted peer account key if this is a trusted * connection, if not then null* * @return New Connection instance * @throws IOException If IO error occurs */ public static Connection create(ByteChannel channel, Consumer receiveAction, AStore store, AccountKey trustedPeerKey) throws IOException { // Needed in case server has incoming connections but no outbound? ensureSelectorLoop(); return new Connection(channel, receiveAction, store, trustedPeerKey); } /** * Gets the global message ID counter * @return Message ID counter for last message sent */ public static long getCounter() { return idCounter; } /** * Create a PeerConnection by connecting to a remote address * * @param hostAddress Internet Address to connect to * @param receiveAction A callback Consumer to be called for any received * messages on this connection * @param store Store to use for this Connection * @return New Connection instance * @throws IOException If connection fails because of any IO problem * @throws TimeoutException If connection cannot be established within an * acceptable time (~5s) */ public static Connection connect(InetSocketAddress hostAddress, Consumer receiveAction, AStore store) throws IOException, TimeoutException { return connect(hostAddress, receiveAction, store, null); } /** * Create a Connection by connecting to a remote address * * @param hostAddress Internet Address to connect to * @param receiveAction A callback Consumer to be called for any received * messages on this connection * @param store Store to use for this Connection * @param trustedPeerKey Trusted peer account key if this is a trusted * connection, if not then null * @return New Connection instance * @throws IOException If connection fails because of any IO problem * @throws TimeoutException If the connection cannot be established within the * timeout period */ public static Connection connect(InetSocketAddress hostAddress, Consumer receiveAction, AStore store, AccountKey trustedPeerKey) throws IOException, TimeoutException { return connect(hostAddress,receiveAction,store,trustedPeerKey,Constants.SOCKET_SEND_BUFFER_SIZE,Constants.SOCKET_RECEIVE_BUFFER_SIZE); } /** * Create a Connection by connecting to a remote address * * @param hostAddress Internet Address to connect to * @param receiveAction A callback Consumer to be called for any received * messages on this connection * @param store Store to use for this Connection * @param trustedPeerKey Trusted peer account key if this is a trusted * connection, if not then null * @param sendBufferSize Size of connection send buffer in bytes * @param receiveBufferSize Size of connection receive buffer in bytes * @return New Connection instance * @throws IOException If connection fails because of any IO problem * @throws TimeoutException If the connection cannot be established within the * timeout period */ public static Connection connect(InetSocketAddress hostAddress, Consumer receiveAction, AStore store, AccountKey trustedPeerKey, int sendBufferSize, int receiveBufferSize) throws IOException, TimeoutException { ensureSelectorLoop(); if (store == null) throw new Error("Connection requires a store"); SocketChannel clientChannel = SocketChannel.open(); clientChannel.configureBlocking(false); clientChannel.socket().setReceiveBufferSize(receiveBufferSize); clientChannel.socket().setSendBufferSize(sendBufferSize); // TODO: reconsider this clientChannel.socket().setTcpNoDelay(true); clientChannel.connect(hostAddress); long start = Utils.getCurrentTimestamp(); while (!clientChannel.finishConnect()) { long now = Utils.getCurrentTimestamp(); long elapsed=now-start; if (elapsed > Constants.DEFAULT_CLIENT_TIMEOUT) throw new TimeoutException("Couldn't connect"); try { Thread.sleep(10+elapsed/5); } catch (InterruptedException e) { throw new IOException("Connect interrupted", e); } } Connection pc = create(clientChannel, receiveAction, store, trustedPeerKey); pc.startClientListening(); log.debug("Connect succeeded for host: {}", hostAddress); return pc; } public long getReceivedCount() { return receiver.getReceivedCount(); } /** * Returns the remote SocketAddress associated with this connection, or null if * not available * * @return An InetSocketAddress if associated, otherwise null */ public InetSocketAddress getRemoteAddress() { if (!(channel instanceof SocketChannel)) return null; try { return (InetSocketAddress) ((SocketChannel) channel).getRemoteAddress(); } catch (Exception e) { // anything fails, we have no address return null; } } /** * Gets the store associated with this Connection * @return Store instance */ public AStore getStore() { return store; } /** * Returns the local SocketAddress associated with this connection, or null if * not available * * @return A SocketAddress if associated, otherwise null */ public InetSocketAddress getLocalAddress() { if (!(channel instanceof SocketChannel)) return null; try { return (InetSocketAddress) ((SocketChannel) channel).getLocalAddress(); } catch (Exception e) { // anything fails, we have no address return null; } } /** * Sends a DATA Message on this connection. * * Does not send embedded values. * * @param value Any data object, which will be encoded and sent as a single cell * @return true if buffered successfully, false otherwise (not sent) * @throws IOException If IO error occurs */ public boolean sendData(ACell value) throws IOException { log.trace("Sending data: {}", value); ByteBuffer buf = Format.encodedBuffer(value); return sendBuffer(MessageType.DATA, buf); } /** * Sends a DATA Message on this connection. * * @param value Any data object * @return true if buffered successfully, false otherwise (not sent) * @throws IOException If IO error occurs */ public boolean sendMissingData(Hash value) throws IOException { log.trace("Requested missing data for hash {} with store {}", value.toHexString(), Stores.current()); return sendObject(MessageType.MISSING_DATA, value); } /** * Sends a QUERY Message on this connection with a null Address * * @param form A data object representing the query form * @return The ID of the message sent, or -1 if send buffer is full. * @throws IOException If IO error occurs */ public long sendQuery(ACell form) throws IOException { return sendQuery(form, null); } /** * Sends a QUERY Message on this connection. * * @param form A data object representing the query form * @param address The address with which to run the query, which may be null * @return The ID of the message sent, or -1 if send buffer is full. * @throws IOException If IO error occurs */ public long sendQuery(ACell form, Address address) throws IOException { AStore temp = Stores.current(); try { long id = ++idCounter; AVector v = Vectors.of(id, form, address); boolean sent = sendObject(MessageType.QUERY, v); return sent ? id : -1; } finally { Stores.setCurrent(temp); } } /** * Sends a STATUS Request Message on this connection. * * @return The ID of the message sent, or -1 if send buffer is full. * @throws IOException If IO error occurs */ public long sendStatusRequest() throws IOException { AStore temp = Stores.current(); try { long id = ++idCounter; CVMLong idPayload = CVMLong.create(id); sendObject(MessageType.STATUS, idPayload); return id; } finally { Stores.setCurrent(temp); } } /** * Sends a CHALLENGE Request Message on this connection. * * @param challenge Challenge a Vector that has been signed by the sending peer. * * @return The ID of the message sent, or -1 if the message cannot be sent. * * @throws IOException If IO error occurs * */ public long sendChallenge(SignedData challenge) throws IOException { AStore temp = Stores.current(); try { long id = ++idCounter; boolean sent = sendObject(MessageType.CHALLENGE, challenge); return (sent) ? id : -1; } finally { Stores.setCurrent(temp); } } /** * Sends a RESPONSE Request Message on this connection. * * @param response Signed response for the remote peer * @return The ID of the message sent, or -1 if the message cannot be sent. * * @throws IOException If IO error occurs * */ public long sendResponse(SignedData response) throws IOException { AStore temp = Stores.current(); try { long id = ++idCounter; boolean sent = sendObject(MessageType.RESPONSE, response); return (sent) ? id : -1; } finally { Stores.setCurrent(temp); } } /** * Sends a transaction if possible, returning the message ID (greater than zero) * if successful. * * Uses the configured CLIENT_STORE to store the transaction, so that any * missing data requests from the server can be honoured. * * Returns -1 if the message could not be sent because of a full buffer. * * @param signed Signed transaction * @return Message ID of the transaction request, or -1 if send buffer is full. * @throws IOException In the event of an IO error, e.g. closed connection */ public long sendTransaction(SignedData signed) throws IOException { AStore temp = Stores.current(); try { Stores.setCurrent(store); long id = ++idCounter; AVector v = Vectors.of(id, signed); boolean sent = sendObject(MessageType.TRANSACT, v); return (sent) ? id : -1; } finally { Stores.setCurrent(temp); } } /** * Sends a RESULT Message on this connection with no error code (i.e. a success) * * @param id ID for result message * @param value Any data object * @return True if buffered for sending successfully, false otherwise * @throws IOException If IO error occurs */ public boolean sendResult(CVMLong id, ACell value) throws IOException { return sendResult(id, value, null); } /** * Sends a RESULT Message on this connection. * * @param id ID for result message * @param value Any data object * @param errorCode Error code for this result. May be null to indicate success * @return True if buffered for sending successfully, false otherwise * @throws IOException In case of IO Error */ public boolean sendResult(CVMLong id, ACell value, ACell errorCode) throws IOException { Result result = Result.create(id, value, errorCode); return sendObject(MessageType.RESULT, result); } /** * Sends a RESULT Message on this connection. * * @param result Result data structure * @return true if message queued successfully, false otherwise * @throws IOException If IO error occurs */ public boolean sendResult(Result result) throws IOException { return sendObject(MessageType.RESULT, result); } private IRefFunction sender() { return sendAll; } private final IRefFunction sendAll = (r -> { // TODO: halt conditions to prevent sending the whole universe ACell o = r.getValue(); if (o == null) return r; // send children first o.updateRefs(sender()); // only send this value if not embedded if (!o.isEmbedded()) { try { sendData(o); } catch (IOException e) { throw Utils.sneakyThrow(e); } } return r; }); /** * Sends a message over this connection * * @param msg Message to send * @return true if message buffered successfully, false if failed * @throws IOException If IO error occurs */ public boolean sendMessage(Message msg) throws IOException { return sendObject(msg.getType(), msg.getPayload()); } /** * Sends a payload for the given message type. Should be called on the thread * that responds to missing data messages from the destination. * * @param type Type of message * @param payload Payload value for message * @return true if message queued successfully, false otherwise * @throws IOException If IO error occurs */ public boolean sendObject(MessageType type, ACell payload) throws IOException { Counters.sendCount++; // Need to ensure message is persisted at least, so we can respond to missing // data messages using the current thread store // We pre-send any novelty to the destination ACell sendVal = payload; ACell.createPersisted(sendVal, r -> { try { ACell data = r.getValue(); if (data==sendVal) return; // skip sending top payload if (!Format.isEmbedded(data)) sendData(data); } catch (IOException e) { throw Utils.sneakyThrow(e); } }); ByteBuffer buf = Format.encodedBuffer(sendVal); if (log.isTraceEnabled()) { log.trace("Sending message: " + type + " :: " + payload + " to " + getRemoteAddress() + " format: " + Format.encodedBlob(payload).toHexString()); } boolean sent = sendBuffer(type, buf); return sent; } /** * Sends a message with the given message type and data buffer. * * @param type MessageType value * @param buf Buffer containing raw wire data for the message * @return true if message sent, false otherwise * @throws IOException */ private boolean sendBuffer(MessageType type, ByteBuffer buf) throws IOException { int dataLength = buf.remaining(); // Total length field is message code + encoded object length int messageLength = dataLength + 1; boolean sent; int headerLength; // synchronize in case we are sending messages from different threads // This is OK but need to avoid corrupted messages. synchronized (frameBuf) { // ensure frameBuf is clear and ready for writing frameBuf.clear(); // write message header Format.writeMessageLength(frameBuf, messageLength); frameBuf.put(type.getMessageCode()); headerLength = frameBuf.position(); // now write message frameBuf.put(buf); frameBuf.flip(); // ensure frameBuf is ready to write to channel sent = sender.bufferMessage(frameBuf); } if (sent) { if (channel instanceof SocketChannel) { SocketChannel chan = (SocketChannel) channel; // register interest in both reads and writes try { chan.register(selector, SelectionKey.OP_WRITE | SelectionKey.OP_READ, this); } catch (CancelledKeyException e) { // ignore. Must have got cancelled elsewhere? } // wake up selector selector.wakeup(); } if (log.isTraceEnabled()) { log.trace("Sent message " + type + " of length: " + dataLength + " Connection ID: " + System.identityHashCode(this)); } } else { log.debug("sendBuffer failed with message {} of length: {} Connection ID: {}" , type, dataLength, System.identityHashCode(this)); } return sent; } public synchronized void close() { SocketChannel chan = (SocketChannel) channel; if (chan != null) { try { chan.close(); } catch (IOException e) { // TODO OK to ignore? } } } /** * Checks if this connection is closed (i.e. the underlying channel is closed) * * @return true if the channel is closed, false otherwise. */ public boolean isClosed() { return !channel.isOpen(); } /** * Starts listening for received events with this given peer connection. * PeerConnection must have a selectable SocketChannel associated * * @throws IOException If IO error occurs */ private void startClientListening() throws IOException { SocketChannel chan = (SocketChannel) channel; chan.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE, this); // seems to be needed to ensure selector sees new connection? selector.wakeup(); } /** * Selector object for all client connections */ private static final Selector selector; static { try { // System.err.println("Initialising Client selector"); selector = Selector.open(); } catch (IOException e) { throw new Error("Error initialising client selector",e); } } private static Thread selectorThread; private static void ensureSelectorLoop() { // Double checked locking. Don't want to start this twice! if (selectorThread==null) { synchronized(Connection.class) { if (selectorThread==null) { selectorThread = new Thread(selectorLoop, "Connection NIO client selector loop"); // make this a daemon thread so it shuts down if everything else exits selectorThread.setDaemon(true); selectorThread.start(); } } } } private static Runnable selectorLoop = new Runnable() { @Override public void run() { log.info("Client selector loop starting..."); while (true) { try { selector.select(300); Set keys = selector.selectedKeys(); Iterator it = keys.iterator(); while (it.hasNext()) { final SelectionKey key = it.next(); it.remove(); // always remove key from selection set // log.finest("PeerConnection key received: "+key); if (!key.isValid()) { continue; } try { if (key.isReadable()) { selectRead(key); } else if (key.isWritable()) { selectWrite(key); } } catch (ClosedChannelException e) { // channel was closed, just lose the key? log.debug("Unexpected ChannelClosedException, cancelling key: {}", e); key.cancel(); } catch (IOException e) { log.debug("Unexpected IOException, cancelling key: {}", e); key.cancel(); } catch (CancelledKeyException e) { log.debug("Cancelled key"); } } } catch (Throwable t) { log.error("Uncaught error in PeerConnection client selector loop: {}", t); t.printStackTrace(); } } } }; /** * Handles channel reads from a SelectionKey for the client listener * * SECURITY: Called on Connection Selector Thread * * @param key * @throws IOException */ protected static void selectRead(SelectionKey key) throws IOException { Connection conn = (Connection) key.attachment(); if (conn == null) throw new Error("No PeerConnection specified"); try { int n = conn.handleChannelRecieve(); // log.finest("Received bytes: " + n); } catch (ClosedChannelException e) { log.debug("Channel closed from: {}", conn.getRemoteAddress()); key.cancel(); } catch (BadFormatException e) { log.warn("Cancelled connection to Peer: Bad data format from: " + conn.getRemoteAddress() + " " + e.getMessage()); key.cancel(); } } /** * Handles receipt of bytes from the channel on this Connection. * * Will switch the current store to the Connection-specific store if required. * * SECURITY: Called on NIO Thread (Server or client Connection) * * @return The number of bytes read from channel * @throws IOException If IO error occurs * @throws BadFormatException If there is an encoding error */ public int handleChannelRecieve() throws IOException, BadFormatException { AStore tempStore = Stores.current(); try { // set the current store for handling incoming messages Stores.setCurrent(store); return receiver.receiveFromChannel(channel); } finally { Stores.setCurrent(tempStore); } } /** * Handles writes to the channel. * * SECURITY: Called on Selector Thread * * @param key Selection Key * @throws IOException */ static void selectWrite(SelectionKey key) throws IOException { Connection pc = (Connection) key.attachment(); boolean allSent = pc.sender.maybeSendBytes(); if (allSent) { // deregister interest in writing key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE); } else { // we want to continue writing } } /** * Sends bytes buffered into the underlying channel. * @return True if all bytes are sent, false otherwise * @throws IOException If an IO Exception occurs */ public boolean flushBytes() throws IOException { return sender.maybeSendBytes(); } @Override public String toString() { return "PeerConnection: " + channel; } public AccountKey getTrustedPeerKey() { return trustedPeerKey; } public void setTrustedPeerKey(AccountKey value) { trustedPeerKey = value; } public boolean isTrusted() { return trustedPeerKey != null; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy