
net.sf.eBus.client.ETCPConnection Maven / Gradle / Ivy
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later
// version.
//
// This library is distributed in the hope that it will be
// useful, but WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more
// details.
//
// You should have received a copy of the GNU Lesser General
// Public License along with this library; if not, write to the
//
// Free Software Foundation, Inc.,
// 59 Temple Place, Suite 330,
// Boston, MA
// 02111-1307 USA
//
// The Initial Developer of the Original Code is Charles W. Rapp.
// Portions created by Charles W. Rapp are
// Copyright 2015, 2016. Charles W. Rapp
// All Rights Reserved.
//
package net.sf.eBus.client;
import java.io.IOException;
import java.net.ProtocolException;
import java.net.SocketAddress;
import java.net.StandardSocketOptions;
import java.nio.BufferOverflowException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SocketChannel;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.eBus.config.EConfigure;
import net.sf.eBus.messages.EMessage;
import net.sf.eBus.messages.EMessageHeader;
import net.sf.eBus.messages.InvalidMessageException;
import net.sf.eBus.messages.UnknownMessageException;
import net.sf.eBus.messages.type.DataType;
import net.sf.eBus.net.AbstractAsyncSocket;
import net.sf.eBus.net.AsyncChannel;
import net.sf.eBus.net.AsyncSecureSocket;
import net.sf.eBus.net.AsyncSecureSocket.SecureSocketBuilder;
import net.sf.eBus.net.AsyncSocket;
import net.sf.eBus.net.AsyncSocket.SocketBuilder;
import net.sf.eBus.net.BufferWriter;
import net.sf.eBus.net.SocketListener;
import net.sf.eBus.util.HexDump;
import net.sf.eBus.util.TimerEvent;
import net.sf.eBus.util.TimerTask;
import net.sf.eBus.util.TimerTaskListener;
/**
* Implements communication between eBus applications using a
* TCP session. This class implements the eBus message protocol,
* handling heart beating and automatic reconnect upon
* disconnect, according to {@link EConfigure} settings. There is
* a one-to-one relationship between {@code ETCPConnection} and
* {@link ERemoteApp}. All inbound messages are posted to the
* {@code ERemoteApp} instance and only that {@code ERemoteApp}
* instance may send outbound messages.
*
* @see ERemoteApp
*
* @author Charles W. Rapp
*/
/* package */ final class ETCPConnection
extends EAbstractConnection
implements SocketListener,
TimerTaskListener
{
//---------------------------------------------------------------
// Member enums.
//
/**
* The eBus connection states.
*/
private enum ConnectState
{
/**
* The eBus connection is closed.
*/
CLOSED,
/**
* eBus is in the process of connecting.
*/
CONNECTING,
/**
* The eBus is connected. Note that this has nothing to
* do with logging on to the remote eBus.
*/
CONNECTED,
/**
* The eBus connection will be closed once all messages
* are sent.
*/
CLOSING,
/**
* The eBus connection is down and will be
* re-established.
*/
RECONNECTING
} // end of enum ConnectState
//---------------------------------------------------------------
// Member data.
//
//-----------------------------------------------------------
// Constants.
//
/**
* A zero bind port value means that the local end of the
* TCP connection will be bound to any available ephemeral
* port.
*/
public static final int ANY_PORT = 0;
/**
* Wait 60 seconds before attempting to reconnect by default.
*/
public static final long DEFAULT_RECONNECT_DELAY =
60000; // milliseconds.
/**
* The default heartbeat rate is 0 milliseconds - which means
* that heartbeating is off.
*/
public static final long DEFAULT_HEARTBEAT_DELAY = 0;
/**
* Wait 60 seconds for a heartbeat reply by default.
*/
public static final long DEFAULT_HEARTBEAT_REPLY_DELAY =
60000; // milliseconds.
//-----------------------------------------------------------
// Statics.
//
/**
* Access point for logging.
*/
private static final Logger sLogger =
Logger.getLogger(ETCPConnection.class.getName());
//-----------------------------------------------------------
// Locals.
//
/**
* Forward connection events (which includes messages) to
* this listener.
*/
private final ERemoteApp mRemoteApp;
/**
* When set to true, attempt to maintain the connection.
*/
private volatile boolean mReconnectFlag;
/**
* If the connection is lost, then reconnect after the
* specified millisecond delay.
*/
private final AtomicLong mReconnectDelay;
/**
* When this timer expires, attempt to connect.
*/
private volatile TimerTask mReconnectTimer;
/**
* Send heartbeats at this frequency.
*/
private final AtomicLong mHeartbeatDelay;
/**
* The heartbeat timer task.
*/
private volatile TimerTask mHeartbeatTimer;
/**
* When this flag is true, we are expecting a heartbeat
* reply. When a heartbeat is received and this flag is
* true, don't send a heartbeat in reply. When this flag is
* false, send a heartbeat.
*/
private volatile boolean mHeartbeatReplyFlag;
/**
* If heartbeating is turned on, then send heartbeat
* messages at this millisecond rate.
*/
private final AtomicLong mHeartbeatReplyDelay;
/**
* The heart beat timer task.
*/
private volatile TimerTask mHeartbeatReplyTimer;
/**
* The current connection state.
*/
private volatile ConnectState mState;
//---------------------------------------------------------------
// Member methods.
//
//-----------------------------------------------------------
// Constructors.
//
/**
* Creates a closed socket with the specified input and
* output buffer sizes and the buffer byte order. The
* {@link AsyncSocket} and {@link MessageWriter} are
* instantiated in
* {@link #initialize(int, int, ByteOrder, int, String)}
* because {@code this} is used as a constructor parameter.
* @param remoteApp pass events to this eBus remote
* application interface.
* @throws IllegalArgumentException
* if {@code remoteApp} is {@code null}.
*/
private ETCPConnection(final ERemoteApp remoteApp)
{
if (remoteApp == null)
{
throw (
new IllegalArgumentException(
"remoteApp is null"));
}
mRemoteApp = remoteApp;
mReconnectFlag = false;
mReconnectDelay =
new AtomicLong(DEFAULT_RECONNECT_DELAY);
mReconnectTimer = null;
mHeartbeatTimer = null;
mHeartbeatDelay =
new AtomicLong(DEFAULT_HEARTBEAT_DELAY);
mHeartbeatReplyTimer = null;
mHeartbeatReplyFlag = false;
mHeartbeatReplyDelay =
new AtomicLong(DEFAULT_HEARTBEAT_REPLY_DELAY);
mState = ConnectState.CLOSED;
} // end of ETCPConnection(ERemoteApp)
//
// end of Constructors.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Abstract Method Implementations.
//
/**
* Returns {@code true} if this connection automatically
* reconnects upon unexpected connection loss.
* @return {@code true} if automatic reconnection is set
* and {@code false} otherwise.
*/
@Override
public boolean willReconnect()
{
return (mReconnectFlag);
} // end of willReconnect()
/**
* Closes the eBus connection after verifying that
* all previously sent messages have been transmitted. No
* new messages will be sent after this call and no new
* messages will be received. If there are any outbound
* messages waiting to be transmitted, then the connection
* will be closed once those messages are sent.
* @see #open(SocketChannel, long, long)
* @see #open(SocketAddress, int, boolean, long, long, long)
* @see #closeNow
* @see #send(EMessageHeader)
*/
@Override
/* package */ void close()
{
if (mState != ConnectState.CLOSING &&
mState != ConnectState.CLOSED)
{
closeConnection();
// Are there still outbound messages pending?
if (mOutputWriter.hasMessages())
{
// Yes. Wait until those messages are sent before
// closing the socket.
mState = ConnectState.CLOSING;
mOutputWriter.setClosing();
}
// There are no pending messages. Close the socket
// if open.
else
{
mState = ConnectState.CLOSED;
if (mAsocket.isOpen())
{
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format(
"%s: closing connection.",
mAsocket.remoteSocketAddress()));
}
mAsocket.close();
}
}
}
return;
} // end of close()
/**
* Closes the eBus connection immediately. All messages
* posted for transmission but not completely sent are lost.
* No new messages may be sent after this point and no new
* messages will be received.
* @see #open(SocketChannel, long, long)
* @see #open(SocketAddress, int, boolean, long, long, long)
* @see #close
* @see #send(EMessageHeader)
*/
@Override
/* package */ void closeNow()
{
if (mState != ConnectState.CLOSED)
{
closeConnection();
if (mAsocket.isOpen())
{
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format(
"%s: closing connection now.",
mAsocket.remoteSocketAddress()));
}
mAsocket.closeNow();
// Discard any and all unsent messages.
mOutputWriter.closed();
}
}
return;
} // end of closeNow()
/**
* Establishes a connection to the address specified in
* {@code config}. Returns {@code true} if the connection is
* established and {@code false} if the connection is
* in-progress. If {@code false} is returned,
* {@link ERemoteApp#handleOpen(EAbstractConnection)} is
* called when the connection is established.
* @param config remote connection configuration.
* @return {@code true} if connection is established and
* {@code false} if the connection is in-progress.
* @exception IllegalStateException
* if the selector thread is not running or if a connection
* is already in place.
* @exception IOException
* if socket connection attempt threw an exception and
* {@code reconnectFlag} is {@code false}.
*/
@Override
/* package */ boolean open(final EConfigure.RemoteConnection config)
throws IOException
{
boolean retcode = false;
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format(
"%s: connecting.", config.address()));
}
try
{
mState = ConnectState.CONNECTING;
retcode =
((AbstractAsyncSocket) mAsocket).open(
config.address(), config.bindPort());
// Set these data members after calling
// AsyncSocket.open() in case the socket throws an
// IllegalStateException.
mBindPort = config.bindPort();
mReconnectFlag = config.reconnectFlag();
mReconnectDelay.set(config.reconnectTime());
mHeartbeatDelay.set(config.heartbeatDelay());
mHeartbeatReplyDelay.set(config.heartbeatReplyDelay());
if (retcode)
{
mState = ConnectState.CONNECTED;
startHeartbeatTimer();
}
}
catch (IOException ioex)
{
sLogger.log(Level.WARNING,
String.format(
"%s: open failed.",
config.address()),
ioex);
// If not reconnecting, then re-throw this exception
// so the caller can see and respond to it.
if (!(config.reconnectFlag()))
{
throw (ioex);
}
// Set these data members after calling
// AsyncSocket.open() in case the socket throws an
// IllegalStateException.
mBindPort = config.bindPort();
mReconnectFlag = config.reconnectFlag();
startReconnectTimer();
}
return (retcode);
} // end of open(SocketAddress, ...)
/**
* Encapsulates an already connected socket accept by an
* {@link EServer} instance. Will not reconnect.
* @param socket the accepted socket connection.
* @param config accept socket configuration.
* @exception NullPointerException
* if {@code socket} is {@code null}.
* @exception IOException
* if {@code socket} is closed.
*/
@Override
/* package */ void open(final SelectableChannel socket,
final EConfigure.Service config)
throws IOException
{
Objects.requireNonNull(socket, "socket is null");
mState = ConnectState.CONNECTED;
mReconnectFlag = false;
mReconnectDelay.set(0L);
mHeartbeatDelay.set(config.heartbeatDelay());
mHeartbeatReplyDelay.set(config.heartbeatReplyDelay());
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format(
"%s: encapsulating socket.",
mAsocket.remoteSocketAddress()));
}
((AbstractAsyncSocket) mAsocket).open(socket);
// If this is a plain text TCP connection, then start
// the heartbeat timer. Otherwise, wait for the secure
// handshake to complete.
if (config.connectionType() ==
EConfigure.ConnectionType.TCP)
{
startHeartbeatTimer();
}
return;
} // end of open(SocketChannel, long, long)
/**
* Sends {@code message} to remote eBus application. Since
* messages are sent asynchronously, then it is likely that
* the message was not transmitted prior to this
* method returning. If an application must be certain that
* all messages are transmitted prior to closing the
* connection, then {@link #close} should be used rather than
* {@link #closeNow} as {@link #close} waits for the pending
* outgoing messages to be transmitted before closing
* the socket connection. Once a connection is closed, no
* further messages may be sent.
* @param header send this header and message.
* @exception IllegalArgumentException
* if {@code header} is {@code null}.
* @exception IllegalStateException
* if this eBus connection is not connected.
* @exception IOException
* if the connection is closed.
*/
@Override
/* package */ void send(final EMessageHeader header)
throws IllegalArgumentException,
IllegalStateException,
IOException
{
if (header == null)
{
throw (new IllegalArgumentException("null header"));
}
else if (mState != ConnectState.CONNECTED)
{
throw (new IllegalStateException("not connected"));
}
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format(
"%s: sending message to remote eBus: from ID=%d, to ID=%d%n%s",
mAsocket.remoteSocketAddress(),
header.fromFeedId(),
header.toFeedId(),
header.message()));
}
else if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format(
"%s: sending %s message to remote eBus: from ID=%d, to ID=%d.",
mAsocket.remoteSocketAddress(),
header.messageClass(),
header.fromFeedId(),
header.toFeedId()));
}
// Give the message to the output writer and the output
// writer to the socket. If the output writer queue is
// full, allow the buffer overflow exception to pass up
// to the caller.
if (mOutputWriter.post(header))
{
// But if the async socket throws a buffer overflow
// exception, do NOT allow that to flow up to the
// caller because this is not an exceptional
// circumstance. Rather, it is the normal progression
// in socket transmissions.
try
{
((AbstractAsyncSocket) mAsocket).send(
(BufferWriter) mOutputWriter);
++mMsgOutCount;
++mTotalOutCount;
}
catch (BufferOverflowException bufex)
{
// Ignore.
}
}
return;
} // end of send(EMessageHeader)
//
// end of Abstract Method Implementations.
//-----------------------------------------------------------
//-----------------------------------------------------------
// SocketListener Interface Implementation
//
/**
* The underlying socket is open. Starts the timer and
* inform the listener.
* @param socket The now open socket.
*/
@Override
public final void handleOpen(final AbstractAsyncSocket socket)
{
connected();
return;
} // end of handleOpen(AbstractAsyncSocket)
/**
* Finds the eBus messages in the newly arrived data. If the
* connection was closed prior to this new input arriving,
* the input is ignored. If this input overflows the
* input buffer, the connection is closed an the listener
* informed. Otherwise, {@code data} is appended to the input
* buffer and messages are extracted.
*
* DO NOT USE THIS METHOD!
* This method is part of the {@code SocketListener}
* interface and should not be called by an application.
* @param buffer the incoming bytes.
* @param asocket bytes read from this socket.
*/
@Override
public void handleInput(final ByteBuffer buffer,
final AbstractAsyncSocket asocket)
{
// Are we still connected?
if (mState == ConnectState.CONNECTED)
{
// Yes, carry on.
processInput(buffer);
}
// No, ignore the input but do clear it out.
else
{
final int position = buffer.position();
final int remaining = buffer.remaining();
buffer.position(position + remaining);
}
return;
} // end of handleInput(ByteBuffer, AbstractAsyncSocket)
/**
* The socket's output queue is no longer full and the socket
* is ready to accept output again.
*
* DO NOT USE THIS METHOD!
* This method is part of the {@code SocketListener}
* interface and should not be called by an application.
* @param s The socket making this call.
*/
@Override
public final void handleOutputAvailable(final AbstractAsyncSocket s)
{
// If the async socket throws a buffer overflow
// exception, do NOT allow that to flow up to the
// caller because this is not an exceptional
// circumstance. Rather, it is the normal progression
// in socket transmissions.
try
{
s.send((BufferWriter) mOutputWriter);
}
catch (BufferOverflowException bufx)
{
// Ignore
}
catch (IOException ioex)
{
sLogger.log(
Level.WARNING,
String.format(
"%s: message transmit failed.",
mAsocket.remoteSocketAddress()),
ioex);
}
return;
} // end of handleOutputAvailable(AbstractAsyncSocket)
/**
* Cleans up the connection data due to an unexpected,
* asynchronous socket closing.
*
* DO NOT USE THIS METHOD!
* This method is part of the
* {@code net.sf.eBus.net.SocketListener} interface and should
* not be called by an application.
* @param t The exception causing this disconnect. May be
* {@code null}.
* @param asocket The now disconnected socket.
*/
@Override
public final void handleClose(final Throwable t,
final AbstractAsyncSocket asocket)
{
disconnected(t);
return;
} // end of handleClose(Throwable, AbstractAsyncSocket)
//
// end of SocketListener Interface Implementation
//-----------------------------------------------------------
//-----------------------------------------------------------
// TimeTaskListener Interface Implementation
//
/**
* Handles both the heartbeat and reconnect timer events.
*
* DO NOT USE THIS METHOD!
* This method is part of the {@code TimerTaskListener}
* interface and should not be called by an application.
* @param event The timer event.
*/
@Override
public final void handleTimeout(final TimerEvent event)
{
final TimerTask task = (TimerTask) event.getSource();
if (task.equals(mHeartbeatTimer))
{
heartbeatTimeout();
}
else if (task.equals(mHeartbeatReplyTimer))
{
heartbeatReplyTimeout();
}
else if (task.equals(mReconnectTimer))
{
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format(
"%s: reconnect timer expired.",
mAsocket.remoteSocketAddress()));
}
mReconnectTimer = null;
reconnect();
}
return;
} // end of handleTimeout(TimerEvent)
//
// TimerTaskListener Interface Implementation
//-----------------------------------------------------------
//-----------------------------------------------------------
// Get Methods.
//
/**
* Returns how long this {@code EConnection} will
* wait before attempting to reconnect.
* @return Reconnect delay in milliseconds.
*/
public long reconnectDelay()
{
return (mReconnectDelay.get());
} // end of reconnectDelay()
/**
* Returns how long this {@code EConnection} will
* wait before sending a heartbeat message.
* @return heartbeat frequency in milliseconds.
*/
public long heartbeatDelay()
{
return (mHeartbeatDelay.get());
} // end of heartbeatDelay()
/**
* Returns the heartbeat reply delay.
* @return heartbeat reply delay. in milliseconds.
*/
public long heartbeatReplyDelay()
{
return (mHeartbeatReplyDelay.get());
} // end of heartbeatReplyDelay()
//
// end of Get Methods.
//-----------------------------------------------------------
/**
* Returns a new, closed eBus connection configured according
* to the given configuration.
* @param config remote connection configuration.
* @param remoteApp pass events to this eBus remote
* application interface.
* @return a closed eBus connection.
* @throws NullPointerException
* if either {@code config} or {@code remoteApp} is
* {@code null}.
*
* @see #create(EConfigure.Service, ERemoteApp)
* @see #open(SocketChannel, EConfigure.Service)
* @see #open(SocketAddress, EConfigure.RemoteConnection)
*/
/* package */ static ETCPConnection
create(final EConfigure.RemoteConnection config,
final ERemoteApp remoteApp)
{
final ETCPConnection retval =
new ETCPConnection(remoteApp);
// Initialize the socket, message writer, and message
// readers map.
retval.initialize(config);
return (retval);
} // end of create(RemoteConnection, ERemoteApp)
/**
* Returns a new, closed eBus connection configured according
* to the given configuration.
* @param config accepted connection configuration.
* @param remoteApp pass events to this eBus remote
* application interface.
* @return a closed eBus connection.
* @throws NullPointerException
* if either {@code config} or {@code remoteApp} is
* {@code null}.
*
*
*/
/* package */ static ETCPConnection
create(final EConfigure.Service config,
final ERemoteApp remoteApp)
{
final ETCPConnection retval =
new ETCPConnection(remoteApp);
// Initialize the socket, message writer, and message
// readers map.
retval.initialize(config);
return (retval);
} // end of create(Service, ERemoteApp)
/**
* Immediately closes the open socket connection and starts
* the reconnect process, but only if this connection is
* configured to reconnect. If this connection is not
* configured to reconnect, then closes the connection only.
* Does nothing if the connection is already closed.
* Messages posted for transmission but not completely sent
* are lost.
*/
/* package */ final void closeAndReconnect()
{
if (mAsocket.isOpen())
{
stopHeartbeatTimer();
stopReconnectTimer();
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format(
"%s: re-establishing connection now.",
mAsocket.remoteSocketAddress()));
}
mAsocket.closeNow();
if (mReconnectFlag)
{
mState = ConnectState.RECONNECTING;
startReconnectTimer();
}
else
{
mState = ConnectState.CLOSED;
}
}
return;
} // end of closeAndReconnect()
/**
* If there is no reconnect timer task running, start one.
*/
private void startReconnectTimer()
{
// If reconnecting is turned off or this eBus
// connection was in the process of closing, then
// don't bother reconnecting.
if (!mReconnectFlag ||
mState == ConnectState.CLOSING)
{
// Mark this connection as closed and stop the
// connection thread - but only after delivering
// all remaining events.
mState = ConnectState.CLOSED;
}
else
{
final long delay = mReconnectDelay.get();
mState = ConnectState.RECONNECTING;
// Stop the current timer if running.
if (mReconnectTimer != null)
{
mReconnectTimer.cancel();
mReconnectTimer = null;
}
mReconnectTimer = new TimerTask(this);
mTimer.schedule(mReconnectTimer, delay);
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format(
"%s: started reconnect timer, delay %,d millis.",
mAsocket.remoteSocketAddress(),
delay));
}
}
return;
} // end of startReconnectTimer()
/**
* Stop the reconnect timer if it is running.
*/
private void stopReconnectTimer()
{
if (mReconnectTimer != null)
{
mReconnectTimer.cancel();
mReconnectTimer = null;
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format(
"%s: reconnect timer stopped.",
mAsocket.remoteSocketAddress()));
}
}
return;
} // end of stopReconnectTimer()
/**
* Informs the listener that the connection is open. Turns
* off Nagle's TCP delay algorithm and starts the heartbeat
* time along the way.
*/
private void connected()
{
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format("%s: connected.",
mAsocket.remoteSocketAddress()));
}
// Turn off Nagle's algorithm which delays TCP transmit.
try
{
mAsocket.setOption(
StandardSocketOptions.TCP_NODELAY, true);
}
catch (IOException ioex)
{
// Ignore.
}
mState = ConnectState.CONNECTED;
startHeartbeatTimer();
// Let the listener know that we are open and ready for
// business.
try
{
final EAbstractConnection abConn = this;
(CONN_CB[OPEN_CB]).invokeExact(mRemoteApp, abConn);
}
catch (Throwable tex)
{
sLogger.log(
Level.WARNING,
CONN_CB_METHOD_NAMES[OPEN_CB] + "exception",
tex);
}
return;
} // end of connected()
/**
* {@code asocket} is disconnected. Stop all timers and
* inform the listener. If set to reconnect, then start
* the reconnect timer.
* @param t The disconnection cause.
*/
private void disconnected(final Throwable t)
{
if (sLogger.isLoggable(Level.FINER))
{
sLogger.log(
Level.FINER,
String.format("%s: closed.",
mAsocket.remoteSocketAddress()),
t);
}
// Stop any and all timers.
stopReconnectTimer();
stopHeartbeatTimer();
// Tell the listener about this sorry state of affairs.
try
{
final EAbstractConnection abConn = this;
(CONN_CB[CLOSE_CB]).invokeExact(mRemoteApp, abConn);
}
catch (Throwable tex)
{
sLogger.log(
Level.WARNING,
CONN_CB_METHOD_NAMES[CLOSE_CB] + "exception",
tex);
}
// Reconnect if configured to do so.
startReconnectTimer();
return;
} // end of disconnected(Throwable)
/**
* Forwards a Java exception to the connection listeners.
* @param t The exception to be passed to the listeners.
*/
private void errorCallback(final Throwable t)
{
if (sLogger.isLoggable(Level.FINE))
{
sLogger.log(
Level.FINE,
String.format(
"%s: input processing error",
mAsocket.remoteSocketAddress()),
t);
}
return;
} // end of errorCallback(Throwable)
/**
* Attempts to connect to the remote eBus.
*/
private void reconnect()
{
// Has anything changed while waiting to reconnect?
if (mState != ConnectState.CLOSING &&
mState != ConnectState.CLOSED)
{
final SocketAddress address =
mAsocket.remoteSocketAddress();
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format(
"%s: attempting to reconnect.",
address));
}
try
{
if (((AbstractAsyncSocket) mAsocket).open(address, mBindPort))
{
connected();
}
else
{
mState = ConnectState.CONNECTING;
}
}
catch (IOException ioex)
{
if (sLogger.isLoggable(Level.FINE))
{
sLogger.log(
Level.FINE,
String.format(
"%s: connect attempt failed.", address),
ioex);
}
// Connect attempt failed. Set timer and retry
// connection later.
startReconnectTimer();
}
}
// Yes, something did change: the eBus connection was
// closed.
return;
} // end of reconnect()
/**
* Performs the actual work of closing an eBus connection
* except the underlying socket connection.
*/
private void closeConnection()
{
stopHeartbeatTimer();
mReconnectFlag = false;
stopReconnectTimer();
return;
} // end of closeConnection()
/**
* Sends a heartbeat message to the far-end.
*/
private void heartbeatTimeout()
{
if (sLogger.isLoggable(Level.FINE))
{
sLogger.finer(
String.format("%s: heartbeat timer expired.",
mAsocket.remoteSocketAddress()));
}
mHeartbeatTimer = null;
mHeartbeatReplyFlag = true;
// If the send fails, what's the point in starting
// the reply timer?
try
{
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format(
"%s: sending heartbeat.",
mAsocket.remoteSocketAddress()));
}
((AbstractAsyncSocket) mAsocket).send(
HEARTBEAT_DATA, 0, HEARTBEAT_DATA.length);
startHeartbeatTimer();
}
catch (IOException ioex)
{
sLogger.log(Level.WARNING,
String.format(
"%s: heartbeat send failed.",
mAsocket.remoteSocketAddress()),
ioex);
disconnected(ioex);
}
return;
} // end of heartbeatTimeout()
/**
* Closes the connect due to the remote eBus failing to
* respond to a heartbeat.
*/
private void heartbeatReplyTimeout()
{
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format(
"%s: heartbeat reply timer expired.",
mAsocket.remoteSocketAddress()));
}
mHeartbeatReplyTimer = null;
// We are no longer waiting for a reply.
mHeartbeatReplyFlag = false;
// We have timed out waiting for the far-end to reply
// to our heartbeat. Something is very wrong.
// Close down this connection.
mAsocket.closeNow();
disconnected(new IOException("no heartbeat reply"));
return;
} // end of heartbeatReplyTimeout()
/**
* Starts either the heartbeat or the heartbeat reply timer
* depending on whether we are expecting a heartbeat reply
* or not. It is also depends on whether heartbeating is
* turned on.
*/
private void startHeartbeatTimer()
{
long delay = mHeartbeatDelay.get();
// Turn on the heartbeat timer if:
// 1. heartbeating is on.
// 2. we are *not* waiting for a heartbeat reply.
// 3. the heartbeat timer is not already running.
if (delay > 0 &&
!mHeartbeatReplyFlag &&
mHeartbeatTimer == null)
{
mHeartbeatTimer = new TimerTask(this);
mTimer.schedule(mHeartbeatTimer, delay);
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format(
"%s: started heartbeat timer, delay %,d millis.",
mAsocket.remoteSocketAddress(),
delay));
}
}
// Turn on the heartbeat reply timer if:
// 1. heartbeating is on.
// 2. we are waiting for a heartbeat reply.
// 3. the heartbeat reply timer is not already
// running
else if (delay > 0 &&
mHeartbeatReplyFlag &&
mHeartbeatReplyTimer == null)
{
delay = mHeartbeatReplyDelay.get();
mHeartbeatReplyTimer = new TimerTask(this);
mTimer.schedule(mHeartbeatReplyTimer, delay);
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format(
"%s: started heartbeat reply timer, delay %,d millis.",
mAsocket.remoteSocketAddress(),
delay));
}
}
return;
} // end of startHeartbeatTimer()
/**
* Stops whichever heartbeat timer is running or both
* (which should never be the case).
*/
private void stopHeartbeatTimer()
{
if (mHeartbeatTimer != null)
{
mHeartbeatTimer.cancel();
}
if(mHeartbeatReplyTimer != null)
{
mHeartbeatReplyTimer.cancel();
mHeartbeatReplyTimer = null;
}
return;
} // end of stopHeartbeatTimer()
/**
* Extract the eBus messages from the buffer and forward them
* to the remote eBus application connection.
* @param buffer extract the messages from this buffer.
*/
private void processInput(final ByteBuffer buffer)
{
int remaining;
int startPosition;
int messageSize = -1;
boolean hbReplyFlag = false;
int keyId = Integer.MIN_VALUE;
MessageReader reader;
EMessageHeader header;
// Stop whichever heart timer is running while processing
// input.
stopHeartbeatTimer();
// The fact that we received input of any kind serves
// purpose of a heartbeat reply. We know that the
// connection is still alive. So turn off the heartbeat
// reply flag.
mHeartbeatReplyFlag = false;
if (sLogger.isLoggable(Level.FINEST))
{
final int rem = buffer.remaining();
final int pos = buffer.position();
final int ms = buffer.getInt(pos);
final byte[] data = new byte[rem];
buffer.mark();
buffer.get(data);
buffer.reset();
sLogger.finest(
String.format(
"%s: %,d bytes available (start=%,d, msg size=%,d):%n%s",
mAsocket.remoteSocketAddress(),
rem,
pos,
ms,
HexDump.dump(data, 0, rem, " ")));
}
else if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format(
"%s: %,d bytes available.",
mAsocket.remoteSocketAddress(),
buffer.remaining()));
}
// Iterate over the buffer until either it is
// entirely consumed or it contains a partial
// serialized message.
for (startPosition = buffer.position(),
remaining = buffer.remaining(),
buffer.mark();
remaining >= 4 &&
(messageSize = buffer.getInt()) <= remaining;
startPosition = buffer.position(),
remaining = buffer.remaining(),
buffer.mark())
{
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format(
"%s: %,d bytes remaining, position is %,d, message size is %,d bytes.",
mAsocket.remoteSocketAddress(),
remaining,
startPosition,
messageSize));
}
// Heartbeat and heartbeat reply messages are
// simply two-byte integers that are < 0.
if (messageSize == HEARTBEAT)
{
hbReplyFlag = true;
}
else if (messageSize == HEARTBEAT_REPLY)
{
// That's nice. A reply to our heartbeat.
// But the heartbeat flag is already turned off.
// So there is nothing else to do.
}
// The message size must be at least big enough for
// the message header or is within the maximum
// allowed message size. If not, disconnect; there is
// not way to recover from this protocol error.
else if (messageSize < MESSAGE_HEADER_SIZE ||
messageSize > MAX_MESSAGE_SIZE)
{
errorCallback(
new ProtocolException(
"invalid message size - " +
Integer.toString(messageSize)));
mAsocket.closeNow();
disconnected(
new IOException("invalid message size"));
}
else
{
try
{
// Have the reader parse the message and
// forward it to its callback.
keyId = buffer.getInt();
reader = mInputReaders.get(keyId);
header =
reader.extractMessage(
buffer,
mAsocket.remoteSocketAddress());
reader.forwardMessage(header, mRemoteApp);
}
catch (NullPointerException nullex)
{
sLogger.log(Level.WARNING,
"received unsupported key ID " +
keyId,
nullex);
}
catch (UnknownMessageException |
InvalidMessageException |
BufferUnderflowException jex)
{
errorCallback(jex);
}
finally
{
// Whether the message was successfully
// deserialized or not, move to past this
// message and to the start of the next.
startPosition += messageSize;
buffer.position(startPosition);
}
}
}
// In case the input buffer contains an incomplete
// message, reset to the mark.
buffer.reset();
// Do we need to send a heartbeat reply?
if (hbReplyFlag)
{
// Yes, send the reply on its way.
try
{
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format(
"%s: sending heartbeat reply.",
mAsocket.remoteSocketAddress()));
}
((AbstractAsyncSocket) mAsocket).send(
HEARTBEAT_REPLY_DATA,
0,
HEARTBEAT_REPLY_DATA.length);
}
catch (BufferOverflowException | IOException bufex)
{
// Ignore.
// Why?
// Because a full output buffer means that we
// have output waiting to be sent and that output
// will serve as a heartbeat reply. See the top
// of this method.
// On the other hand, if the other side is
// sending us a heartbeat while our output buffer
// is full up, then something is desparately
// wrong. We're pitching bytes and they keep
// missing them. If max output message queue is
// set, we will likely hit that max and close
// the connection. If not set, then this JVM will
// run out of heap and crash.
}
}
// Now that we have finished processing input,
// start the appropriate heartbeat timer.
startHeartbeatTimer();
return;
} // end of processInput(ByteBuffer)
/**
* The output message writer is reporting that the outbound
* message queue is empty. If this connection is waiting on
* this condition to complete a slow close, then complete it
* now.
*/
private void outboundQueueEmpty()
{
// Is this eBus connection in the process of closing?
if (mState == ConnectState.CLOSING)
{
// Yes, complete the closing process by closing the
// socket.
mState = ConnectState.CLOSED;
if (mAsocket.isOpen())
{
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format(
"%s: closing connection.",
mAsocket.remoteSocketAddress()));
}
mAsocket.close();
}
}
return;
} // end of outboundQueueEmpty()
/**
* Initializes the {@link AsyncSocket TCP socket} and
* {@link MessageWriter message writer} instances. This is
* done outside the constructor because both classes require
* {@code this} as a constructor parameter.
* @param config remote connection configuration.
*/
private void initialize(final EConfigure.RemoteConnection config)
{
switch (config.connectionType())
{
case TCP:
mAsocket = createPlainTextTCP(config);
break;
case SECURE_TCP:
mAsocket = createSecureTCP(config);
break;
}
mOutputWriter =
new MessageWriter(config.messageQueueSize(), this);
// Initialize the input readers.
super.initialize();
return;
} // end of initialize(RemoteConnection)
/**
* Returns a plain text TCP connection based on the given
* remote connection configuration.
* @param config remote connection configuration.
* @return un-secure TCP connection.
*/
private AsyncChannel createPlainTextTCP(final EConfigure.RemoteConnection config)
{
final SocketBuilder builder = AsyncSocket.builder();
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine("Creating plain text TCP connection.");
}
return (builder.inputBufferSize(config.inputBufferSize())
.outputBufferSize(config.outputBufferSize())
.byteOrder(config.byteOrder())
.selector(config.selector())
.listener(this)
.build());
} // end of createPlainTextTCP(EConfigure.RemoteConnection)
/**
* Returns an SSL/TLS secure TCP connection based on the
* given remote connection configuration.
* @param config remote connection configuration.
* @return secure TCP connection.
*/
private AsyncChannel createSecureTCP(final EConfigure.RemoteConnection config)
{
final SecureSocketBuilder builder =
AsyncSecureSocket.builder();
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine("Creating secure TCP connection.");
}
return (builder.sslContext(config.sslContext())
.inputBufferSize(config.inputBufferSize())
.outputBufferSize(config.outputBufferSize())
.byteOrder(config.byteOrder())
.selector(config.selector())
.listener(this)
.build());
} // end of createSecureTCP(EConfigure.RemoteConnection)
/**
* Initializes the {@link AsyncSocket TCP socket} and
* {@link MessageWriter message writer} instances. This is
* done outside the constructor because both classes require
* {@code this} as a constructor parameter.
* @param config accepted connection configuration.
*/
private void initialize(final EConfigure.Service config)
{
switch (config.connectionType())
{
case TCP:
mAsocket = createPlainTextTCP(config);
break;
case SECURE_TCP:
mAsocket = createSecureTCP(config);
break;
}
mOutputWriter =
new MessageWriter(config.messageQueueSize(), this);
// Initialize the input readers.
super.initialize();
return;
} // end of initialize(Service)
/**
* Returns a plain text TCP connection based on the given
* remote connection configuration.
* @param config remote connection configuration.
* @return un-secure TCP connection.
*/
private AsyncChannel createPlainTextTCP(final EConfigure.Service config)
{
final SocketBuilder builder = AsyncSocket.builder();
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine("Creating plain text TCP connection.");
}
return (builder.inputBufferSize(config.inputBufferSize())
.outputBufferSize(config.outputBufferSize())
.byteOrder(config.byteOrder())
.selector(config.connectionSelector())
.listener(this)
.build());
} // end of createPlainTextTCP(EConfigure.Service)
/**
* Returns an SSL/TLS secure TCP connection based on the
* given remote connection configuration.
* @param config remote connection configuration.
* @return secure TCP connection.
*/
private AsyncChannel createSecureTCP(final EConfigure.Service config)
{
final SecureSocketBuilder builder =
AsyncSecureSocket.builder();
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine("Creating secure TCP connection.");
}
return (builder.sslContext(config.sslContext())
.inputBufferSize(config.inputBufferSize())
.outputBufferSize(config.outputBufferSize())
.byteOrder(config.byteOrder())
.selector(config.connectionSelector())
.listener(this)
.build());
} // end of createSecureTCP(EConfigure.Service)
//---------------------------------------------------------------
// Inner classes.
//
/**
* This class is responsible for serializing messages
* directly to a given {@link AsyncSocket socket}
* {@link ByteBuffer output buffer}.
*/
private static final class MessageWriter
extends AbstractMessageWriter
implements BufferWriter
{
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
/**
* Creates a message writer instance with the given
* maximum outbound message queue size and eBus
* connection.
* @param maxSize maximum outbound message queue size.
* @param connection eBus connection.
*/
public MessageWriter(final int maxSize,
final ETCPConnection connection)
{
super (maxSize, connection);
} // end of MessageWriter(int, EConnection)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// BufferWriter Interface Implementations.
//
/**
* Serializes as many queued messages as possible. This
* loop will terminate when either the message queue is
* empty or {@code buffer} overflows. If an overflow
* occurs, then
* {@link #handleOutputAvailable(AsyncSocket)} will pass
* this message writer back to the socket so that the
* remaining messages may be serialized.
*
* The serialized information consists of the from proxy
* identifier, the to proxy identifier, and the eBus
* message.
*
* @param buffer the output buffer.
* @throws BufferOverflowException
* if a message serialization overflows {@code buffer}
* available bytes.
*/
@Override
public void fill(final ByteBuffer buffer)
throws BufferOverflowException
{
EMessageHeader header;
DataType msgType;
int sizePosition;
int queueSize;
if (_sublogger.isLoggable(Level.FINEST))
{
_sublogger.finest(
String.format(
"%s queue: sending messages (size=%,d, remaining=%,d).",
_connection.remoteSocketAddress(),
_transmitQueueSize.get(),
buffer.remaining()));
}
while (!_transmitQueue.isEmpty())
{
// If the buffer does not contain sufficient
// space to store the message header, then throw
// a buffer overflow exception because setting
// the buffer position past its limit results in
// an illegal argument exception.
if (buffer.remaining() < MESSAGE_HEADER_SIZE)
{
throw (new BufferOverflowException());
}
// Do not remove this message from the queue
// until successfully transmitted.
header = _transmitQueue.peek();
msgType =
DataType.findType(header.messageClass());
// While AsyncSocket marks the buffer before
// making this call, we still need to mark the
// buffer again before serializing each message.
// If we did not do that, all the successfully
// serialized messages before the buffer overflow
// would be lost.
buffer.mark();
// Remember where the message size is written so
// we can come back there and write the size.
sizePosition = buffer.position();
buffer.position(
sizePosition + MESSAGE_SIZE_SIZE);
// Serialize the header then the message.
buffer.putInt(header.classId())
.putInt(header.fromFeedId())
.putInt(header.toFeedId());
msgType.serialize(header.message(), buffer);
// Now write out the message size.
buffer.putInt(
sizePosition,
(buffer.position() - sizePosition));
// Now dequeue the message - one way or another
// we are finished with it.
_transmitQueue.poll();
// Decrement the transmit queue size
// if-and-only-if the message was *not* system.
if (header.messageType() ==
EMessage.MessageType.SYSTEM)
{
queueSize = _transmitQueueSize.get();
}
else
{
queueSize =
_transmitQueueSize.decrementAndGet();
}
++_transmitCount;
if (_sublogger.isLoggable(Level.FINEST))
{
_sublogger.finest(
String.format(
"%s: queued message sent (size=%,d, transmited=%,d, discarded=%,d).",
_connection.remoteSocketAddress(),
queueSize,
_transmitCount,
_discardCount));
}
}
// Is the eBus connection doing a slow close?
// Note: the message queue is empty at this point
// because we wouldn't get here if a buffer overflow
// happened.
if (_closingFlag)
{
// Yes, report the outbound queue being empty.
((ETCPConnection) _connection).outboundQueueEmpty();
}
return;
} // end of fill(ByteBuffer)
//
// end of BufferWriter Interface Implementations.
//-------------------------------------------------------
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Statics.
//
/**
* Logging subsystem interface.
*/
private static final Logger _sublogger =
Logger.getLogger(MessageWriter.class.getName());
} // end of class MessageWriter
/**
* This event informs the {@link ERemoteApp} that the
* eBus connection is up.
*/
/* package */ static final class ConnectEvent
{
//---------------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
/**
* Creates a new connect callback task.
*/
public ConnectEvent()
{}
//
// end of Constructors.
//-------------------------------------------------------
//-----------------------------------------------------------
// Member data.
//
} // end of class ConnectEvent
/**
* Informs the {@code ERemoteApp} that the eBus connection
* is down and contains the (possibly {@code null}) exception
* associated with the disconnect.
*/
/* package */ static final class DisconnectEvent
{
//---------------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
/**
* Creates a new disconnect event.
* @param t the disconnect cause. May be {@code null}.
*/
private DisconnectEvent(final Throwable t)
{
_t = t;
} // end of DisconnectEvent(Throwable)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Get Methods.
//
/**
* Returns the exception associated with the disconnect.
* May return {@code null}.
* @return the disconnect exception.
*/
/* package */ Throwable exception()
{
return (_t);
} // end of exception()
//
// end of Get Methods.
//-------------------------------------------------------
//-----------------------------------------------------------
// Member data.
//
/**
* The exception associated with the disconnect.
*/
private final Throwable _t;
} // end of class DisconnectEvent
} // end of class ETCPConnection