![JAR search and dependency download from the Maven repository](/logo.png)
convex.net.Connection Maven / Gradle / Ivy
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.HashMap;
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.Blob;
import convex.core.data.Format;
import convex.core.data.Hash;
import convex.core.data.IRefFunction;
import convex.core.data.Ref;
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.impl.HandlerException;
import convex.peer.Config;
/**
*
* Class representing the low-level NIO network 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 Connection
*/
private long idCounter = 0;
/**
* Timestamp of last connection activity
*/
private long lastActivity;
/**
* 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());
private final MessageReceiver receiver;
private final MessageSender sender;
private Connection(ByteChannel channel, Consumer receiveAction, AStore store,
AccountKey trustedPeerKey) {
this.channel = channel;
receiver = new MessageReceiver(receiveAction, this);
sender = new MessageSender(channel);
this.store = store;
this.lastActivity=Utils.getCurrentTimestamp();
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);
}
/**
* Create a Connection by connecting to a remote address
*
* @param socketAddress 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 socketAddress, Consumer receiveAction, AStore store)
throws IOException, TimeoutException {
return connect(socketAddress, receiveAction, store, null);
}
/**
* Create a Connection by connecting to a remote address
*
* @param socketAddress 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 socketAddress, Consumer receiveAction, AStore store,
AccountKey trustedPeerKey) throws IOException, TimeoutException {
return connect(socketAddress,receiveAction,store,trustedPeerKey,Config.SOCKET_SEND_BUFFER_SIZE,Config.SOCKET_RECEIVE_BUFFER_SIZE);
}
/**
* Create a Connection by connecting to a remote address
*
* @param socketAddress 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 socketAddress, 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);
// Disable Nagle, we don't want this as we want to send one-way traffic as fast as possible
clientChannel.socket().setTcpNoDelay(true);
clientChannel.connect(socketAddress);
// System.out.println("Connection: attempting to connect to: "+socketAddress);
long start = Utils.getCurrentTimestamp();
while (!clientChannel.finishConnect()) {
long now = Utils.getCurrentTimestamp();
long elapsed=now-start;
if (elapsed > Config.DEFAULT_CLIENT_TIMEOUT)
throw new TimeoutException("Couldn't connect after "+elapsed+"ms");
try {
Thread.sleep(10+elapsed/3);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("Connect interrupted", e);
}
}
Connection pc = create(clientChannel, receiveAction, store, trustedPeerKey);
pc.startClientListening();
log.trace("Connect succeeded for host: {}", socketAddress);
return pc;
}
public long getReceivedCount() {
return receiver.getReceivedCount();
}
/**
* Sets an optional additional message receiver hook (for debugging / observability purposes)
* @param hook Hook to call when a message is received
*/
public void setReceiveHook(Consumer hook) {
receiver.setHook(hook);
}
/**
* 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 (IOException 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 (IOException e) {
// anything fails, we have no address
return null;
}
}
/**
* Sends a DATA Message on this connection.
*
* Does not send embedded values.
*
* @param data Encoded data object
* @return true if buffered successfully, false otherwise (not sent)
* @throws IOException If IO error occurs
*/
public boolean sendData(Blob data) throws IOException {
log.trace("Sending data: {}", data);
return sendBuffer(MessageType.DATA, data);
}
/**
* 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();
long id = ++idCounter;
AVector v = Vectors.of(id, form, address);
boolean sent = sendObject(MessageType.QUERY, v);
return sent ? id : -1;
}
/**
* 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();
long id = ++idCounter;
CVMLong idPayload = CVMLong.create(id);
boolean sent=sendObject(MessageType.STATUS, idPayload);
return sent? id:-1;
}
/**
* 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.
*
* 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 {
long id = getNextID();
AVector v = Vectors.of(id, signed);
boolean sent = sendObject(MessageType.TRANSACT, v);
return (sent) ? id : -1;
}
/**
* Sends a message over this connection
*
* @param msg Message to send
* @return true if message buffered successfully, false if failed due to full buffer
* @throws IOException If IO error occurs while sending
*/
public boolean sendMessage(Message msg) throws IOException {
return sendBuffer(msg.getType(),msg.getMessageData());
}
/**
* Sends a message with full payload for the given message type.
*
* @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
*/
private boolean sendObject(MessageType type, ACell payload) throws IOException {
Counters.sendCount++;
Blob enc = Format.encodeMultiCell(payload,true);
if (log.isTraceEnabled()) {
log.trace("Sending message: " + type + " :: " + payload + " to " + getRemoteAddress() + " format: "
+ Format.encodedBlob(payload).toHexString());
}
boolean sent = sendBuffer(type, enc);
return sent;
}
/**
* Sends a message with the given message type and data.
*
* @param type MessageType value
* @param data Raw data for the message
* @return true if message sent, false otherwise
* @throws IOException
*/
private boolean sendBuffer(MessageType type, Blob data) throws IOException {
// synchronise on sender
synchronized (sender) {
if (!sender.canSendMessage()) return false;
int dataLength = Utils.checkedInt(data.count());
// Total message length field is one byte for message code + encoded object length
int messageLength = dataLength + 1;
boolean sent;
int headerLength;
// ensure frameBuf is clear and ready for writing
ByteBuffer frameBuf=ByteBuffer.allocate(messageLength+10);
// write message header (length plus message code)
Format.writeMessageLength(frameBuf, messageLength);
frameBuf.put(type.getMessageCode());
headerLength = frameBuf.position();
// now write message
frameBuf.put(headerLength, data.getInternalArray(), data.getInternalOffset(), dataLength);
frameBuf.position(headerLength+dataLength);
frameBuf.flip(); // ensure frameBuf is ready to write to channel
sent = sender.bufferMessage(frameBuf);
if (sent) {
lastActivity=System.currentTimeMillis();
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 if needed. TODO: do we need this?
// if (!sender.canSendMessage()) {
// selector.wakeup();
// }
}
if (log.isTraceEnabled()) {
log.trace("Sent message " + type + " of length: " + dataLength + " Connection ID: "
+ System.identityHashCode(this));
}
} else {
log.warn("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?
}
}
}
@Override
public void finalize() {
close();
}
/**
* 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 Selector selector;
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) {
try {
// System.err.println("Initialising Client selector");
selector = Selector.open();
} catch (IOException e) {
throw new Error("Error initialising client selector",e);
}
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.trace("Client selector loop starting...");
while (!Thread.currentThread().isInterrupted()) {
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.trace("Unexpected ChannelClosedException, cancelling key: {}", e);
key.cancel();
} catch (IOException e) {
log.trace("Unexpected IOException, cancelling key: {}", e);
key.cancel();
} catch (CancelledKeyException e) {
log.trace("Cancelled key");
}
}
} catch (IOException t) {
log.warn("Uncaught IO error in PeerConnection client selector loop: ", t);
}
}
}
};
/**
* 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();
if (n<0) {
// Deregister interest in reading if EOS
log.trace("Cancelled Key due to EOS");
key.cancel();
}
// log.finest("Received bytes: " + n);
} catch (ClosedChannelException e) {
log.trace("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();
} catch (HandlerException e) {
log.warn("Cancelled connection: error in handler: " +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, or -1 if EOS
* @throws IOException If IO error occurs
* @throws BadFormatException If there is an encoding error
*/
public int handleChannelRecieve() throws IOException, BadFormatException, HandlerException {
AStore savedStore = Stores.current();
try {
// set the current store for handling incoming messages
Stores.setCurrent(store);
int recd= receiver.receiveFromChannel(channel);
int total =recd;
while (recd>0) {
recd=receiver.receiveFromChannel(channel);
total+=recd;
}
if (recd>0) lastActivity=System.currentTimeMillis();
return total;
} finally {
Stores.setCurrent(savedStore);
}
}
/**
* Handles writes to the channel.
*
* SECURITY: Called on Selector Thread, must never block
*
* @param key Selection Key
* @throws IOException
*/
static void selectWrite(SelectionKey key) throws IOException {
Connection pc = (Connection) key.attachment();
synchronized(pc.sender) {
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;
}
public long getLastActivity() {
return lastActivity;
}
public long getNextID() {
return ++idCounter;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy