net.sf.eBus.client.EServer Maven / Gradle / Ivy
//
// Copyright 2001 - 2008, 2011, 2015, 2016, 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.io.PrintWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.channels.SelectableChannel;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.annotation.Nullable;
import net.sf.eBus.client.EFeed.FeedScope;
import net.sf.eBus.config.AddressFilter;
import net.sf.eBus.config.EConfigure;
import net.sf.eBus.config.EConfigure.ConnectionType;
import static net.sf.eBus.config.EConfigure.ConnectionType.TCP;
import net.sf.eBus.config.EConfigure.Service;
import net.sf.eBus.util.MultiKey2;
import net.sf.eBus.util.logging.StatusReport;
import net.sf.eBus.util.logging.StatusReporter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Accepts new connections to this eBus application. If an
* address filter is set, verifies that the accepted connection
* passes the filter. Servers are distinguished by
* {@link ConnectionType connection type} and address. Note
* that {@link ConnectionType#TCP plain text} and
* {@link ConnectionType#SECURE_TCP secure} TCP connections are
* considered the same for these purposes.
*
* Applications wishing to be notified when {@code EServer}
* accepts a new remote eBus application connection need to
* implement the {@link ESubscriber} interface and subscribe
* for the message key
* {@link ServerMessage#MESSAGE_KEY net.sf.eBus.client.ServerMessage:/eBus}.
* {@link ServerMessage} contains the remote address of the
* newly accepted TCP connection. Note: this is
* a local-only feed and cannot be remote accessed.
*
*
* @author Charles Rapp
*/
public abstract class EServer
implements EPublisher,
StatusReporter
{
//---------------------------------------------------------------
// Member data.
//
//-----------------------------------------------------------
// Constants.
//
/**
* TCP ports must be >= to 1.
*/
public static final int MIN_PORT = 1;
/**
* TCP ports must be <= 65,535.
*/
public static final int MAX_PORT = 65535;
//-----------------------------------------------------------
// Statics.
//
/**
* eBus may have at most two server sockets open: one for
* accepting binary connections and one for XML connections.
*/
private static final ConcurrentMap, EServer> sServers =
new ConcurrentHashMap<>();
private static final DateTimeFormatter sFormatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd @ HH:mm:ss.SSS");
/**
* The logging subsystem interface.
*/
private static final Logger sLogger =
LoggerFactory.getLogger(EServer.class);
//-----------------------------------------------------------
// Locals.
//
/**
* Server is for this eBus connection type.
*/
protected final ConnectionType mType;
/**
* The service is open on this TCP/IP address.
*/
protected final InetSocketAddress mAddress;
/**
* Contains the configuration information used for accepted
* socket connections.
*/
protected final Service mConfiguration;
/**
* Service name based on type and address.
*/
protected final String mName;
/**
* Accept connections only if there are in this filter.
* If null, then accept all connections.
*/
private final AddressFilter mPositiveFilter;
/**
* Set to true when the service has been opened.
*/
private boolean mIsOpen;
/**
* The instance creation timestamp.
*/
private final LocalDateTime mCreated;
/**
* Tally up the number of connections accepted during the
* current status report period.
*/
private int mAcceptCount;
/**
* Total number of accepted connections since server
* creation.
*/
private int mTotalAcceptCount;
/**
* Publish new connection notifications on this feed.
*/
private IEPublishFeed mNewConnectionFeed;
//---------------------------------------------------------------
// Member methods.
//
//-----------------------------------------------------------
// Constructors.
//
/**
* Creates a server socket that will listen on the specified
address and create {@link ERemoteApp} objects. The parameters
* are used to configure the accept remote connections.
* @param config eBus service configuration.
*/
protected EServer(final Service config)
{
mType = config.connectionType();
mAddress = config.address();
mName = mType + "-server-" + mAddress;
mPositiveFilter = config.addressFilter();
mConfiguration = config;
mIsOpen = false;
mCreated = LocalDateTime.now();
mAcceptCount = 0;
} // end of EServer(Service)
//
// end of Constructors.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Abstract Method Declarations.
//
/**
* Opens the underlying server socket for the given address.
* @throws IOException
* if an I/O failure occurs while creating the server socket.
*/
protected abstract void doOpen()
throws IOException;
/**
* Closes the underlying server socket.
*/
protected abstract void doClose();
/**
* Cleans up the accepted channel after a connect attempt
* failed.
* @param text text explaining why the connect attempt failed.
* @param channel the accepted channel.
*/
protected abstract void doAcceptFailed(String text,
SelectableChannel channel);
//
// end of Abstract Method Declarations.
//-----------------------------------------------------------
//-----------------------------------------------------------
// StatusReporter Interface Implementation
//
/**
* Adds the eBus server connection status to the status
* report.
* @param report the logged status report.
*/
@Override
public void reportStatus(final PrintWriter report)
{
int acceptCount = mAcceptCount;
// Reset the report period accept count.
mAcceptCount = 0;
report.format("The eBus service is open on %s.%n",
mAddress)
.format(" created on %s%n",
sFormatter.format(mCreated))
.format(" accepted %,d %s.%n",
acceptCount,
(acceptCount == 1 ?
"connection" :
"connections"))
.format("total accepted %,d %s.%n",
mTotalAcceptCount,
(mTotalAcceptCount == 1 ?
"connection" :
"connections"));
} // end of reportStatus(PrintWriter)
//
// end of StatusReporter Interface Implementation
//-----------------------------------------------------------
//-----------------------------------------------------------
// EObject Interface Implementation.
//
/**
* Returns eBus object name. May not be unique.
* @return eBus object name.
*/
@Override
public String name()
{
return (mName);
} // end of name()
//
// end of EObject Interface Implementation.
//-----------------------------------------------------------
//-----------------------------------------------------------
// EPublisher Interface Implementation.
//
/**
* Updates the feed state. This method does nothing since the
* feed state is stored in the publish feed.
* @param pubState the new publisher feed state
* @param feed the feed state applies to this feed.
*/
@Override
public void publishStatus(final EFeedState pubState,
final IEPublishFeed feed)
{} // end of publishStatus(EFeedState, IEPublishFeed)
//
// end of EPublisher Interface Implementation.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Get methods.
//
/**
* Returns the eBus connection type for this server.
* @return eBus connection type.
*/
public ConnectionType connectionType()
{
return (mType);
} // end of connectionType()
/**
* Returns host on which this service is accepting
* connections.
* @return address on which this service is accepting
* connections.
*/
public @Nullable InetAddress host()
{
return (mAddress.getAddress());
} // end of host()
/**
* Returns port on which this service is accepting
* connections.
* @return port on which this service is accepting
* connections.
*/
public int port()
{
return (mAddress.getPort());
} // end of address()
/**
* Returns address and port on which this service is
* accepting connections.
* @return server host and port.
*/
public InetSocketAddress address()
{
return (mAddress);
} // end of address()
/**
* Returns eBus service configuration.
* @return eBus service configuration.
*/
public Service configuration()
{
return (mConfiguration);
} // end of configuration()
/**
* Returns {@code true} if this server is open and
* {@code false} otherwise.
* @return {@code true} if this server is open.
*/
public boolean isOpen()
{
return (mIsOpen);
} // end of isOpen()
/**
* Returns {@code true} if the singleton eBus service exists
* and is open; otherwise, returns {@code false}.
* @param type eBus connection type.
* @param address check if this service address is open.
* @return {@code true} if this eBus application has an open
* service.
*/
public static boolean isServiceOpen(final ConnectionType type,
final SocketAddress address)
{
final MultiKey2 sKey =
new MultiKey2<>(normalize(type), address);
final EServer server = sServers.get(sKey);
return (server != null && server.mIsOpen);
} // end of isServiceOpen(ConnectionType, SocketAddress)
/**
* Returns the number of existing eBus services for the
* given connection type.
* @param type eBus connection type.
* @return number of existing eBus services for the given
* connection type.
*/
public static int serviceCount(final ConnectionType type)
{
final ConnectionType normalized = normalize(type);
int retval = 0;
return (sServers.keySet()
.stream()
.filter(k -> (normalized == k.key(0)))
.map(item -> 1)
.reduce(retval, Integer::sum));
} // end of serviceCount(ConnectionType)
/**
* Returns the number of existing eBus services.
* @return number of existing eBus services.
*/
public static int serviceCount()
{
return (sServers.size());
} // end of serviceCount()
/**
* Returns a copy of the existing eBus service ports.
* @return existing eBus services.
*/
public static Collection> services()
{
return (new ArrayList<>(sServers.keySet()));
} // end of services()
/**
* Returns the eBus server instance associated with the given
* connection type and address. May return {@code null} if
* there is no eBus eBus server for {@code type} and
* {@code address}.
* @param type eBus connection type.
* @param address eBus server address.
* @return eBus server instance.
*/
@Nullable
public static EServer server(final ConnectionType type,
final SocketAddress address)
{
final MultiKey2 sKey =
new MultiKey2<>(normalize(type), address);
return (sServers.get(sKey));
} // end of server(ConnectionType, SocketAddress)
//
// end of Get methods.
//-----------------------------------------------------------
/**
* Opens a service socket on the given port and accepting
* connections of the given type and from the specified
* hosts and ports. The accepted connections
* are configured as per the given service configuration.
* Returns the opened {@code EServer} instance.
*
* If
* {@link net.sf.eBus.config.EConfigure.Service#addressFilter() address filter}
* contains an entry with a socket address but a address set
* to zero, that means the client connection may be bound to
* any address. If the address is > zero, then the client
* connection must be bound to the specific address. If the
* address filter is {@code null}, then all connections are
* accepted.
*
*
* Negative filters are not supported. A negative filter
* would accept all connections except those listed.
*
* @param config eBus service configuration.
* @return the opened server instance.
* @throws IllegalArgumentException if any of the given
* parameters is invalid.
* @throws IllegalStateException
* if service address is already open.
*/
public static EServer openServer(final Service config)
{
Objects.requireNonNull(config, "config is null");
final ConnectionType cType =
normalize(config.connectionType());
final InetSocketAddress address = config.address();
final MultiKey2 sKey =
new MultiKey2<>(cType, address);
if (sServers.containsKey(sKey))
{
throw (
new IllegalStateException(
"service already open"));
}
final EServer retval = createServer(config);
sServers.put(sKey, retval);
(StatusReport.getsInstance()).register(retval);
sLogger.debug("Opening {} server on {}:\n{}",
cType,
address,
config);
if (!retval.open())
{
retval.close();
throw (
new IllegalStateException(
"service failed to open"));
}
return (retval);
} // end of openServer(Service)
/**
* Closes the specified eBus service if open.
* @param cType eBus connection type.
* @param address close the service on this service address.
*/
public static void closeServer(final ConnectionType cType,
final SocketAddress address)
{
final MultiKey2 sKey =
new MultiKey2<>(normalize(cType), address);
if (sServers.containsKey(sKey))
{
final EServer server = sServers.remove(sKey);
(StatusReport.getsInstance()).deregister(server);
server.close();
}
} // end of closeServer(ConnectionType, SocketAddress)
/**
* Closes are currently open eBus servers.
*/
public static void closeAllServers()
{
final StatusReport report = StatusReport.getsInstance();
sServers.values()
.stream()
.map(
s ->
{
report.deregister(s);
return (s);
})
.forEachOrdered(s -> s.close());
sServers.clear();
} // end of closeAllServers()
/**
* Creates and opens an eBus service for this application as
* per the {@link net.sf.eBus.config.EConfigure eBus configuration}.
* @param config the eBus configuration.
* @throws IOException
* if the configured eBus service failed to open.
*/
public static void configure(final EConfigure config)
throws IOException
{
(config.services()).values()
.forEach(EServer::openServer);
} // end of configure(EConfigure)
/**
* Accepts the connection if the far-end address passes the
* address filter. If it fails, the connect attempt is
* rejected. If it passes, then a new {@code ERemoteApp}
* connection is opened.
* @param iAddress far-end address.
* @param channel accepted channel.
*/
protected void acceptConnection(final InetSocketAddress iAddress,
final SelectableChannel channel)
{
// Accept the client connection if:
// 1. There is no filter in place.
// 2. The filter contains the client's inetAddress and
// address.
// 3. The filter contains the client's inetAddress and
// accepts any address.
if (mPositiveFilter != null &&
!mPositiveFilter.passes(iAddress))
{
sLogger.info(
"Accepted unknown client connection {}; disconnecting.",
iAddress);
doAcceptFailed("unknown client connection", channel);
}
else
{
sLogger.info("Accepted client from {}.", iAddress);
++mAcceptCount;
++mTotalAcceptCount;
ERemoteApp.openConnection(this,
mType,
iAddress,
channel,
mConfiguration);
// Tell the listeners about this new remote
// application connection.
if (mNewConnectionFeed.isFeedUp())
{
mNewConnectionFeed.publish(
(ServerMessage.builder()).connectionType(mType)
.remoteAddress(iAddress)
.serverAddress(mAddress)
.build());
}
}
} // end of acceptConnection(...)
/**
* Server socket is unexpectedly closed. Marks the server as
* closed and close the server feed.
* @param jex exception behind the server close.
*/
protected void serverClosed(final Throwable jex)
{
final String message = jex.getMessage();
mIsOpen = false;
mNewConnectionFeed.close();
sLogger.warn("Service on {} unexpectedly closed, {}.",
mAddress,
(Strings.isNullOrEmpty(message) ?
"no reason given." :
message),
jex);
} // end of serverClosed(Throwable)
/**
* Performs the actual work of instantiating the eBus server
* and opening it on the specified port. Returns {@code true}
* if the eBus service is successfully opened.
* @return {@code true} if the eBus service is open.
*/
private boolean open()
{
// Open the service only if this service is closed.
if (!mIsOpen)
{
sLogger.debug("Opening service on address {}.",
mAddress);
try
{
// No. Establish the service.
doOpen();
mIsOpen = true;
// Tell the world that this service is open.
sLogger.info("Service open on {}.", mAddress);
// Open the new connection notification feed.
mNewConnectionFeed = openFeed();
}
catch (IOException ioex)
{
final String message = ioex.getMessage();
sLogger.warn(
"Failed to open eBus service on {}, {}.",
mAddress,
((message == null || message.length() == 0) ?
"no reason given." :
message),
ioex);
}
}
return (mIsOpen);
} // end of open()
/**
* Returns a publish feed for the "new connection"
* notification. This feed is both advertised and with an
* up feed state.
* @return "new connection" publish feed.
*/
private IEPublishFeed openFeed()
{
final EPublishFeed.Builder builder =
EPublishFeed.builder();
final IEPublishFeed retval =
builder.target(this)
.messageKey(ServerMessage.MESSAGE_KEY)
.scope(FeedScope.LOCAL_ONLY)
.build();
retval.advertise();
retval.updateFeedState(EFeedState.UP);
return (retval);
} // end of openFeed()
/**
* Performs the actual work of closing the open eBus service.
*/
private void close()
{
// Close the service only if this service is open.
if (mIsOpen)
{
sLogger.debug("Closing service on {}.", mAddress);
mIsOpen = false;
doClose();
// Close the new connection feed as well.
mNewConnectionFeed.close();
mNewConnectionFeed = null;
sLogger.info("Service closed on {}.", mAddress);
}
} // end of close()
/**
* If {@code type} is {@link ConnectionType#SECURE_TCP}, then
* returns {@link ConnectionType#TCP}; otherwise returns
* {@code type}.
* @param type normalizes this connection type.
* @return normalized connection type.
*/
private static ConnectionType normalize(final ConnectionType type)
{
return (type == ConnectionType.SECURE_TCP ?
ConnectionType.TCP :
type);
} // end of normalize(ConnectionType)
/**
* Returns the {@code EServer} instance of the appropriate
* subclass based on the connection type.
* @param config server configuration.
* @return eBus server instance.
*/
private static EServer createServer(final Service config)
{
final EServer retval;
switch (config.connectionType())
{
case TCP:
case SECURE_TCP:
retval = new ETCPServer(config);
break;
case UDP:
case SECURE_UDP:
retval = new EUDPServer(config);
break;
default:
retval = new EReliableUDPServer(config);
}
return (retval);
} // end of createServer(Service)
} // end of class EServer