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

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

///////////////////////////////////////////////////////////////////////////
//
// Copyright (c) 2010-2024 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.charset.StandardCharsets;
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.List;
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.TimedOutException;
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
{
    protected        Client            _client              = null;
    protected        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 volatile AMPSRunnable    _idleRunnable      = null;
    private          int               _readTimeout         = 0;
    private          int               _connectTimeout      = 0;
    private          int               _idleReadTimeout     = 1000; // 1 second default
    private    ThreadCreatedHandler _threadCreatedHandler   = null;
    protected        boolean           _httpPreflight       = false;
    protected        boolean           _handshakeSetStreams = false;
    private          List      _httpPreflightHeaders = null;

    /**
     * Constructs a TCPTransportImpl object with the specified message type, properties, and transport filter.
     * @param messageType The message type associated with the transport.
     * @param properties  The properties to be applied to the transport.
     * @param filter      The transport filter to be applied.
     */
    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();
    }

    /**
     * Factory method for creating a new instance of {@link TCPTransport}.
     * @param messageType The protocol message type for the transport.
     * @param impl The underlying implementation for the new transport.
     * @return A new instance of TCPTransport with the specified protocol message type.
     */
    public TCPTransport createTransport(Protocol messageType, TCPTransportImpl impl)
    {
        return TCPTransport.createTransport(messageType, impl);
    }

    /**
     * Sets the message handler for the transport.
     * @param h The message handler to be set.
     */
    public void setMessageHandler(MessageHandler h)
    {
        this._onMessage = h;
    }

    /**
     * Sets the disconnect handler for the transport.
     * @param h The disconnect handler to be set.
     */
    public void setDisconnectHandler(TransportDisconnectHandler h)
    {
        this._onDisconnect = h;
    }

    /**
     * Sets the exception listener for the transport.
     * @param exceptionListener The exception listener to be set.
     */
    public void setExceptionListener(ExceptionListener exceptionListener)
    {
        this._exceptionListener = exceptionListener;
    }

    /**
     * Sets the thread created handler for the transport.
     * @param h The thread created handler to be set.
     */
    public void setThreadCreatedHandler(ThreadCreatedHandler h)
    {
        this._threadCreatedHandler = h;
    }

    /**
     * Sets the transport filter for the transport.
     * @param filter The transport filter to be set.
     */
    public void setTransportFilter(TransportFilter filter)
    {
        this._filter = filter;
    }

    /**
     * Connects the transport to the specified URI.
     * @param addr The URI to connect to.
     * @throws ConnectionRefusedException Thrown if the connection is refused.
     * @throws AlreadyConnectedException  Thrown if the transport is already connected.
     * @throws InvalidURIException        Thrown if the URI is invalid.
     */
    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();
            _addr = addr;

            // Merge properties from construction and the ones in this URI.
            URIProperties properties = new URIProperties(addr);
            if(_properties != null) properties.putAll(_properties);
            applySocketProperties(properties);
            connectSocket(addr, properties);
            handshake();
            _socket.setSoTimeout(_idleReadTimeout);
            createStream(addr);

            _connectionVersion++;
            _readerThread = new TCPReaderThread(this, this._messageType);
            _readerThread.barrier.await();
        }
        catch (InvalidURIException iuex)
        {
            _addr = null;
            throw iuex;
        }
        catch (ClosedByInterruptException e)
        {
            // Clear the interrupt flag!  It is set, and simply catching this exception
            // doesn't clear it.
            Thread.interrupted();
            _addr = null;
            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();
            _addr = null;
            throw new ConnectionRefusedException("Sync with reader thread interrupted, but please try again.", e);
        }
        catch (IllegalArgumentException iaex)
        {
            _addr = null;
            throw new InvalidURIException("Error setting socket options", iaex);
        }
        catch (AlreadyConnectedException e)
        {
            _addr = null;
            throw e;
        }
        catch (Exception ex)
        {
            _addr = null;
            throw new ConnectionRefusedException("Unable to connect to AMPS at " +
                                                 addr.getHost() + ":" + addr.getPort(),
                                                 ex);
        }
        finally
        {
            _lock.unlock();
        }
    }

    /**
     * Creates a socket using the default socket factory.
     * @return A newly created socket.
     * @throws Exception If an error occurs while creating the socket.
     */
    protected Socket createSocket() throws Exception
    {
        return SocketFactory.getDefault().createSocket();
    }

    protected void connectSocket(URI addr, URIProperties properties) throws Exception
    {
        String default_ip_proto_prefer = System.getProperty("com.crankuptheamps.client.DEFAULT_IP_PROTOCOL_PREFER", "ipv4");
        if(!("ipv4".equals(default_ip_proto_prefer) || "ipv6".equals(default_ip_proto_prefer)))
        {
            throw new IllegalArgumentException("Invalid value '" + default_ip_proto_prefer + "' for com.crankuptheamps.client.DEFAULT_IP_PROTOCOL_PREFER System Property");
        }
        String ip_proto_prefer = properties.getProperty("ip_protocol_prefer", default_ip_proto_prefer);
        if(!("ipv4".equals(ip_proto_prefer) || "ipv6".equals(ip_proto_prefer)))
        {
            throw new InvalidURIException("Invalid value '" + ip_proto_prefer + "' for ip_protocol_prefer URI parameter");
        }
        int preferredAddrIndex = -1;
        InetAddress[] addrCandidates = InetAddress.getAllByName(addr.getHost());
        if("ipv4".equals(ip_proto_prefer))
        {
            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();
            }
        }
        else
        {
            for(int i = 0; i < addrCandidates.length; i++)
            {
                 if(addrCandidates[i] instanceof Inet6Address)
                 {
                    preferredAddrIndex = i;
                    break;
                 }
            }
            if(preferredAddrIndex == -1)
            {
                for(int i = 0; i < addrCandidates.length; i++)
                {
                     if(addrCandidates[i] instanceof Inet4Address)
                     {
                        preferredAddrIndex = i;
                        break;
                     }
                }
            }
            if(preferredAddrIndex == -1)
            {
                throw new UnknownHostException();
            }
        }

        _socket.connect(new InetSocketAddress(addrCandidates[preferredAddrIndex], addr.getPort()), _connectTimeout);
    }

    protected void createStream(URI addr) throws IOException
    {
        if (!_handshakeSetStreams) {
            _inputStream = _socket.getInputStream();
            _outputStream = _socket.getOutputStream();
        }
        else {
            _handshakeSetStreams = false;
        }
    }

    /**
     * Performs the handshake for the TCP connection.
     * @throws Exception If an error occurs during the handshake process.
     */
    protected void handshake() throws Exception
    {
        if (_httpPreflight) {
            createStream(_addr);
            _handshakeSetStreams = true;
            StringBuilder get = new StringBuilder();
            get.append("GET ").append(_addr.getPath()).append(" HTTP/1.1\r\n").append("Host: ").append(_addr.getHost()).append("\r\nConnection: upgrade\r\nUpgrade: ").append(_addr.getScheme()).append("\r\n");
            if (_httpPreflightHeaders != null) {
                for (String header : _httpPreflightHeaders) {
                    get.append(header).append("\r\n");
                }
            }
            get.append("\r\n");
            _outputStream.write(get.toString().getBytes(StandardCharsets.UTF_8));
            byte[] getResponse = new byte[2046];
            int readBytes = _inputStream.read(getResponse, 0, 2046);
            int totalBytes = readBytes;
            // Make sure we at least read initial line of response
            while (totalBytes < 15) {
                if(readBytes == -1 || readBytes == 0)
                {
                    throw new DisconnectedException("The remote server has closed the upgrade connection.");
                }
                readBytes = _inputStream.read(getResponse, readBytes, 2046);
                totalBytes += readBytes;
            }
            if (getResponse[9] != (byte)'1' || getResponse[10] != (byte)'0' || getResponse[11] != (byte)'1') {
                throw new ConnectionRefusedException("Failed to upgrade connection failed response code");
            }
            // Clear any remaining response bytes
            long available = (long)_inputStream.available();
            while (available > 0) {
                _inputStream.skip(available);
                available = (long)_inputStream.available();
            }
        }
    }

    /**
     * Applies socket properties based on the provided properties.
     * @param properties_ The properties to be applied to the socket.
     * @throws SocketException      If an error occurs while setting socket options.
     * @throws InvalidURIException If an invalid URI parameter is encountered.
     */
    protected void applySocketProperties(Properties properties_)
      throws SocketException, InvalidURIException
    {
        _socket.setKeepAlive(true);
        if(properties_ == null) return;

        for(Map.Entry entry : properties_.entrySet())
        {
            Object key = entry.getKey();
            Object value = entry.getValue();
            applySocketProperty(key, value);
        }
    }

    protected void applySocketProperty(Object key, Object value)
      throws SocketException, InvalidURIException
    {
        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: " + value, ex);
            }
        }
        else if("tcp_idlereadtimeout".equals(key))
        {
            try
            {
                int idleReadTimeout = Integer.parseInt((String)value);
                if (idleReadTimeout <= 0 || idleReadTimeout > 1000) {
                    throw new InvalidURIException("Invalid value for "
                        + "tcp_idlereadtimeout. Must be between 1 and 1000"
                        + " ms inclusive: " + value);
                }
                _idleReadTimeout = idleReadTimeout;
            }
            catch (NumberFormatException ex)
            {
                throw new InvalidURIException("Invalid value for tcp_idlereadtimeout: " + value, ex);
            }
        }
        else if("bind".equals(key))
        {
            String addr = (String)value;
            if (addr == null || addr.isEmpty()) return;
            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 if("sni".equals(key))
        {
            // no-op; handled in TCPSTransportImpl
        }
        else if("http_preflight".equals(key))
        {
            if ("true".equals((String)value)) {
                _httpPreflight = true;
            }
        }
        else
        {
            throw new InvalidURIException("Unrecognized URI parameter `" + key +"'");
        }
    }

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

                // Give thread a chance to end gracefully, otherwise
                // 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.isAlive())
                {
                    // Wait up to 5 ms for reader thread to die from closed
                    // socket and stop flag.
                    for (int i = 0; i < 5 && _readerThread.isAlive(); ++i) {
                        try {
                            // We don't join yet because it's possible for
                            // join to hang indefinitely in rare cases. See...
                            // https://insidecoffe.blogspot.com/2011/12/when-timeout-fails-in-threadjoin.html
                            Thread.sleep(1L);
                        } catch (InterruptedException e) {
                            // If we are interrupted, then we'll interrupt the
                            // reader thread to hasten its exit before we give
                            // up our wait.
                            _readerThread.interrupt();
                            throw e;
                        }
                    }
                    // If the thread is still alive, we get more aggressive.
                    long start = System.currentTimeMillis();
                    while (_readerThread.isAlive()) {
                        // Now we'll interrupt the thread to break it out of long
                        // blocking calls. We'll keep repeating this every 5
                        // seconds while the thread remains alive. This is because
                        // a blocking call that throws InterruptedException clears
                        // the interrupted state of the thread, so it's possible
                        // it may enter another blocking call after handling the
                        // InterruptedException.
                        _readerThread.interrupt();
                        // Now we'll risk joining for 5 seconds.
                        _readerThread.join(5000L);
                        // Notify any exception listener that this is taking longer
                        // than expected.
                        if (_exceptionListener != null && _readerThread.isAlive()) {
                            long elapsed = System.currentTimeMillis() - start;
                            StringBuilder sb = new StringBuilder(512);
                            String clientName = (_client == null) ? "": _client.getName();
                            sb.append(String.format(
                                "WARNING: Client [%s] is taking longer to close down "
                                + "than expected: elapsed time = %d ms%n", clientName, elapsed));
                            sb.append(String.format(
                                "\tClient reader thread [%s], id = %d, state = %s, interrupted = %s%n",
                                _readerThread.getName(), _readerThread.getId(),
                                _readerThread.getState(), _readerThread.isInterrupted()));
                            StackTraceElement[] st = _readerThread.getStackTrace();
                            if (st != null) {
                                for (StackTraceElement ste: st) {
                                    sb.append(String.format("\tat %s%n", ste));
                                }
                            }
                            sb.append(String.format(
                                "------------- End Client Reader Thread Stack Trace -------------%n"));
                            try {
                                _exceptionListener.exceptionThrown(
                                    new TimedOutException(sb.toString()));
                            } catch (RuntimeException e) { /* ignore */ }
                        }
                    }
                }
            }
        }
        catch (IOException ex)
        {
            try {
                if(_exceptionListener != null)
                  _exceptionListener.exceptionThrown(ex);
            } catch (RuntimeException e2) { /* ignore */ }
        }
        catch (SecurityException ex)
        {
            throw new RuntimeException("Security exception while "+
                "interrupting reader thread.", ex);
        }
        catch (InterruptedException ex)
        {
            try {
                if(_exceptionListener != null)
                  _exceptionListener.exceptionThrown(ex);
            } catch (RuntimeException e2) { /* ignore */ }
        }
        _addr = null;
    }

    /**
     * Initiates the disconnection process.
     */
    public void disconnect()
    {
        _lock.lock();
        try
        {
            _disconnecting = true;
            _disconnect();
        }
        finally
        {
            _lock.unlock();
        }
    }

    /**
     * Sends data through the socket.
     * @param buf The ByteBuffer containing the data to be sent.
     * @throws DisconnectedException If the socket is disconnected while sending the message.
     */
    public void send(ByteBuffer buf) throws DisconnectedException
    {
        try
        {
            byte[] array = buf.array();
            int remaining = buf.remaining();
            int position = buf.position();
            int offset = buf.arrayOffset();
            _filter.outgoing(buf);
            _outputStream.write(array,position+offset,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);
        }
    }

    /**
     * Retrieves the underlying socket.
     * @return The underlying socket.
     */
    public Socket socket()
    {
        return _socket;
    }

    /**
     * Returns the size of the write queue.
     * @return The size of the write queue, which is always 0 in this implementation.
     */
    public long writeQueueSize()
    {
        return 0;
    }

    /**
     * Returns the size of the read queue.
     * @return The size of the read queue, which is always 0 in this implementation.
     */
    public long readQueueSize()
    {
        return 0;
    }

     /**
     * Flushes the data in the write queue.
     * @return Always returns 0 as the data has been flushed.
     */
    public long flush()
    {
        _lock.lock();
        try
        {
            // Having the lock means we've sent all data in this
            //   TCP implementation.
            return 0;
        }
        finally
        {
            _lock.unlock();
        }
    }

    /**
     * Flushes the data in the write queue with a specified timeout.
     * @param timeout The timeout value in milliseconds.
     * @return Always returns 0 as the data has been flushed.
     */
    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();
        }
    }

    /**
     * Handles the event of the connection being closed.
     * @param failedVersion The version of the failed connection.
     * @param message       The message associated with the event.
     * @param e_            The exception that caused the event, if any.
     * @throws RetryOperationException If a new connection is available and retrying the operation is necessary.
     * @throws DisconnectedException   If an error occurs during reconnection or disconnection.
     */
    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))
                {
                    // Don't try to lock and reconnect if a shutdown is in progress.
                    if(_disconnecting)
                    {
                        throw new DisconnectedException("Disconnect in progress.");
                    }
                    if (Thread.currentThread().isInterrupted())
                        throw new DisconnectedException("Reconnect is in progress in send thread.");
                }
            }
            catch(InterruptedException e)
            {
                if(_disconnecting)
                {
                    throw new DisconnectedException("Disconnect in progress.", e);
                }
                throw new DisconnectedException("Reconnect already in progress in send thread.", e);
            }
        }
        try
        {
            // Don't try to reconnect if a shutdown 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)
            {
                try {
                    if(_exceptionListener != null)
                      _exceptionListener.exceptionThrown(ex);
                } catch (RuntimeException e) { /* ignore */ }
            }

            _disconnect();

            // Create a wrapper around ourselves to pass into the disconnect handler.
            TCPTransport t = TCPTransport.createTransport(this._messageType, 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.");
        }
    }

    /**
     * A thread responsible for reading data from the TCP transport.
     */
    static class TCPReaderThread extends Thread
    {
        TCPTransportImpl transport   = null;
        Protocol         messageType = null;
        volatile boolean stopped     = false;
        CyclicBarrier    barrier     = new CyclicBarrier(2);
        boolean          msgReceived = false;
        MessageHandler   onMessage   = new MessageHandler() {

            @Override
            public void invoke(Message msg) {
                msgReceived = true;
                transport._onMessage.invoke(msg);
            }

        };

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

        /**
         * Stops the thread.
         */
        public void stopThread()
        {
            this.stopped = true;
        }

        /**
         * Runs the background reader thread for the AMPS Java Client.
         * This thread is responsible for reading data from the TCP transport,
         * handling disconnects, and invoking necessary handlers.
         */
        @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) {
                    try {
                        if (transport._exceptionListener != null) {
                            transport._exceptionListener.exceptionThrown(e);
                        }
                    } catch (RuntimeException e2) { /* ignore */ }
                    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();
            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(msgReceived && transport._idleRunnable != null)
                {
                    try
                    {
                        transport._idleRunnable.run();
                    }
                    catch (Exception e)
                    {
                        try
                        {
                            if (transport._exceptionListener != null) {
                                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());
                        int position = rfifo.position();
                        this.transport._filter.incoming(rfifo);
                        rfifo.position(position);
                        // 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.onMessage);
                            }
                            catch(Exception e)
                            {
                                try {
                                    if(transport._exceptionListener != null)
                                    {
                                        transport._exceptionListener.exceptionThrown(e);
                                    }
                                } catch (RuntimeException e2) { /* ignore */ }
                            }

                            // 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)
                {
                    try {
                        if(!this.stopped && transport._exceptionListener != null)
                        {
                            transport._exceptionListener.exceptionThrown(e);
                        }
                    } catch (RuntimeException e2) { /* ignore */ }
                }
            }
        }

        /**
         * 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());
            }
        }
    }

    /**
     * Sets the read timeout for the TCP transport.
     * @param readTimeoutMillis_ The read timeout value in milliseconds.
     */
    public void setReadTimeout(int readTimeoutMillis_)
    {
        _readTimeout = readTimeoutMillis_;
    }

    /**
     * Sets the connect timeout for the TCP transport.
     * @param connectTimeoutMillis_ The connect timeout value in milliseconds.
     */
    public void setConnectTimeout(int connectTimeoutMillis_)
    {
        _connectTimeout = connectTimeoutMillis_;
    }

    /**
     * Gets the transport impl's currently registered idle task, or null
     * if there is none.
     *
     * @return The currently registered idle runnable or null.
     */
    public AMPSRunnable getIdleRunnable() {
        return _idleRunnable;
    }

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

    public void initFromClient(Client client) {
        _client = client;
        setMessageHandler(client.getTransportMessageHandler());
        setDisconnectHandler(client.getTransportDisconnectHandler());
        _httpPreflightHeaders = client.getHttpPreflightHeaders();
        setThreadCreatedHandler(client.getThreadCreatedHandler());
        setExceptionListener(client.exceptionListener);
        // This should never be true, but just in case
        TransportFilter filter = client.getTransportFilter();
        if (filter != null) {
            setTransportFilter(filter);
        }
        else {
            setTransportFilter(new DefaultTransportFilter());
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy