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

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

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

package net.sf.eBus.client;

import com.google.common.base.Strings;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.StandardProtocolFamily;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectableChannel;
import java.util.logging.Level;
import java.util.logging.Logger;
import static net.sf.eBus.client.EAbstractConnection.ANY_PORT;
import net.sf.eBus.client.sysmessages.SystemMessageType;
import net.sf.eBus.client.sysmessages.UdpConnectReply;
import net.sf.eBus.client.sysmessages.UdpConnectRequest;
import net.sf.eBus.config.EConfigure.Service;
import net.sf.eBus.messages.EMessage;
import net.sf.eBus.messages.EMessageHeader;
import net.sf.eBus.messages.EReplyMessage.ReplyStatus;
import net.sf.eBus.messages.ESystemMessage;
import net.sf.eBus.messages.type.DataType;
import net.sf.eBus.messages.type.MessageType;
import net.sf.eBus.net.AbstractAsyncDatagramSocket;
import static net.sf.eBus.net.AsyncChannel.LOCALHOST;
import net.sf.eBus.net.AsyncDatagramSocket;
import net.sf.eBus.net.DatagramBufferWriter;
import net.sf.eBus.net.DatagramListener;

/**
 * This UDP datagram socket "accepts" a new UDP datagram socket
 * from a remote eBus application. While UDP is a connection-less
 * protocol eBus is connection-based. So a UDP server socket is
 * used to accept a new connection via when it receives a
 * {@link UdpConnectRequest} message. UDP server is created in
 * two ways: via the
 * {@link net.sf.eBus.config.EConfigure.ServerBuilder} API and
 * passing the {@link Service} instance to
 * {@link EUDPServer#EUDPServer(EConfigure.Service)} or at
 * application start using a properties or JSON configuration
 * file. See {@link net.sf.eBus.config.EConfigure} about using
 * properties or configuration files.
 *
 * @see ETCPServer
 * @see EUDPConnection
 *
 * @author Charles W. Rapp
 */

public class EUDPServer
    extends EServer
    implements DatagramListener
{
//---------------------------------------------------------------
// Member data.
//

    //-----------------------------------------------------------
    // Constants.
    //

    /**
     * UDP connect request key identifier.
     */
    private static final int UDP_CONNECT_REQUEST_KEY_ID =
        (SystemMessageType.UDP_CONNECT_REQUEST).keyId();

    /**
     * UDP connect reply key identifier.
     */
    private static final int UDP_CONNECT_REPLY_KEY_ID =
        (SystemMessageType.UDP_CONNECT_REPLY).keyId();

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

    /**
     * Message type for de-serializing UDP connect request
     * messages.
     */
    private static final MessageType sRequestType =
        (MessageType) DataType.findType(UdpConnectRequest.class);

    private static final MessageType sReplyType =
        (MessageType) DataType.findType(UdpConnectReply.class);

    /**
     * The logging subsystem interface.
     */
    private static final Logger sLogger =
        Logger.getLogger(EUDPServer.class.getName());

    //-----------------------------------------------------------
    // Locals.
    //

    /**
     * Encodes a {@link UdpConnectReply} message to the UDP
     * socket.
     */
    private final MessageWriter mWriter;

    /**
     * UDP datagram socket used to accept UDP connect requests.
     */
    private AsyncDatagramSocket mSocket;

    /**
     * The most recent UDP connect request came from this address.
     * Stored temporarily during the connection process and
     * dropped once the connect reply is sent.
     */
    private SocketAddress mSourceAddress;

//---------------------------------------------------------------
// Member methods.
//

    //-----------------------------------------------------------
    // Constructors.
    //

    /**
     * Creates a new, closed UDP server instance.
     * @param config UDP server configuration.
     */
    public EUDPServer(final Service config)
    {
        super (config);

        mWriter = new MessageWriter();
    } // end of EUDPServer(Service)

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

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

    /**
     * Creates a new {@code AsyncDatagramSocket} and opens on the
     * given server port.
     * @throws IOException
     * if there is an I/O failure creating and opening the server
     * UDP socket.
     */
    @Override
    protected void doOpen()
        throws IOException
    {
        if (mSocket == null)
        {
            final AsyncDatagramSocket.DatagramBuilder builder =
                AsyncDatagramSocket.builder();

            mSocket =
                builder.byteOrder(mConfiguration.byteOrder())
                       .inputBufferSize(mConfiguration.inputBufferSize())
                       .outputBufferSize(mConfiguration.outputBufferSize())
                       .selector(mConfiguration.serviceSelector())
                       .listener(this)
                       .build();
        }

        if (!mSocket.isOpen())
        {
            final SocketAddress bindAddr =
                new InetSocketAddress(LOCALHOST, mPort);

            mSocket.open(bindAddr);
        }
    } // end of doOpen()

    /**
     * Closes and drops the open UDP server socket.
     */
    @Override
    protected void doClose()
    {
        final AsyncDatagramSocket asyncSocket = mSocket;

        if (asyncSocket != null && asyncSocket.isOpen())
        {
            mSocket = null;
            asyncSocket.close();
        }
    } // end of doClose()

    /**
     * Closes the accepted UDP connection and sends a failed
     * UDP connect reply.
     * @param text explains the connect attempt failure.
     * @param channel accepted UDP connection.
     */
    @Override
    protected void doAcceptFailed(final String text,
                                  final SelectableChannel channel)
    {
        // Close the accepted channel first and then send the
        // UDP connect reply via the server UDP socket.
        try
        {
            channel.close();
        }
        catch (IOException ioex)
        {
            // Ignore exception when closing.
        }

        sendReply(ReplyStatus.ERROR, text, -1, mSourceAddress);
    } // end of doAcceptFailed(String, SelectableChannel)

    //
    // end of Abstract Method Implementations.
    //-----------------------------------------------------------

    //-----------------------------------------------------------
    // DatagramListener Interface Implementation.
    //

    /**
     * Creates a {@link ERemoteApp} to handle this new
     * connection if the connection passes the positive filter.
     * If not, the accepted client connection is immediately
     * closed.
     * @param buffer contains encoded eBus message.
     * @param address message received from this address.
     * @param socket message received on this UDP socket.
     */
    // java:S2095 - Resources should be closed.
    // Since this is an async channel the underlying socket is
    // closed at a later time.
    @SuppressWarnings({"java:S2095"})
    @Override
    public void handleInput(final ByteBuffer buffer,
                            final InetSocketAddress address,
                            final AbstractAsyncDatagramSocket socket)
    {
        final UdpConnectRequest request = decode(buffer);

        // Was a valid UDP connect request sent?
        if (request != null)
        {
            // Yes. Create a UDP channel to go along with it.
            try
            {
                final SocketAddress bindAddr =
                    new InetSocketAddress(ANY_PORT);
                final DatagramChannel channel =
                    DatagramChannel.open(
                        StandardProtocolFamily.INET6);

                // Have the datagram channel bind to any
                // available ephemeral port.
                channel.bind(bindAddr);
                channel.configureBlocking(false);

                if (sLogger.isLoggable(Level.FINE))
                {
                    sLogger.fine(
                        String.format(
                            "UDP server %d: accepted connection from %s, new local UDP channel bound to %s.",
                            mPort,
                            address,
                            channel.getLocalAddress()));
                }

                mSourceAddress = address;
                acceptConnection(address, channel);
            }
            catch (IOException ioex)
            {
                String message = ioex.getMessage();

                if (Strings.isNullOrEmpty(message))
                {
                    message = "(no reason given)";
                }

                if (sLogger.isLoggable(Level.WARNING))
                {
                    sLogger.log(
                        Level.WARNING,
                        String.format(
                            "Failed to open DatagramChannel: %s",
                            message),
                        ioex);
                }

                sendReply(ReplyStatus.ERROR,
                          message,
                          -1,
                          address);
            }
        }
    } // end of handleInput(...)

    /**
     * Logs the given {@code Throwable} as a warning.
     * @param t error occurring on {@code socket}.
     * @param socket UDP datagram socket on which the error
     * occurred.
     */
    @Override
    public void handleError(final Throwable t,
                            final AbstractAsyncDatagramSocket socket)
    {
        sLogger.log(Level.WARNING,
                    String.format(
                        "%s: error on UDP server socket.",
                        socket.localSocketAddress()),
                    t);
    } // end of handleError(...)

    //
    // end of DatagramListener Interface Implementation.
    //-----------------------------------------------------------

    /**
     * Creates a {@link UdpConnectReply} from the parameters and
     * sends that message to {@code address}.
     * @param status connect attempt status.
     * @param text text explaining {@code status}.
     * @param address send the message to this address.
     */
    /* package */ void sendReply(final ReplyStatus status,
                                 final String text,
                                 final int localPort,
                                 final SocketAddress address)
    {
        final UdpConnectReply.Builder builder =
            UdpConnectReply.builder();
        final UdpConnectReply reply =
            builder.status(status)
                   .text(text)
                   .port(localPort)
                   .build();

        if (sLogger.isLoggable(Level.FINEST))
        {
            sLogger.finest(
                String.format(
                    "UDP Server %d: sending message to %s:%n%s",
                    mPort,
                    address,
                    reply));
        }

        mWriter.setWrite(reply, address);

        try
        {
            mSocket.send(mWriter);
        }
        catch (IOException ioex)
        {
            sLogger.log(Level.WARNING,
                        String.format(
                            "UDP server %d: failed to send message:%n%s",
                            mPort,
                            reply),
                        ioex);
        }
    } // end of sendReply(...)

    /**
     * Returns the UDP connect request decoded from the given
     * buffer. Returns {@code null} if the message is not a UDP
     * connect request or the decode failed.
     * @param buffer contains encoded eBus message.
     * @param address message came from this remote eBus
     * application.
     * @return decoded eBus message.
     * @throws BufferUnderflowException
     * if {@code buffer} contains an incomplete UDP connect
     * request message.
     */
    private UdpConnectRequest decode(final ByteBuffer buffer)
    {
        final int size = buffer.getInt();
        final int keyId = (size < 0 ? -1: buffer.getInt());
        UdpConnectRequest retval = null;

        if (sLogger.isLoggable(Level.FINEST))
        {
            sLogger.finest(
                String.format(
                    "%s: received message, size=%,d, key ID=%d.",
                    mSourceAddress,
                    size,
                    keyId));
        }

        // Ignore heartbeat messages and non-UDP connect request
        // messages.
        if (keyId == UDP_CONNECT_REQUEST_KEY_ID)
        {
            // Skip the from and to identifiers.
            buffer.getInt();
            buffer.getInt();

            retval =
                (UdpConnectRequest) sRequestType.deserialize(
                    ESystemMessage.SYSTEM_SUBJECT, buffer);

            if (sLogger.isLoggable(Level.FINEST))
            {
                sLogger.finest(
                    String.format(
                        "%s: handling UDPConnectRequest message:%n%s",
                        mSocket.localSocketAddress(),
                        retval));
            }
            else if (sLogger.isLoggable(Level.FINER))
            {
                sLogger.finer(
                    String.format(
                        "%s: handling UDPConnectRequest message.",
                        mSocket.localSocketAddress()));
            }
        }

        return (retval);
    } // end of decode(ByteBuffer)

//---------------------------------------------------------------
// Member methods.
//

    /**
     * Used to encode a {@code UDPConnectReply} message directly
     * to a datagram output buffer.
     */
    private static final class MessageWriter
        implements DatagramBufferWriter
    {
    //-----------------------------------------------------------
    // Member data.
    //

        //-------------------------------------------------------
        // Locals.
        //

        /**
         * UDP connect reply message to be serialized.
         */
        private EMessage mMessage;

        /**
         * Contains the destination address of the next outbound
         * UDP connect reply message. Set just prior to sending
         * the message.
         */
        private SocketAddress mDestination;

    //-----------------------------------------------------------
    // Member methods.
    //

        //-------------------------------------------------------
        // Constructors.
        //

        private MessageWriter()
        {}

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

        //-------------------------------------------------------
        // DatagramBufferWriter Interface Implementation.
        //

        /**
         * Encodes {@link #mMessage} to {@code buffer} including
         * the eBus message header.
         * @param buffer encode the message to this buffer.
         * @return the UDP packet destination.
         */
        @Override
        public SocketAddress fill(final ByteBuffer buffer)
        {
            // Skip over the size byte position and fill in last.
            (buffer.position(EAbstractConnection.MESSAGE_SIZE_SIZE))
                  .putInt(UDP_CONNECT_REPLY_KEY_ID)
                  .putInt(EMessageHeader.NO_ID)
                  .putInt(EMessageHeader.NO_ID);
            sReplyType.serialize(mMessage, buffer);

            // Now write out the message size.
            buffer.putInt(0, buffer.position());

            return (mDestination);
        } // end of fill(ByteBuffer)

        //
        // end of DatagramBufferWriter Interface Implementation.
        //-------------------------------------------------------

        //-------------------------------------------------------
        // Set Methods.
        //

        /**
         * Sets the message and destination used by the
         * {@code fill(ByteBuffer)} method.
         * @param msg encode this message.
         * @param destination send the encoded message to this
         * destination.
         */
        private void setWrite(final EMessage msg,
                              final SocketAddress destination)
        {
            mMessage = msg;
            mDestination = destination;
        } // end of setWrite(EMessage, SocketAddress)

        //
        // end of Set Methdos.
        //-------------------------------------------------------
    } // end of class MessageReader
} // end of class EUDPServer




© 2015 - 2025 Weber Informatics LLC | Privacy Policy