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

net.sf.eBus.client.ETCPConnection Maven / Gradle / Ivy

There is a newer version: 7.6.0
Show newest version
//
// Copyright 2015, 2016 Charles W. Rapp
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

package net.sf.eBus.client;

import java.io.IOException;
import java.net.InetSocketAddress;
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 net.sf.eBus.config.EConfigure;
import net.sf.eBus.config.EConfigure.AbstractConfig;
import net.sf.eBus.config.EConfigure.ConnectionRole;
import net.sf.eBus.config.EConfigure.ConnectionType;
import net.sf.eBus.config.EConfigure.RemoteConnection;
import net.sf.eBus.config.EConfigure.Service;
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.LazyString;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 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
{
//---------------------------------------------------------------
// Member data.
//

    //-----------------------------------------------------------
    // Statics.
    //

    /**
     * Access point for logging.
     */
    private static final Logger sLogger =
        LoggerFactory.getLogger(ETCPConnection.class);

//---------------------------------------------------------------
// 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 connType remote connection type.
     * @param connRole either accepted or initiated.
     * @param remoteApp pass events to this eBus remote
     * application interface.
     * @throws IllegalArgumentException
     * if {@code remoteApp} is {@code null}.
     */
    private ETCPConnection(final ConnectionType connType,
                           final ConnectionRole connRole,
                           final ERemoteApp remoteApp)
    {
        super (connType, connRole, remoteApp, true);
    } // end of ETCPConnection(...)

    //
    // end of Constructors.
    //-----------------------------------------------------------

    //-----------------------------------------------------------
    // Abstract Method Implementations.
    //

    /**
     * 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.
     */
    @Override
    protected void doInitialize(final RemoteConnection config)
    {
        switch (config.connectionType())
        {
            case TCP:
                mAsocket = createClearTextTCP(config);
                break;

            case SECURE_TCP:
                mAsocket = createSecureTCP(config);
                break;

            default:
        }
    } // end of doInitialize(RemoteConnection)

    /**
     * Initializes a newly accepted remote connection with the
     * given service configuration.
     * @param config initialize connection with this
     * configuration.
     */
    @Override
    protected void doInitialize(final Service config)
    {
        switch (config.connectionType())
        {
            case TCP:
                mAsocket = createClearTextTCP(config);
                break;

            case SECURE_TCP:
                mAsocket = createSecureTCP(config);
                break;

            default:
        }
    } // end of doInitialize(Service)

    /**
     * Returns the output writer for this connection type.
     * @param queueSize maximum message queue size for this
     * writer.
     * @return connection output writer.
     */
    @Override
    protected AbstractMessageWriter createWriter(final AbstractConfig config)
    {
        return (new MessageWriter(config, this));
    } // end of createWriter(AbstractConfig)

    /**
     * Closes the TCP socket connection if open. Performs a
     * "slow" close on the socket which means the socket closes
     * only after any and all pending output is transmitted.
     *
     * @see #doCloseNow()
     * @see #doOpen(RemoteConnection)
     * @see #doOpen(SelectableChannel)
     */
    @Override
    protected void doClose()
    {
        if (mAsocket.isOpen())
        {
            sLogger.debug("{}: closing connection.",
                          mRemoteAddress);

            mAsocket.close();
        }
    } // end of doClose()

    /**
     * Closes the TCP socket connection immediately if open.
     * Performs a "fast" socket close which means all pending
     * output is discarded and not transmitted before closing.
     *
     * @see #doOpen(RemoteConnection)
     * @see #doOpen(SelectableChannel)
     * @see #doClose()
     */
    @Override
    protected void doCloseNow()
    {
        if (mAsocket.isOpen())
        {
            sLogger.debug("{}: closing connection now.",
                          mRemoteAddress);

            mAsocket.closeNow();
        }
    } // end of doCloseNow()

    /**
     * Establishes a TCP socket connection to the given address
     * and bind port. Returns {@code true} if the
     * connection is immediately established and {@code false} if
     * the connection is in-progress. If {@code false} is
     * returned, then {@link #handleOpen(AbstractAsyncSocket)}
     * will be called when the connection process completes.
     * @param config remote connection configuration.
     * @return {@code true} if connection is established and
     * {@code false} if the connection is in-progress.
     * @exception IOException
     * if socket connection attempt threw an exception and
     * {@code reconnectFlag} is {@code false}.
     *
     * @see #doOpen(SelectableChannel)
     * @see #doClose()
     * @see #doCloseNow()
     */
    @Override
    protected boolean doOpen(final SocketAddress address,
                             final SocketAddress bindAddress)
        throws IOException
    {
        return (
            ((AbstractAsyncSocket) mAsocket).open(
                address, bindAddress));
    } // end of doOpen(SocketAddress, SocketAddress)

    /**
     * Encapsulates an already connected socket accept by an
     * {@link ETCPServer} instance. Will not reconnect
     * if the connection is lost.
     * @param socket the accepted socket connection.
     * @return {@code true} because this TCP socket is fully
     * connected once opened.
     * @exception IOException
     * if {@code socket} is closed.
     *
     * @see #doOpen(InetSocketAddress, int)
     * @see #doClose()
     * @see #doCloseNow()
     */
    @Override
    protected boolean doOpen(final SelectableChannel socket)
        throws IOException
    {
        ((AbstractAsyncSocket) mAsocket).open(socket);

        return (true);
    } // end of doOpen(SelectableChannel)

    /**
     * Turns off Nagle's algorithm on the TCP socket which
     * delays TCP transmit.
     * @throws IOException
     * if an I/O failure occurs while configuring the socket.
     */
    @Override
    protected void doConnected()
        throws IOException
    {
        mAsocket.setOption(
            StandardSocketOptions.TCP_NODELAY, true);
    } // end of doConnected()

    /**
     * Returns {@code true} if connected.
     * @return {@code true} if clear to transmit a message.
     */
    @Override
    protected boolean maySend()
    {
        return (mState.get() == ConnectState.CONNECTED);
    } // end of maySend()

    /**
     * Sends message stored in {@code writer} 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 writer outbound messages are stored in this writer.
     * @exception IOException
     * if there is an I/O failure when transmitting the message.
     */
    @Override
    protected void doSend(final AbstractMessageWriter writer)
        throws IOException
    {
        ((AbstractAsyncSocket) mAsocket).send(
            (BufferWriter) writer);
    } // end of doSend(AbstractMessageWriter)

    /**
     * Sends the heartbeat bytes to the far-end.
     * @throws IOException
     * if an I/O failure occurs when transmitting the heartbeat
     * bytes.
     */
    @Override
    protected void doSendHeartbeat()
        throws IOException
    {
        ((AbstractAsyncSocket) mAsocket).send(
            mHeartbeatData, 0, mHeartbeatData.length);
    } // end of doSendHeartbeat()

    //
    // 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();
    } // 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.get() == 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); } } // 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.warn("{}: message transmit failed.", mRemoteAddress, ioex); } } // 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); } // end of handleClose(Throwable, AbstractAsyncSocket) // // end of SocketListener Interface Implementation //----------------------------------------------------------- /** * 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 RemoteConnection config, final ERemoteApp remoteApp) { final ETCPConnection retval = new ETCPConnection(config.connectionType(), ConnectionRole.INITIATOR, 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 Service config, final ERemoteApp remoteApp) { final ETCPConnection retval = new ETCPConnection(config.connectionType(), ConnectionRole.ACCEPTOR, remoteApp); // Initialize the socket, message writer, and message // readers map. retval.initialize(config); return (retval); } // end of create(Service, ERemoteApp) /** * Extract the eBus messages from the buffer and forward them * to the remote eBus application connection. * @param buffer extract the messages from this buffer. */ @SuppressWarnings({"java:S3776", "java:S3457"}) 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.isTraceEnabled()) { final int rem = buffer.remaining(); final int pos = buffer.position(); final int ms = (rem >= Integer.BYTES ? buffer.getInt(pos) : -1); final byte[] data = new byte[rem]; buffer.mark(); buffer.get(data); buffer.reset(); sLogger.trace( "{}: {} bytes available (start={}, msg size={}):\n{}", mRemoteAddress, rem, pos, ms, new LazyString( () -> HexDump.dump(data, 0, rem, " "))); } else { sLogger.debug("{}: {} bytes available.", mRemoteAddress, 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 >= Integer.BYTES && (messageSize = buffer.getInt()) <= remaining; startPosition = buffer.position(), remaining = buffer.remaining(), buffer.mark()) { sLogger.trace( "{}: {} bytes remaining, position is {}, message size is {} bytes.", mRemoteAddress, 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) { logError( 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, mRemoteAddress); reader.forwardMessage(header, mRemoteApp); } catch (NullPointerException nullex) { sLogger.warn( "received unsupported key ID {}", keyId, nullex); } catch (UnknownMessageException | InvalidMessageException | BufferUnderflowException jex) { logError(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 { sLogger.trace("{}: sending heartbeat reply.", mRemoteAddress); ((AbstractAsyncSocket) mAsocket).send(mHeartbeatReplyData, 0, mHeartbeatReplyData.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(); } // 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. */ @SuppressWarnings({"java:S3398"}) private void outboundQueueEmpty() { // Is this eBus connection in the process of closing? if (mState.get() == ConnectState.CLOSING) { // Yes, complete the closing process by closing the // socket. setState(ConnectState.CLOSED); if (mAsocket.isOpen()) { sLogger.debug("{}: closing connection.", mRemoteAddress); mAsocket.close(); } } } // end of outboundQueueEmpty() /** * Returns a clear text TCP connection based on the given * remote connection configuration. * @param config remote connection configuration. * @return un-secure TCP connection. */ private AsyncChannel createClearTextTCP(final RemoteConnection config) { final SocketBuilder builder = AsyncSocket.builder(); sLogger.debug( "Creating clear text TCP client connection."); return (builder.inputBufferSize(config.inputBufferSize()) .outputBufferSize(config.outputBufferSize()) .byteOrder(config.byteOrder()) .selector(config.selector()) .listener(this) .build()); } // end of createClearTextTCP(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 RemoteConnection config) { final SecureSocketBuilder builder = AsyncSecureSocket.builder(); sLogger.debug( "Creating secure TCP client connection."); return (builder.sslContext(config.sslContext()) .inputBufferSize(config.inputBufferSize()) .outputBufferSize(config.outputBufferSize()) .byteOrder(config.byteOrder()) .selector(config.selector()) .listener(this) .build()); } // end of createSecureTCP(RemoteConnection) /** * Returns a clear text TCP connection based on the given * local service configuration. * @param config local service configuration. * @return un-secure TCP connection. */ // Yes this method is used. @SuppressWarnings({"java:S1144"}) private AsyncChannel createClearTextTCP(final Service config) { final SocketBuilder builder = AsyncSocket.builder(); sLogger.debug( "Creating clear text TCP accepted connection."); return (builder.inputBufferSize(config.inputBufferSize()) .outputBufferSize(config.outputBufferSize()) .byteOrder(config.byteOrder()) .selector(config.connectionSelector()) .listener(this) .build()); } // end of createClearTextTCP(Service) /** * Returns an SSL/TLS secure TCP connection based on the * given remote connection configuration. * @param config remote connection configuration. * @return secure TCP connection. */ // Yes this method is used. @SuppressWarnings({"java:S1144"}) private AsyncChannel createSecureTCP(final Service config) { final SecureSocketBuilder builder = AsyncSecureSocket.builder(); sLogger.debug( "Creating secure TCP accepted connection."); return (builder.sslContext(config.sslContext()) .inputBufferSize(config.inputBufferSize()) .outputBufferSize(config.outputBufferSize()) .byteOrder(config.byteOrder()) .selector(config.connectionSelector()) .listener(this) .build()); } // end of createSecureTCP(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 data. // //------------------------------------------------------- // Statics. // /** * Logging subsystem interface. */ private static final Logger sSublogger = LoggerFactory.getLogger(MessageWriter.class); //----------------------------------------------------------- // Member methods. // //------------------------------------------------------- // Constructors. // /** * Creates a message writer instance with the given * maximum outbound message queue size and eBus * connection. * @param config connection configuration. * @param connection eBus connection. */ public MessageWriter(final AbstractConfig config, final ETCPConnection connection) { super (config, connection); } // end of MessageWriter(AbstractConfig, EConnection) // // end of Constructors. //------------------------------------------------------- //------------------------------------------------------- // BufferWriter Interface Implementations. // /** * Returns {@code true} if there are messages in the * transmit queue waiting to be transmitted; otherwise * returns {@code false}. * @return {@code true} if there are messages in the * transmit queue. */ @Override public boolean hasOutput() { return (!mTransmitQueue.isEmpty()); } // end of hasOutput() /** * 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; sSublogger.trace( "{} queue: sending messages (size={}, remaining={}).", mConnection.remoteSocketAddress(), mTransmitQueueSize.get(), buffer.remaining()); while (!mTransmitQueue.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 = mTransmitQueue.peek(); msgType = header.dataType(); // 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(), null, 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. mTransmitQueue.poll(); // Decrement the transmit queue size // if-and-only-if the message was *not* system. if (header.messageType() == EMessage.MessageType.SYSTEM) { queueSize = mTransmitQueueSize.get(); } else { queueSize = mTransmitQueueSize.decrementAndGet(); } ++mTransmitCount; sSublogger.trace( "{}: queued message sent (size={}, transmited={}, discarded={}).", mConnection.remoteSocketAddress(), queueSize, mTransmitCount, mDiscardCount); } // 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 (mClosingFlag) { // Yes, report the outbound queue being empty. ((ETCPConnection) mConnection).outboundQueueEmpty(); } } // end of fill(ByteBuffer) // // end of BufferWriter Interface Implementations. //------------------------------------------------------- } // end of class MessageWriter } // end of class ETCPConnection




© 2015 - 2025 Weber Informatics LLC | Privacy Policy