/*
* Copyright 2014 Robert von Burg <eitch@eitchnet.ch>
*
* 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 li.strolch.communication.tcpip;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.BindException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.text.MessageFormat;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import li.strolch.communication.CommunicationConnection;
import li.strolch.communication.CommunicationEndpoint;
import li.strolch.communication.ConnectionException;
import li.strolch.communication.ConnectionMessages;
import li.strolch.communication.ConnectionState;
import li.strolch.communication.IoMessage;
import li.strolch.communication.IoMessageVisitor;
import li.strolch.utils.helper.StringHelper;
/**
* <p>
* This {@link CommunicationEndpoint} is an abstract implementation with everything needed to start a {@link Socket}
* server which waits for a request from a single client
* </p>
* <p>
* This end point only allows a single connection at a time and implements all exception handling for opening and
* closing a {@link Socket} connection
* </p>
*
* @see ServerSocketEndpoint#configure(CommunicationConnection, IoMessageVisitor) for details on configuring the end
* point
*
* @author Robert von Burg <eitch@eitchnet.ch>
*/
public class ServerSocketEndpoint implements CommunicationEndpoint, Runnable {
protected static final Logger logger = LoggerFactory.getLogger(ServerSocketEndpoint.class);
private Thread serverThread;
// state variables
private boolean connected;
private boolean closed;
private boolean fatal;
private long lastConnect;
private boolean useTimeout;
private int timeout;
private long retry;
private boolean clearOnConnect;
// address
private String localInputAddressS;
private int localInputPort;
private String remoteOutputAddressS;
private int remoteOutputPort;
private InetAddress localInputAddress;
private InetAddress remoteOutputAddress;
// connection
private ServerSocket serverSocket;
private Socket socket;
protected DataOutputStream outputStream;
protected DataInputStream inputStream;
protected CommunicationConnection connection;
protected SocketMessageVisitor messageVisitor;
/**
* Default constructor
*/
public ServerSocketEndpoint() {
this.connected = false;
this.closed = true;
this.fatal = false;
}
/**
* Checks the state of the connection and returns true if {@link Socket} is connected and ready for transmission,
* false otherwise
*
* @return true if {@link Socket} is connected and ready for transmission, false otherwise
*/
protected boolean checkConnection() {
return !this.closed
&& this.connected
&& (this.socket != null && !this.socket.isClosed() && this.socket.isBound()
&& this.socket.isConnected() && !this.socket.isInputShutdown() && !this.socket
.isOutputShutdown());
}
/**
* Listens on the {@link ServerSocket} for an incoming connection. Prepares the connection then for use. If the
* remote address has been defined, then the remote connection is validated to come from this appropriate host.
* CommunicationConnection attempts are always separated by a configured amount of time
*/
protected void openConnection() {
ConnectionState state = this.connection.getState();
// do not open the connection if state is
// - CREATED
// - CONNECTING
// - WAITING
// - CLOSED
if (state == ConnectionState.CREATED || state == ConnectionState.CONNECTING || state == ConnectionState.WAITING
|| state == ConnectionState.DISCONNECTED) {
ConnectionMessages.throwIllegalConnectionState(state, ConnectionState.CONNECTING);
}
// first close the connection
closeConnection();
while (!this.connected && !this.closed) {
try {
this.connection.notifyStateChange(ConnectionState.CONNECTING, ConnectionState.CONNECTING.toString());
// only try in proper intervals
long currentTime = System.currentTimeMillis();
long timeDifference = currentTime - this.lastConnect;
if (timeDifference < this.retry) {
long wait = this.retry - timeDifference;
logger.info(MessageFormat.format("Waiting: {0}ms", wait)); //$NON-NLS-1$
this.connection.notifyStateChange(ConnectionState.WAITING, ConnectionState.WAITING.toString());
Thread.sleep(wait);
this.connection
.notifyStateChange(ConnectionState.CONNECTING, ConnectionState.CONNECTING.toString());
}
// don't try and connect if we are closed!
if (this.closed) {
logger.error("The connection has been closed and can not be connected"); //$NON-NLS-1$
closeConnection();
this.connection.notifyStateChange(ConnectionState.DISCONNECTED, null);
return;
}
// keep track of the time of this connection attempt
this.lastConnect = System.currentTimeMillis();
// open the socket
String msg = "Waiting for connections on: {0}:{1}..."; //$NON-NLS-1$
logger.info(MessageFormat.format(msg, this.localInputAddress.getHostAddress(),
Integer.toString(this.localInputPort)));
this.socket = this.serverSocket.accept();
// validate that the remote side of the socket is really the client we want
if (this.remoteOutputAddress != null) {
String remoteAddr = this.socket.getInetAddress().getHostAddress();
if (!remoteAddr.equals(this.remoteOutputAddress.getHostAddress())) {
msg = "Illegal remote client at address {0}. Expected is {1}"; //$NON-NLS-1$
msg = MessageFormat.format(msg, remoteAddr, this.remoteOutputAddress.getHostAddress());
logger.error(msg);
closeConnection();
throw new ConnectionException(msg);
}
}
// configure the socket
if (logger.isDebugEnabled()) {
msg = "BufferSize (send/read): {0} / {1} SoLinger: {2} TcpNoDelay: {3}"; //$NON-NLS-1$
logger.debug(MessageFormat.format(msg, this.socket.getSendBufferSize(),
this.socket.getReceiveBufferSize(), this.socket.getSoLinger(), this.socket.getTcpNoDelay()));
}
//inputSocket.setSendBufferSize(1);
//inputSocket.setSoLinger(true, 0);
//inputSocket.setTcpNoDelay(true);
// activate connection timeout
if (this.useTimeout) {
this.socket.setSoTimeout(this.timeout);
}
// get the streams
this.outputStream = new DataOutputStream(this.socket.getOutputStream());
this.inputStream = new DataInputStream(this.socket.getInputStream());
if (this.clearOnConnect) {
// clear the input stream
int available = this.inputStream.available();
logger.info(MessageFormat.format("clearOnConnect: skipping {0} bytes.", available)); //$NON-NLS-1$
this.inputStream.skip(available);
}
msg = "Connected {0}{1}: {2}:{3} with local side {4}:{5}"; //$NON-NLS-1$
logger.info(MessageFormat.format(msg, this.getClass().getSimpleName(), this.connection.getId(),
this.socket.getInetAddress().getHostName(), Integer.toString(this.socket.getPort()),
this.socket.getLocalAddress().getHostAddress(), Integer.toString(this.socket.getLocalPort())));
// we are connected!
this.connection.notifyStateChange(ConnectionState.CONNECTED, ConnectionState.CONNECTED.toString());
this.connected = true;
} catch (InterruptedException e) {
logger.warn("Interrupted!"); //$NON-NLS-1$
this.closed = true;
this.connection.notifyStateChange(ConnectionState.DISCONNECTED, null);
} catch (Exception e) {
if (this.closed && e instanceof SocketException) {
logger.warn("Socket closed!"); //$NON-NLS-1$
this.connection.notifyStateChange(ConnectionState.DISCONNECTED, null);
} else {
String msg = "Error while opening socket for inbound connection {0}: {1}"; //$NON-NLS-1$
logger.error(MessageFormat.format(msg, this.connection.getId()), e.getMessage());
this.connected = false;
this.connection.notifyStateChange(ConnectionState.BROKEN, e.getLocalizedMessage());
}
}
}
}
/**
* closes the connection HARD by calling close() on the streams and socket. All Exceptions are caught to make sure
* that the connections are cleaned up
*/
protected void closeConnection() {
this.connected = false;
this.connection.notifyStateChange(ConnectionState.BROKEN, null);
if (this.outputStream != null) {
try {
this.outputStream.close();
} catch (IOException e) {
logger.error(MessageFormat.format("Error closing OutputStream: {0}", e.getLocalizedMessage())); //$NON-NLS-1$
} finally {
this.outputStream = null;
}
}
if (this.inputStream != null) {
try {
this.inputStream.close();
} catch (IOException e) {
logger.error(MessageFormat.format("Error closing InputStream: {0}", e.getLocalizedMessage())); //$NON-NLS-1$
} finally {
this.inputStream = null;
}
}
if (this.socket != null) {
try {
this.socket.close();
} catch (IOException e) {
logger.error(MessageFormat.format("Error closing InputSocket: {0}", e.getLocalizedMessage())); //$NON-NLS-1$
} finally {
this.socket = null;
}
String msg = "Socket closed for inbound connection {0} at local input address {1}:{2}"; //$NON-NLS-1$
logger.info(MessageFormat.format(msg, this.connection.getId(), this.localInputAddressS,
Integer.toString(this.localInputPort)));
}
}
/**
* <p>
* Configures this {@link ServerSocketEndpoint}
* </p>
* gets the parameter map from the connection and reads the following parameters from the map:
* <ul>
* <li>localInputAddress - the local IP or Hostname to bind to for incoming connections</li>
* <li>localInputPort - the local port on which to listen for incoming connections</li>
* <li>remoteOutputAddress - the IP or Hostname of the remote client. If this value is not null, then it will be
* verified that the connecting client is connecting from this address</li>
* <li>remoteOutputPort - the port from which the remote client must connect. If this value is not null, then it
* will be verified that the connecting client is connecting from this port</li>
* <li>retry - a configured retry wait time. Default is {@link SocketEndpointConstants#RETRY}</li>
* <li>timeout - the timeout after which an idle socket is deemed dead. Default is
* {@link SocketEndpointConstants#TIMEOUT}</li>
* <li>useTimeout - if true, then the timeout is activated. default is {@link SocketEndpointConstants#USE_TIMEOUT}</li>
* <li>clearOnConnect - if true, then the after a successful connect the input is cleared by discarding all
* available bytes. This can be useful in cases where the channel is clogged with stale data. default is
* {@link SocketEndpointConstants#CLEAR_ON_CONNECT}</li>
* </ul>
*
* @see CommunicationEndpoint#configure(CommunicationConnection, IoMessageVisitor)
*/
@Override
public void configure(CommunicationConnection connection, IoMessageVisitor messageVisitor) {
if (this.connection != null && connection.getState().compareTo(ConnectionState.INITIALIZED) > 0) {
logger.warn(MessageFormat.format("Inbound connection {0} already configured.", connection.getId())); //$NON-NLS-1$
return;
}
ConnectionMessages.assertLegalMessageVisitor(this.getClass(), SocketMessageVisitor.class, messageVisitor);
this.messageVisitor = (SocketMessageVisitor) messageVisitor;
this.connection = connection;
configure();
}
private void configure() {
Map<String, String> parameters = this.connection.getParameters();
this.localInputAddressS = parameters.get(SocketEndpointConstants.PARAMETER_LOCAL_INPUT_ADDRESS);
String localInputPortS = parameters.get(SocketEndpointConstants.PARAMETER_LOCAL_INPUT_PORT);
this.remoteOutputAddressS = parameters.get(SocketEndpointConstants.PARAMETER_REMOTE_OUTPUT_ADDRESS);
String remoteOutputPortS = parameters.get(SocketEndpointConstants.PARAMETER_REMOTE_OUTPUT_PORT);
// parse local Address to InetAddress object
try {
this.localInputAddress = InetAddress.getByName(this.localInputAddressS);
} catch (UnknownHostException e) {
throw ConnectionMessages.throwInvalidParameter(ServerSocketEndpoint.class,
SocketEndpointConstants.PARAMETER_LOCAL_INPUT_ADDRESS, this.localInputAddressS);
}
// parse local address port to integer
try {
this.localInputPort = Integer.parseInt(localInputPortS);
} catch (NumberFormatException e) {
throw ConnectionMessages.throwInvalidParameter(ServerSocketEndpoint.class,
SocketEndpointConstants.PARAMETER_LOCAL_INPUT_PORT, localInputPortS);
}
// if remote address is not set, then we will use the localhost InetAddress
if (this.remoteOutputAddressS == null || this.remoteOutputAddressS.length() == 0) {
logger.debug("No remoteOutputAddress set. Allowing connection from any remote address"); //$NON-NLS-1$
} else {
// parse remote output address name to InetAddress object
try {
this.remoteOutputAddress = InetAddress.getByName(this.remoteOutputAddressS);
} catch (UnknownHostException e) {
throw ConnectionMessages.throwInvalidParameter(ServerSocketEndpoint.class,
SocketEndpointConstants.PARAMETER_REMOTE_OUTPUT_ADDRESS, this.remoteOutputAddressS);
}
// parse remote output address port to integer
try {
this.remoteOutputPort = Integer.parseInt(remoteOutputPortS);
} catch (NumberFormatException e) {
throw ConnectionMessages.throwInvalidParameter(ServerSocketEndpoint.class,
SocketEndpointConstants.PARAMETER_REMOTE_OUTPUT_PORT, remoteOutputPortS);
}
}
// configure retry wait time
String retryS = parameters.get(SocketEndpointConstants.PARAMETER_RETRY);
if (retryS == null || retryS.length() == 0) {
ConnectionMessages.warnUnsetParameter(ServerSocketEndpoint.class, SocketEndpointConstants.PARAMETER_RETRY,
String.valueOf(SocketEndpointConstants.RETRY));
this.retry = SocketEndpointConstants.RETRY;
} else {
try {
this.retry = Long.parseLong(retryS);
} catch (NumberFormatException e) {
throw ConnectionMessages.throwInvalidParameter(ServerSocketEndpoint.class,
SocketEndpointConstants.PARAMETER_RETRY, retryS);
}
}
// configure if timeout on connection should be activated
String useTimeoutS = parameters.get(SocketEndpointConstants.PARAMETER_USE_TIMEOUT);
if (useTimeoutS == null || useTimeoutS.length() == 0) {
ConnectionMessages.warnUnsetParameter(ServerSocketEndpoint.class,
SocketEndpointConstants.PARAMETER_USE_TIMEOUT, String.valueOf(SocketEndpointConstants.USE_TIMEOUT));
this.useTimeout = SocketEndpointConstants.USE_TIMEOUT;
} else {
this.useTimeout = Boolean.parseBoolean(useTimeoutS);
}
if (this.useTimeout) {
// configure timeout on connection
String timeoutS = parameters.get(SocketEndpointConstants.PARAMETER_TIMEOUT);
if (timeoutS == null || timeoutS.length() == 0) {
ConnectionMessages.warnUnsetParameter(ServerSocketEndpoint.class,
SocketEndpointConstants.PARAMETER_TIMEOUT, String.valueOf(SocketEndpointConstants.TIMEOUT));
this.timeout = SocketEndpointConstants.TIMEOUT;
} else {
try {
this.timeout = Integer.parseInt(timeoutS);
} catch (NumberFormatException e) {
throw ConnectionMessages.throwInvalidParameter(ServerSocketEndpoint.class,
SocketEndpointConstants.PARAMETER_TIMEOUT, timeoutS);
}
}
}
// configure if the connection should be cleared on connect
String clearOnConnectS = parameters.get(SocketEndpointConstants.PARAMETER_CLEAR_ON_CONNECT);
if (clearOnConnectS == null || clearOnConnectS.length() == 0) {
ConnectionMessages.warnUnsetParameter(ServerSocketEndpoint.class,
SocketEndpointConstants.PARAMETER_CLEAR_ON_CONNECT,
String.valueOf(SocketEndpointConstants.CLEAR_ON_CONNECT));
this.clearOnConnect = SocketEndpointConstants.CLEAR_ON_CONNECT;
} else {
this.clearOnConnect = Boolean.parseBoolean(clearOnConnectS);
}
}
/**
* @return the uri as String to which this {@link ServerSocketEndpoint} is locally bound to
*/
@Override
public String getLocalUri() {
if (this.socket != null) {
InetAddress localAddress = this.socket.getLocalAddress();
return localAddress.getHostAddress() + StringHelper.COLON + this.socket.getLocalPort();
} else if (this.localInputAddress != null) {
return this.localInputAddress.getHostAddress() + StringHelper.COLON + this.localInputPort;
}
return "0.0.0.0:0"; //$NON-NLS-1$
}
/**
* @return the uri as String from which this {@link ServerSocketEndpoint} is receiving data from
*/
@Override
public String getRemoteUri() {
if (this.socket != null) {
InetAddress remoteAddress = this.socket.getInetAddress();
return remoteAddress.getHostAddress() + StringHelper.COLON + this.socket.getPort();
} else if (this.remoteOutputAddressS != null) {
return this.remoteOutputAddress.getHostAddress() + StringHelper.COLON + this.remoteOutputPort;
}
return "0.0.0.0:0"; //$NON-NLS-1$
}
/**
* Starts the {@link Thread} to allow incoming connections
*
* @see CommunicationEndpoint#start()
*/
@Override
public void start() {
if (this.fatal) {
String msg = "CommunicationConnection had a fatal exception and can not yet be started. Please check log file for further information!"; //$NON-NLS-1$
throw new ConnectionException(msg);
}
if (this.serverThread != null) {
logger.warn(MessageFormat.format("CommunicationConnection {0} already started.", this.connection.getId())); //$NON-NLS-1$
} else {
// logger.info(MessageFormat.format("Enabling connection {0}...", this.connection.getId())); //$NON-NLS-1$
this.closed = false;
this.serverThread = new Thread(this, this.connection.getId());
this.serverThread.start();
}
}
/**
* Closes any open connection and then stops the {@link Thread} disallowing incoming connections
*
* @see CommunicationEndpoint#stop()
*/
@Override
public void stop() {
closeThread();
closeConnection();
this.connection.notifyStateChange(ConnectionState.DISCONNECTED, ConnectionState.DISCONNECTED.toString());
logger.info(MessageFormat.format("Disabled connection {0}.", this.connection.getId())); //$NON-NLS-1$
}
@Override
public void reset() {
closeThread();
closeConnection();
configure();
this.connection.notifyStateChange(ConnectionState.INITIALIZED, ConnectionState.INITIALIZED.toString());
}
private void closeThread() {
this.closed = true;
this.fatal = false;
if (this.serverThread != null) {
try {
this.serverThread.interrupt();
if (this.serverSocket != null)
this.serverSocket.close();
this.serverThread.join(2000l);
} catch (Exception e) {
logger.error(MessageFormat.format(
"Exception while interrupting server thread: {0}", e.getLocalizedMessage())); //$NON-NLS-1$
}
this.serverThread = null;
}
}
/**
* Thread is listening on the ServerSocket and opens a new connection if necessary
*/
@Override
public void run() {
while (!this.closed) {
// bomb-proof, catches all exceptions!
try {
// if serverSocket is null or closed, open a new server socket
if (this.serverSocket == null || this.serverSocket.isClosed()) {
try {
// String msg = "Opening socket on {0}:{1}..."; //$NON-NLS-1$
// logger.info(MessageFormat.format(msg, this.localInputAddress.getHostAddress(),
// Integer.toString(this.localInputPort)));
this.serverSocket = new ServerSocket(this.localInputPort, 1, this.localInputAddress);
this.serverSocket.setReuseAddress(true);
} catch (BindException e) {
logger.error("Fatal BindException occurred! Port is already in use, or address is illegal!"); //$NON-NLS-1$
logger.error(e.getMessage(), e);
this.closed = true;
this.fatal = true;
String msg = "Fatal error while binding to server socket. ServerSocket endpoint is dead"; //$NON-NLS-1$
throw new ConnectionException(msg);
}
}
// open the connection
openConnection();
// as long as connection is connected
while (checkConnection()) {
// read and write from the connected server socket
IoMessage message = this.messageVisitor.visit(this.inputStream, this.outputStream);
if (message != null) {
this.connection.handleNewMessage(message);
}
}
} catch (Exception e) {
if (e instanceof InterruptedException) {
logger.error("Interrupted!"); //$NON-NLS-1$
} else {
logger.error(e.getMessage(), e);
}
this.connection.notifyStateChange(ConnectionState.BROKEN, e.getLocalizedMessage());
} finally {
closeConnection();
}
}
if (!this.fatal) {
logger.warn(MessageFormat.format(
"CommunicationConnection {0} is not running anymore!", this.connection.getId())); //$NON-NLS-1$
this.connection.notifyStateChange(ConnectionState.BROKEN, null);
} else {
String msg = "CommunicationConnection {0} is broken due to a fatal exception!"; //$NON-NLS-1$
logger.error(MessageFormat.format(msg, this.connection.getId()));
this.connection.notifyStateChange(ConnectionState.DISCONNECTED, null);
}
}
@Override
public void simulate(IoMessage message) throws Exception {
send(message);
}
@Override
public void send(IoMessage message) throws Exception {
String msg = "The Server Socket can not send messages, use the {0} implementation instead!"; //$NON-NLS-1$
throw new UnsupportedOperationException(MessageFormat.format(msg, ClientSocketEndpoint.class.getName()));
}
}