com.nokia.dempsy.messagetransport.tcp.TcpReceiver.java Source code

Java tutorial

Introduction

Here is the source code for com.nokia.dempsy.messagetransport.tcp.TcpReceiver.java

Source

/*
 * Copyright 2012 the original author or authors.
 *
 * 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 com.nokia.dempsy.messagetransport.tcp;

import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import org.apache.commons.io.IOUtils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MarkerFactory;

import com.nokia.dempsy.messagetransport.Listener;
import com.nokia.dempsy.messagetransport.MessageTransportException;
import com.nokia.dempsy.messagetransport.OverflowHandler;
import com.nokia.dempsy.messagetransport.Receiver;

public class TcpReceiver implements Receiver {
    private static Logger logger = LoggerFactory.getLogger(TcpReceiver.class);
    protected TcpDestination destination;

    private ServerSocket serverSocket;
    private Thread serverThread;
    private AtomicBoolean stopMe = new AtomicBoolean(false);

    private Listener messageTransportListener;
    private Set<ClientThread> clientThreads = new HashSet<ClientThread>();
    private OverflowHandler overflowHandler = null;

    private Object eventLock = new Object();
    private volatile boolean eventSignaled = false;

    protected boolean useLocalhost = false;
    protected int port = -1;

    @PostConstruct
    public synchronized void start() throws MessageTransportException {
        if (isStarted())
            return;

        // this sets the destination instance
        getDestination();

        // we need to call bind here in case the getDestination didn't do it
        //  (which it wont if the port is not ephemeral)
        bind();

        // in case this is a restart, we want to reset the stopMe value.
        stopMe.set(false);

        serverThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (!stopMe.get()) {
                    try {
                        // Wait for an event one of the registered channels
                        Socket clientSocket = serverSocket.accept();

                        // at the point we're committed to adding a new ClientThread to the set.
                        //  So we need to lock it.
                        synchronized (clientThreads) {
                            // unless we're done.
                            if (!stopMe.get()) {
                                // This should come from a thread pool
                                ClientThread clientThread = new ClientThread(clientSocket);
                                Thread thread = new Thread(clientThread,
                                        "Client Handler for " + getClientDescription(clientSocket));
                                thread.setDaemon(true);
                                thread.start();

                                clientThreads.add(clientThread);
                            }
                        }

                    }
                    // This can happen if I rip the socket out from underneath the accept call. 
                    // Because accept doesn't exit with a Thread.interrupt call so closing the server
                    // socket from another thread is the only way to make this happen.
                    catch (SocketException se) {
                        // however, if we didn't explicitly stop the server, then there's another problem
                        if (!stopMe.get())
                            logger.error("Socket error on the server managing " + destination, se);
                    } catch (Throwable th) {
                        logger.error("Major error on the server managing " + destination, th);
                    }
                }

                // we're leaving so signal
                synchronized (eventLock) {
                    eventSignaled = true;
                    eventLock.notifyAll();
                }
            }
        }, "Server for " + destination);

        serverThread.start();
    }

    /**
     * When an overflow handler is set the Adaptor indicates that a 'failFast' should happen
     * and any failed message deliveries end up passed to the overflow handler. 
     */
    public void setOverflowHandler(OverflowHandler handler) {
        this.overflowHandler = handler;
    }

    @PreDestroy
    public synchronized void stop() {
        stopMe.set(true);

        try {
            if (messageTransportListener != null)
                messageTransportListener.shuttingDown();
        } catch (Throwable th) {
            logger.error("Listener threw exception when being notified of shutdown on " + destination, th);
        }

        if (serverThread != null)
            serverThread.interrupt();
        serverThread = null;

        // this is a hack because the thread interrupt doesn't wake 
        // up an accept call
        closeQuietly(serverSocket);
        serverSocket = null;

        synchronized (clientThreads) {
            for (ClientThread ct : clientThreads)
                ct.stop();

            clientThreads.clear();
        }

        // now wait until the event is signaled
        synchronized (eventLock) {
            if (!eventSignaled) {
                try {
                    eventLock.wait(500);
                } catch (InterruptedException e) {
                } // wait for 1/2 second
            }

            if (!eventSignaled)
                logger.warn("Couldn't release the socket accept for " + destination);
        }

    }

    public synchronized boolean isStarted() {
        return serverThread != null;
    }

    @Override
    public synchronized TcpDestination getDestination() throws MessageTransportException {
        if (destination == null)
            destination = doGetDestination();

        if (destination.isEphemeral())
            bind();

        return destination;
    }

    @Override
    public void setListener(Listener messageTransportListener) {
        this.messageTransportListener = messageTransportListener;
    }

    public TcpDestination doGetDestination() throws MessageTransportException {
        try {
            InetAddress inetAddress = useLocalhost ? InetAddress.getLocalHost() : getFirstNonLocalhostInetAddress();
            return new TcpDestination(inetAddress, port);
        } catch (UnknownHostException e) {
            throw new MessageTransportException("Failed to identify the current hostname", e);
        } catch (SocketException e) {
            throw new MessageTransportException("Failed to identify the current hostname", e);
        }
    }

    /**
     * Directs the factory to create TcpDestinations using "localhost." By default
     * the factory will not do this since the use of this InetAddress from another
     * machine will not result in a connection back to the machine the TcpDestination
     * was created from.
     */
    public void setUseLocalhost(boolean useLocalhost) {
        this.useLocalhost = useLocalhost;
    }

    /**
     * <p>Directs the factory to create TcpDestinations indicating that the port number is dynamic.
     * It does this by setting the port to -1. This is actually the default if the port number
     * isn't set using {@code setPort()}.</p>
     * 
     * <p>This is meant to be used from a DI container and set as a property. The value passed
     * doesn't actually do anything. To use a fixed port call setPort which will undo this setting.</p>
     */
    public void setUseEphemeralPort(boolean useEphemeralPort) {
        this.port = -1;
    }

    /**
     * Directs the factory to create TcpDestinations providing the given port number.
     */
    public void setPort(int port) {
        this.port = port;
    }

    private static InetAddress getFirstNonLocalhostInetAddress() throws SocketException {
        Enumeration<NetworkInterface> netInterfaces = NetworkInterface.getNetworkInterfaces();
        while (netInterfaces.hasMoreElements()) {
            NetworkInterface networkInterface = (NetworkInterface) netInterfaces.nextElement();
            for (Enumeration<InetAddress> loopInetAddress = networkInterface.getInetAddresses(); loopInetAddress
                    .hasMoreElements();) {
                InetAddress tempInetAddress = loopInetAddress.nextElement();
                if (!tempInetAddress.isLoopbackAddress() && tempInetAddress instanceof Inet4Address)
                    return tempInetAddress;
            }
        }
        return null;
    }

    protected void bind() throws MessageTransportException {
        if (serverSocket == null || !serverSocket.isBound()) {
            try {
                InetSocketAddress inetSocketAddress = new InetSocketAddress(destination.inetAddress,
                        destination.port < 0 ? 0 : destination.port);
                serverSocket = new ServerSocket();
                serverSocket.setReuseAddress(true); // this allows the server port to be bound to even if it's in TIME_WAIT
                serverSocket.bind(inetSocketAddress);
                destination.port = serverSocket.getLocalPort();
            } catch (IOException ioe) {
                throw new MessageTransportException("Cannot bind to port "
                        + (destination.isEphemeral() ? "(ephemeral port)" : destination.port), ioe);
            }
        }
    }

    protected class ClientThread implements Runnable {
        private Socket clientSocket;
        private DataInputStream dataInputStream;
        private Thread thisThread;
        private AtomicBoolean stopClient = new AtomicBoolean(false);

        protected ClientThread(Socket clientSocket) throws IOException {
            this.clientSocket = clientSocket;
            this.dataInputStream = new DataInputStream(new BufferedInputStream(clientSocket.getInputStream()));
        }

        @Override
        public void run() {
            thisThread = Thread.currentThread();
            InputStream inputStream = null;
            Exception clientIsApparentlyGone = null;

            try {
                while (!stopMe.get() && !stopClient.get()) {
                    try {
                        byte[] messageBytes = null;
                        try {
                            messageBytes = new byte[dataInputStream.readInt()];
                            dataInputStream.readFully(messageBytes);
                        }
                        // either a problem with the socket OR a thread interruption (InterruptedIOException)
                        catch (EOFException eof) {
                            clientIsApparentlyGone = eof;
                            messageBytes = null; // no message if exception
                        } catch (IOException ioe) {
                            clientIsApparentlyGone = ioe;
                            messageBytes = null; // no message if exception
                        }

                        if (messageBytes != null) {
                            if (messageTransportListener != null) {
                                try {
                                    boolean messageSuccess = messageTransportListener.onMessage(messageBytes,
                                            overflowHandler != null);
                                    if (overflowHandler != null && !messageSuccess)
                                        overflowHandler.overflow(messageBytes);
                                } catch (Throwable se) {
                                    String messageAsString;
                                    try {
                                        messageAsString = (messageBytes == null ? "null" : messageBytes.toString());
                                    } catch (Throwable th) {
                                        messageAsString = "(failed to convert message to a string)";
                                    }
                                    logger.error("Unexpected listener exception on adaptor for " + destination
                                            + " trying to process a message of type "
                                            + messageBytes.getClass().getSimpleName() + " with a value of "
                                            + messageAsString + " using listener "
                                            + messageTransportListener.getClass().getSimpleName(), se);
                                }
                            }
                        } else if (clientIsApparentlyGone == null && logger.isDebugEnabled()) {
                            if (logger.isDebugEnabled())
                                logger.debug("Received a null message on destination " + destination);

                            // if we read no bytes we should just consider ourselves lucky that we
                            // escaped a blocking read.
                            stopClient.set(true); // leave the loop.
                        }

                        if (clientIsApparentlyGone != null) {
                            String logmessage = "Client " + getClientDescription(clientSocket)
                                    + " has apparently disconnected from sending messages to " + destination;
                            if (logger.isDebugEnabled())
                                logger.debug(logmessage, clientIsApparentlyGone);
                            // assume the client socket is dead.

                            stopClient.set(true); // leave the loop.
                            clientIsApparentlyGone = null;
                        }
                    } catch (Throwable th) {
                        logger.error(MarkerFactory.getMarker("FATAL"),
                                "Completely unexpected error. This problem should be addressed because we should never make it here in the code.",
                                th);
                        stopClient.set(true); // leave the loop, close up.
                    }
                } // end while loop
            } catch (Throwable ue) {
                logger.error("Completely unexpected error", ue);
            } finally {
                if (inputStream != null)
                    IOUtils.closeQuietly(inputStream);
                closeQuietly(clientSocket);

                // remove me from the client list.
                synchronized (clientThreads) {
                    clientThreads.remove(this);
                }
            }
        }

        public void stop() {
            stopClient.set(true);
            if (thisThread != null)
                thisThread.interrupt();
        }
    }

    private static String getClientDescription(Socket socket) {
        try {
            return "(" + socket.getInetAddress().getHostAddress() + ":" + socket.getPort() + ")";
        } catch (Throwable th) {
            return "(Unknown Client)";
        }
    }

    private void closeQuietly(Socket socket) {
        if (socket != null) {
            try {
                socket.close();
            } catch (IOException ioe) {
                if (logger.isDebugEnabled())
                    logger.debug("close socket failed for " + destination, ioe);
            }
        }
    }

    private void closeQuietly(ServerSocket socket) {
        if (socket != null) {
            try {
                socket.close();
            } catch (IOException ioe) {
                if (logger.isDebugEnabled())
                    logger.debug("close socket failed for " + destination, ioe);
            }
        }
    }

}