net.sf.eBus.client.EUDPServer Maven / Gradle / Ivy
The newest version!
//
// 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 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;
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 net.sf.eBus.net.AsyncDatagramSocket;
import net.sf.eBus.net.DatagramBufferWriter;
import net.sf.eBus.net.DatagramListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 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.
*/
protected static final int UDP_CONNECT_REQUEST_KEY_ID =
(SystemMessageType.UDP_CONNECT_REQUEST).keyId();
/**
* UDP connect reply key identifier.
*/
protected 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);
/**
* Message type for serializing UDP connect reply.
*/
protected static final MessageType sReplyType =
(MessageType) DataType.findType(UdpConnectReply.class);
/**
* The logging subsystem interface.
*/
private static final Logger sLogger =
LoggerFactory.getLogger(EUDPServer.class);
//-----------------------------------------------------------
// Locals.
//
/**
* Encodes a {@link UdpConnectReply} message to the UDP
* socket.
*/
private final DatagramBufferWriter 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 UDPMessageWriter();
} // end of EUDPServer(Service)
/**
* Creates a new, closed UDP server instance for the given
* configuration and message writer.
* @param config UDP server configuration.
* @param writer server message writer.
*/
protected EUDPServer(final Service config,
final UDPMessageWriter writer)
{
super (config);
mWriter = writer;
} // end of EUDPServer(Service, UDPMessageWriter)
//
// end of Constructors.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Abstract Method Implementations.
//
/**
* Creates a new {@code AsyncDatagramSocket} and opens on the
* given server address.
* @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())
{
mSocket.open(mAddress);
}
} // 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 and wildcard address.
channel.bind(bindAddr);
channel.configureBlocking(false);
sLogger.debug(
"UDP server {}: accepted connection from {}, new local UDP channel bound to {}.",
mAddress,
address,
channel.getLocalAddress());
mSourceAddress = address;
acceptConnection(address, channel);
}
catch (IOException ioex)
{
String message = ioex.getMessage();
if (Strings.isNullOrEmpty(message))
{
message = "(no reason given)";
}
sLogger.warn(
"Failed to open DatagramChannel: {}",
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.warn("{}: 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();
sLogger.trace("UDP Server {}: sending message to {}:\n{}",
mAddress,
address,
reply);
((UDPMessageWriter) mWriter).setTarget(reply, address);
try
{
mSocket.send(mWriter);
}
catch (IOException ioex)
{
sLogger.warn(
"UDP server {}: failed to send message:\n{}",
mAddress,
reply,
ioex);
}
} // end of sendReply(...)
/**
* Reads in eBus message header from and to identifiers.
* @param buffer extract eBus message header from this
* buffer.
*/
protected void decodeHeader(final ByteBuffer buffer)
{
// Skip the from and to identifiers.
buffer.getInt();
buffer.getInt();
} // end of decodeHeader(ByteBuffer)
/**
* 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;
sLogger.trace("{}: received message, size={}, key ID={}.",
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.
decodeHeader(buffer);
retval =
(UdpConnectRequest) sRequestType.deserialize(
ESystemMessage.SYSTEM_SUBJECT, buffer);
if (sLogger.isTraceEnabled())
{
sLogger.trace(
"{}: handling UDPConnectRequest message:\n{}",
mSocket.localSocketAddress(),
retval);
}
else
{
sLogger.debug(
"{}: handling UDPConnectRequest message.",
mSocket.localSocketAddress());
}
}
return (retval);
} // end of decode(ByteBuffer)
//---------------------------------------------------------------
// Inner classes.
//
/**
* Used to encode a {@code UDPConnectReply} message directly
* to a datagram output buffer.
*/
protected static class UDPMessageWriter
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.
//
protected UDPMessageWriter()
{}
//
// 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);
fillHeader(buffer);
sReplyType.serialize(mMessage, null, 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 setTarget(final EMessage msg,
final SocketAddress destination)
{
mMessage = msg;
mDestination = destination;
} // end of setTarget(EMessage, SocketAddress)
//
// end of Set Methdos.
//-------------------------------------------------------
/**
* Fills in the message header. Note that the buffer
* has already been positioned after the message size
* field.
* @param buffer serialize message header to this buffer.
*/
protected void fillHeader(final ByteBuffer buffer)
{
buffer.putInt(UDP_CONNECT_REPLY_KEY_ID)
.putInt(EMessageHeader.NO_ID)
.putInt(EMessageHeader.NO_ID);
} // end of fillHeader(ByteBuffer)
} // end of class UDPMessageReader
} // end of class EUDPServer