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

com.crankuptheamps.client.TCPTransportImpl Maven / Gradle / Ivy

///////////////////////////////////////////////////////////////////////////
//
// Copyright (c) 2010-2020 60East Technologies Inc., All Rights Reserved.
//
// This computer software is owned by 60East Technologies Inc. and is
// protected by U.S. copyright laws and other laws and by international
// treaties.  This computer software is furnished by 60East Technologies
// Inc. pursuant to a written license agreement and may be used, copied,
// transmitted, and stored only in accordance with the terms of such
// license agreement and with the inclusion of the above copyright notice.
// This computer software or any other copies thereof may not be provided
// or otherwise made available to any other person.
//
// U.S. Government Restricted Rights.  This computer software: (a) was
// developed at private expense and is in all respects the proprietary
// information of 60East Technologies Inc.; (b) was not developed with
// government funds; (c) is a trade secret of 60East Technologies Inc.
// for all purposes of the Freedom of Information Act; and (d) is a
// commercial item and thus, pursuant to Section 12.212 of the Federal
// Acquisition Regulations (FAR) and DFAR Supplement Section 227.7202,
// Government's use, duplication or disclosure of the computer software
// is subject to the restrictions set forth by 60East Technologies Inc..
//
////////////////////////////////////////////////////////////////////////////

package com.crankuptheamps.client;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.UnknownHostException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.URI;
import java.beans.ExceptionListener;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedByInterruptException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import javax.net.SocketFactory;
import java.net.SocketTimeoutException;

import com.crankuptheamps.client.exception.AlreadyConnectedException;
import com.crankuptheamps.client.exception.ConnectionRefusedException;
import com.crankuptheamps.client.exception.DisconnectedException;
import com.crankuptheamps.client.exception.RetryOperationException;
import com.crankuptheamps.client.exception.InvalidURIException;

/**
 * Socket implementation for {@link TCPTransport}. TCP Transport delegates to an instance of this class. 
 * This is used internally by the client's transport. There is usually no reason to make direct use of it.
 * Its functionality is best accessed via the client instance. 
 */
public class TCPTransportImpl
{
    private          URI               _addr              = null;
    protected        Socket            _socket            = null;
    protected        InputStream       _inputStream       = null;
    protected        OutputStream      _outputStream      = null;
    public final     Lock              _lock              = new ReentrantLock();

    volatile         int               _connectionVersion = 0;
    private volatile boolean           _disconnecting     = false;

    private          Protocol          _messageType       = null;
    private          MessageHandler    _onMessage         = DefaultMessageHandler.instance;
    private TransportDisconnectHandler _onDisconnect      = DefaultDisconnectHandler.instance;
    private          TCPReaderThread   _readerThread      = null;
    private          ExceptionListener _exceptionListener = null;
    private          Properties        _properties        = null;
    protected        TransportFilter   _filter            = null;
    protected        AMPSRunnable      _idleRunnable      = null;
    private          int               _readTimeout       = 0;
    private          int               _connectTimeout    = 0;
    private    ThreadCreatedHandler _threadCreatedHandler = null;

    public TCPTransportImpl(Protocol messageType, Properties properties, TransportFilter filter)
    {
        this._messageType    = messageType;
        this._properties     = properties;
        this._filter         = filter;
        this._readTimeout    = TCPTransport.getDefaultReadTimeout();
        this._connectTimeout = TCPTransport.getDefaultConnectTimeout();
    }

    public void setMessageHandler(MessageHandler h)
    {
        this._onMessage = h;
    }

    public void setDisconnectHandler(TransportDisconnectHandler h)
    {
        this._onDisconnect = h;
    }

    public void setExceptionListener(ExceptionListener exceptionListener)
    {
        this._exceptionListener = exceptionListener;
    }

    public void setThreadCreatedHandler(ThreadCreatedHandler h)
    {
        this._threadCreatedHandler = h;
    }

    public void setTransportFilter(TransportFilter filter)
    {
        this._filter = filter;
    }

    public void connect(URI addr) throws ConnectionRefusedException,
           AlreadyConnectedException, InvalidURIException
    {
        _lock.lock();
        _disconnecting = false;
        try
        {
            // clear out the interrupt bit.  If we don't do this, and the thread is interrupted,
            // EVERY SUBSEQUENT CALL TO connect() WILL FAIL FOREVER FROM THIS THREAD and throw a
            // ClosedByInterruptException.  Unlike InterruptedException, ClosedByInterruptException
            // does not clear the thread's interrupted state.
            Thread.interrupted();
            if(this._addr != null)
            {
                throw new AlreadyConnectedException("Already connected to AMPS at " +
                                                    this._addr.getHost() + ":" + this._addr.getPort() + "\n");
            }
            _socket = createSocket();

            // Merge properties from construction and the ones in this URI.
            URIProperties properties = new URIProperties(addr);
            if(_properties != null) properties.putAll(_properties);
            applySocketProperties(properties);

            int preferredAddrIndex = -1;
            InetAddress[] addrCandidates = InetAddress.getAllByName(addr.getHost());
            for(int i = 0; i < addrCandidates.length; i++)
            {
                 if(addrCandidates[i] instanceof Inet4Address)
                 {
                    preferredAddrIndex = i;
                    break;
                 }
            }
            if(preferredAddrIndex == -1)
            {
                for(int i = 0; i < addrCandidates.length; i++)
                {
                     if(addrCandidates[i] instanceof Inet6Address)
                     {
                        preferredAddrIndex = i;
                        break;
                     }
                }
            }
            if(preferredAddrIndex == -1)
            {
                throw new UnknownHostException();
            }

            _socket.connect(new InetSocketAddress(addrCandidates[preferredAddrIndex], addr.getPort()));
            handshake();
            _inputStream = _socket.getInputStream();
            _outputStream = _socket.getOutputStream();

            final int INTERNAL_TIMEOUT = 1000; // 1 second
            _socket.setSoTimeout(INTERNAL_TIMEOUT);
            _readerThread = new TCPReaderThread(this, this._messageType);
            _readerThread.barrier.await();
            _addr = addr;
            _connectionVersion++;
        }
        catch (InvalidURIException iuex)
        {
            throw iuex;
        }
        catch (ClosedByInterruptException e)
        {
            // Clear the interrupt flag!  It is set, and simply catching this exception
            // doesn't clear it.
            Thread.interrupted();
            throw new ConnectionRefusedException("Interrupted, but please try again.", e);
        }
        catch (BrokenBarrierException|InterruptedException e)
        {
            // Clear the interrupt flag!  It is set, and simply catching this
            // exception doesn't clear it.
            Thread.interrupted();
            throw new ConnectionRefusedException("Sync with reader thread interrupted, but please try again.", e);
        }
        catch (IllegalArgumentException iaex)
        {
            throw new InvalidURIException("Error setting socket options", iaex);
        }
        catch (Exception ex)
        {
            throw new ConnectionRefusedException("Unable to connect to AMPS at " +
                                                 addr.getHost() + ":" + addr.getPort(),
                                                 ex);
        }
        finally
        {
            _lock.unlock();
        }
    }

    protected Socket createSocket() throws Exception
    {
        return SocketFactory.getDefault().createSocket();
    }

    protected void handshake() throws Exception
    {
    }

    private void applySocketProperties(Properties properties_)
      throws SocketException, InvalidURIException
    {
        _socket.setKeepAlive(true);
        // We default SO_LINGER on, on Java, because
        // a shutdown() option isn't available on Java SocketChannels until JDK 1.7.
        _socket.setSoLinger(true, 10);
        if(properties_ == null) return;

        for(Map.Entry entry : properties_.entrySet())
        {
            Object key = entry.getKey();
            Object value = entry.getValue();
            if("tcp_keepalive".equals(key))
            {
                if("false".equals(value))
                {
                    _socket.setKeepAlive(false);
                }
                else if("true".equals(value))
                {
                    _socket.setKeepAlive(true);
                }
                else throw new InvalidURIException("Invalid value for tcp_keepalive.");
            }
            else if("tcp_sndbuf".equals(key))
            {
                try
                {
                    int sndbuf = Integer.parseInt((String)value);
                    _socket.setSendBufferSize(sndbuf);
                }
                catch(NumberFormatException ex)
                {
                    throw new InvalidURIException("Invalid value for tcp_sndbuf.", ex);
                }
            }
            else if("tcp_rcvbuf".equals(key))
            {
                try
                {
                    int rcvbuf = Integer.parseInt((String)value);
                    _socket.setReceiveBufferSize(rcvbuf);
                }
                catch(NumberFormatException ex)
                {
                    throw new InvalidURIException("Invalid value for tcp_rcvbuf.", ex);
                }
            }
            else if("tcp_linger".equals(key))
            {
                try
                {
                    int linger = Integer.parseInt((String)value);
                    if(linger == -1)
                    {
                        _socket.setSoLinger(false, 0);
                    }
                    else
                    {
                        _socket.setSoLinger(true, linger);
                    }
                }
                catch(NumberFormatException ex)
                {
                    throw new InvalidURIException("Invalid value for tcp_linger.", ex);
                }
            }
            else if("tcp_nodelay".equals(key))
            {
                if("false".equals(value))
                {
                    _socket.setTcpNoDelay(false);
                }
                else if ("true".equals(value))
                {
                    _socket.setTcpNoDelay(true);
                }
                else throw new InvalidURIException("Invalid value for tcp_nodelay.");
            }
            else if("tcp_connecttimeout".equals(key))
            {
                try
                {
                    int connectTimeout = Integer.parseInt((String)value);
                    _connectTimeout = connectTimeout;
                }
                catch (NumberFormatException ex)
                {
                  throw new InvalidURIException("Invalid value for tcp_connecttimeout.", ex);
                }
            }
            else if("bind".equals(key))
            {
                String addr = (String)value;
                if (addr == null || addr.isEmpty()) continue;
                int port = 0;
                int colon = addr.indexOf(':');
                int openBracket = addr.indexOf('[');
                if (openBracket > -1) {
                    int closeBracket = addr.indexOf(']');
                    if (closeBracket < 0)
                    {
                        throw new InvalidURIException("Invalid ipV6 uri: '" + addr + "' for bind.");
                    }
                    colon = closeBracket+1;
                    if (colon < addr.length() && colon == ':')
                    {
                        port = Integer.parseInt(addr.substring(colon+1));
                    }
                    addr = addr.substring(openBracket+1, closeBracket);
                }
                else if (colon > 0)
                {
                    addr = addr.substring(0, colon);
                    port = Integer.parseInt(addr.substring(colon+1));
                }
                try {
                    _socket.bind(new InetSocketAddress(addr, port));
                }
                catch (IOException ex) {
                    throw new InvalidURIException("Error binding to " + (String)value, ex);
                }
            }
            else if("pretty".equals(key))
            {
                // no-op; handled by the Client on logon.
            }
            else if("ip_protocol_prefer".equals(key))
            {
                // no-op; handled by the connect function.
            }
            else
            {
                throw new InvalidURIException("Unrecognized URI parameter `" + key +"'");
            }
        }
    }

    private void _disconnect()
    {
        try
        {
            if(_addr != null)
            {
                if(_readerThread != null) _readerThread.stopThread();
                _socket.close();

                // we'd like the next blocking operation in the
                // reader thread to throw so it can end.  Unless,
                // of course, *we* are the reader thread.
                if(_readerThread!=null &&
                  !_readerThread.equals(Thread.currentThread()))
                {
                    _readerThread.interrupt();
                    _readerThread.join();
                }
            }
        }
        catch (SocketException ex)
        {
            if(_exceptionListener != null)
              _exceptionListener.exceptionThrown(ex);
        }
        catch (IOException ex)
        {
            if(_exceptionListener != null)
              _exceptionListener.exceptionThrown(ex);
        }
        catch (SecurityException ex)
        {
            throw new RuntimeException("Security exception while "+
                "interrupting reader thread.", ex);
        }
        catch (InterruptedException ex)
        {
            if(_exceptionListener != null)
              _exceptionListener.exceptionThrown(ex);
        }
        _addr = null;
    }

    public void disconnect()
    {
        _lock.lock();
        try
        {
            _disconnecting = true;
            _disconnect();
        }
        finally
        {
            _lock.unlock();
        }
    }

    public void send(ByteBuffer buf) throws DisconnectedException
    {
        try
        {
            _filter.outgoing(buf);
            byte[] array = buf.array();
            int remaining = buf.remaining();
            int position = buf.position() + buf.arrayOffset();
            _outputStream.write(array,position,remaining);
        }
        catch (NullPointerException ex)
        {
            throw new DisconnectedException("Socket error while sending message.", ex);
        }
        catch (IOException ioex)
        {
            throw new DisconnectedException("Socket error while sending message.", ioex);
        }
    }


    public Socket socket()
    {
        return _socket;
    }

    public long writeQueueSize()
    {
        return 0;
    }

    public long readQueueSize()
    {
        return 0;
    }

    public long flush()
    {
        _lock.lock();
        try
        {
            // Having the lock means we've sent all data in this
            //   TCP implementation.
            return 0;
        }
        finally
        {
            _lock.unlock();
        }
    }

    public long flush(long timeout)
    {
        _lock.lock();
        try
        {
            // Having the lock means we've sent all data in this
            //   TCP implementation.
            return 0;
        }
        finally
        {
            _lock.unlock();
        }
    }

    public void handleCloseEvent(int failedVersion, String message, Exception e_) throws RetryOperationException, DisconnectedException
    {
        // Let all waiting clients know that this version of the connection is dead, if they didn't already know.
        _onDisconnect.preInvoke(failedVersion);

        //if readerThread is null, we were never fully initialized
        if((_readerThread != null) && !_readerThread.equals(Thread.currentThread()))
        {
            _lock.lock();
        }
        else
        {
            // The reader thread can cause a deadlock if send thread first grabs
            // the lock and then reader thread blocks trying to acquire it
            // before getting interrupted by the send thread.
            try
            {
                while (!_lock.tryLock(100, TimeUnit.MILLISECONDS))
                {
                    if (Thread.currentThread().isInterrupted())
                        throw new DisconnectedException("Reconnect is in progress in send thread.");
                }
            }
            catch(InterruptedException e)
            {
                throw new DisconnectedException("Reconnect already in progress in send thread.", e);
            }
        }
        try
        {
            // Don't try to reconnect if a disconnect is in progress.
            if(_disconnecting)
            {
                throw new DisconnectedException("Disconnect in progress.");
            }
            // If there's a new version of the connection available, you should use that.
            if(failedVersion != _connectionVersion)
            {
                throw new RetryOperationException("A new connection is available.");
            }

            //OK, our thread is in charge of disconnecting and reconnecting. Let's do it.
            try
            {
                // forget about any SO_LINGER we might have going; we want to kill this connection now.
                // this could fail, based on the underlying state of the socket.  Just ignore that --
                // if it fails, that means we're not going to have to wait for anything in _disconnect either.
                _socket.setSoLinger(true, 0);
            }
            catch(SocketException ex)
            {
                if(_exceptionListener != null)
                  _exceptionListener.exceptionThrown(ex);
            }

            _disconnect();

            // Create a wrapper around ourselves to pass into the disconnect handler.
            TCPTransport t = TCPTransport.createTransport(this._messageType);
            t._impl = this;
            try
            {
                _onDisconnect.invoke(t, new DisconnectedException(message, e_));
            }
            catch (Exception e)
            {
                throw new DisconnectedException("Disconnect handler threw an exception", e);
            }
        }
        finally
        {
            _lock.unlock();
        }
        // This doesn't need to be locked, because we only care if the
        // connection version is something other than what we arrived with.
        if(_connectionVersion == failedVersion)
        {
            // no work was done.  bail.
            throw new DisconnectedException("A disconnect occurred, and no disconnect handler successfully reconnected.");
        }
        else
        {
            throw new RetryOperationException("Reconnect successful; retry the operation.");
        }
    }
    static class TCPReaderThread extends Thread
    {
        TCPTransportImpl transport   = null;
        Protocol         messageType = null;
        volatile boolean stopped     = false;
        CyclicBarrier    barrier     = new CyclicBarrier(2);

        TCPReaderThread(TCPTransportImpl transport, Protocol messageType)
        {
            this.transport   = transport;
            this.messageType = messageType;
            this.stopped = false;
            this.setDaemon(TCPTransport.isDaemon());
            this.start();
        }

        public void stopThread()
        {
            this.stopped = true;
        }

        @Override
        public void run()
        {
            this.setName(String.format("AMPS Java Client Background Reader Thread %d", Thread.currentThread().getId()));
            if (this.transport._threadCreatedHandler != null)
            {
                try {
                    this.transport._threadCreatedHandler.invoke();
                }
                catch (Exception e) {
                    if (transport._exceptionListener != null) {
                        transport._exceptionListener.exceptionThrown(e);
                    }
                    String message = "The ThreadCreatedHandler failed.";
                    barrier.reset(); // Signal thread calling connect()
                    try {
                        transport.handleCloseEvent(transport._connectionVersion,
                                message,
                                new DisconnectedException(message, e));
                    } catch (RetryOperationException|DisconnectedException x) { ; }
                    return;
                }
            }
            try {
                barrier.await();
            }
            catch (BrokenBarrierException|InterruptedException e)
            {
                Thread.interrupted();
                String message = "The connection attempt was interrupted.";
                try {
                    transport.handleCloseEvent(transport._connectionVersion,
                            message,
                            new DisconnectedException(message));
                } catch (RetryOperationException|DisconnectedException x) { ; }
                return;
            }
            ByteBuffer rfifo = ByteBuffer.allocate(16 * 1024);
            ProtocolParser protocolParser = this.messageType.getMessageStream();
            final int IDLE_PROCESSING_TIMEOUT = 100; //millis
            long lastIdleProcessing = System.currentTimeMillis();
            long lastReadTime = System.currentTimeMillis();
            while (true)
            {

                // This won't stop us if we're blocked on the socket read
                if(this.stopped) return;
                // Do we need to do any idle-time processing (flushing acks)
                if(transport._idleRunnable != null)
                {
                  long currentTime = System.currentTimeMillis();
                  if(currentTime - lastIdleProcessing > IDLE_PROCESSING_TIMEOUT)
                  {
                    lastIdleProcessing = currentTime;
                    try
                    {
                      transport._idleRunnable.run();
                    }
                    catch (Exception e)
                    {
                      try
                      {
                        transport._exceptionListener.exceptionThrown(e);
                      }
                      catch (Exception e2)
                      {;}
                    }
                  }
                }
                try
                {
                    // remember the connection version we attempted to read with.
                    int currentVersion = this.transport._connectionVersion;
                    try
                    {
                        // read more data into the rfifo from the non-blocking socket.
                        int currentPosition = rfifo.position();
                        int readBytes = transport._inputStream.read(
                            rfifo.array(),rfifo.position(),rfifo.remaining());

                        if(readBytes == -1 || readBytes == 0)
                        {
                            String message = "The remote server has closed the connection.";
                            transport.handleCloseEvent(currentVersion,
                                    message,
                                    new DisconnectedException(message));
                            return;
                        }

                        lastReadTime = System.currentTimeMillis();

                        rfifo.position(currentPosition + readBytes);
                    }
                    catch (SocketTimeoutException ioex)
                    {
                        long timeSinceLastRead = System.currentTimeMillis() - lastReadTime;
                        if (transport._readTimeout != 0 && timeSinceLastRead > transport._readTimeout)
                        {
                            String message = String.format("No activity after %d milliseconds; connection closed by client.",
                                                           transport._readTimeout);
                            transport.handleCloseEvent(currentVersion,
                                    message,
                                    new DisconnectedException(message));
                            return;
                        }
                        // If no read timeout specified: just continue on.
                    }
                    catch (IOException ioex)
                    {
                        // only handle the close event if stopThead was not invoked
                        // we don't want to invoke the disconnectHandler if we intentionally
                        // try to disconnect
                        if(!this.stopped)
                        {
                            this.transport.handleCloseEvent(currentVersion, "Exception while reading", ioex);
                        }
                        return;
                    }

                    rfifo.flip(); // Move into get-mode

                    if(rfifo.remaining() >= 4)
                    {
                        // We read data into readBuffer
                        int size = rfifo.getInt(rfifo.position());
                        this.transport._filter.incoming(rfifo);
                        // Process until we need to fetch more data
                        while (rfifo.remaining() - 4 >= size)
                        {
                            // Best-case: We have everything in the buffer
                            size = rfifo.getInt();
                            try
                            {
                                protocolParser.process(rfifo, size, this.transport._onMessage);
                            }
                            catch(Exception e)
                            {
                                if(transport._exceptionListener != null)
                                {
                                    transport._exceptionListener.exceptionThrown(e);
                                }
                            }

                            // Fetch next size, if possible
                            if(rfifo.remaining() < 4)
                                break;
                            size = rfifo.getInt(rfifo.position());
                        }

                        // We need to prepare for fetching more data
                        if(rfifo.capacity() < size + 4)
                        {
                            // Worst-case: Our buffer isn't big enough to hold the
                            // message, let's resize.
                            int newSize = rfifo.capacity();
                            while (newSize < size + 4) newSize *= 2; // double until big enough
                            // System.out.println("size: " + size);
                            // System.out.println("newSize: " + newSize);
                            ByteBuffer newBuffer = ByteBuffer.allocate(newSize);
                            newBuffer.put(rfifo);
                            rfifo = newBuffer;
                        }
                        else compactBuffer(rfifo);
                    }
                    else compactBuffer(rfifo);

                }
                catch (Exception e)
                {
                    if(!this.stopped && transport._exceptionListener != null)
                    {
                        transport._exceptionListener.exceptionThrown(e);
                    }
                }
            }
        }

        /**
         * Helper method to skip unnecessary array copy in ByteBuffer.compact()
         * when it's not needed. See AC-842.
         */
        protected static void compactBuffer(ByteBuffer rfifo) {
            if (rfifo.position() != 0)
            {
                rfifo.compact();
            }
            else
            {
                // The position is 0, so skip unnecessary array
                // copy in compact(). See AC-842
                rfifo.position(rfifo.remaining());
                rfifo.limit(rfifo.capacity());
            }
        }
    }

    public void setReadTimeout(int readTimeoutMillis_)
    {
        _readTimeout = readTimeoutMillis_;
    }

    public void setConnectTimeout(int connectTimeoutMillis_)
    {
        _connectTimeout = connectTimeoutMillis_;
    }

    public void setIdleRunnable(AMPSRunnable runnable)
    {
        _idleRunnable = runnable;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy