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

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




© 2015 - 2025 Weber Informatics LLC | Privacy Policy