/* * 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.InetAddress; import java.net.Socket; 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.communication.IoMessage.State; import li.strolch.utils.helper.StringHelper; /** * <p> * This {@link CommunicationEndpoint} is an abstract implementation with everything needed to connect through a * {@link Socket} to a remote server which is listening for incoming {@link Socket} connections. This {@link Socket} * endpoint can send messages to the remote side, as well as receive messages from the remote side * </p> * <p> * This endpoint is maintained as a client connection. This means that this endpoint opens the {@link Socket} to the * remote server * </p> * * @author Robert von Burg <eitch@eitchnet.ch> */ public class ClientSocketEndpoint implements CommunicationEndpoint { protected static final Logger logger = LoggerFactory.getLogger(ClientSocketEndpoint.class); // state variables private boolean connected; private boolean closed; private long lastConnect; private boolean useTimeout; private int timeout; private long retry; private boolean clearOnConnect; private boolean connectOnStart; private boolean closeAfterSend; // remote address private String remoteInputAddressS; private int remoteInputPort; private String localOutputAddressS; private int localOutputPort; private InetAddress remoteInputAddress; private InetAddress localOutputAddress; // connection private Socket socket; protected DataOutputStream outputStream; protected DataInputStream inputStream; protected CommunicationConnection connection; protected SocketMessageVisitor messageVisitor; /** * Default constructor */ public ClientSocketEndpoint() { this.connected = false; this.closed = true; } /** * 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()); } /** * Establishes a {@link Socket} connection to the remote server. This method blocks till a connection is * established. In the event of a connection failure, the method waits a configured time before retrying */ protected void openConnection() { ConnectionState state = this.connection.getState(); // do not allow connecting 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 if (this.localOutputAddress != null) { String msg = "Opening connection to {0}:{1} from {2}:{3}..."; //$NON-NLS-1$ logger.info(MessageFormat.format(msg, this.remoteInputAddress.getHostAddress(), Integer.toString(this.remoteInputPort), this.localOutputAddress.getHostAddress(), Integer.toString(this.localOutputPort))); this.socket = new Socket(this.remoteInputAddress, this.remoteInputPort, this.localOutputAddress, this.localOutputPort); } else { String msg = "Opening connection to {0}:{1}..."; //$NON-NLS-1$ logger.info(MessageFormat.format(msg, this.remoteInputAddress.getHostAddress(), Integer.toString(this.remoteInputPort))); this.socket = new Socket(this.remoteInputAddress, this.remoteInputPort); } // configure the socket if (logger.isDebugEnabled()) { String msg = "BufferSize (send/read): {0} / {1} SoLinger: {2} TcpNoDelay: {3}"; //$NON-NLS-1$ logger.info(MessageFormat.format(msg, this.socket.getSendBufferSize(), this.socket.getReceiveBufferSize(), this.socket.getSoLinger(), this.socket.getTcpNoDelay())); } //outputSocket.setSendBufferSize(1); //outputSocket.setSoLinger(true, 0); //outputSocket.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); } String msg = "Connected {0}: {1}:{2} with local side {3}:{4}"; //$NON-NLS-1$ logger.info(MessageFormat.format(msg, this.connection.getId(), this.remoteInputAddressS, Integer.toString(this.remoteInputPort), 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.info("Interrupted!"); //$NON-NLS-1$ this.closed = true; this.connection.notifyStateChange(ConnectionState.DISCONNECTED, null); } catch (Exception e) { String msg = "Error while connecting to {0}:{1}: {2}"; //$NON-NLS-1$ logger.error( MessageFormat.format(msg, this.remoteInputAddressS, Integer.toString(this.remoteInputPort)), e.getMessage()); 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 OutputSocket: {0}", e.getLocalizedMessage())); //$NON-NLS-1$ } finally { this.socket = null; } String msg = "Socket closed for connection {0} at remote input address {1}:{2}"; //$NON-NLS-1$ logger.info(MessageFormat.format(msg, this.connection.getId(), this.remoteInputAddressS, Integer.toString(this.remoteInputPort))); } } /** * <p> * Configures this {@link ClientSocketEndpoint} * </p> * gets the parameter map from the connection and reads the following parameters from the map: * <ul> * <li>remoteInputAddress - the IP or Hostname of the remote server</li> * <li>remoteInputPort - the port to which the socket should be established</li> * <li>localOutputAddress - the IP or Hostname of the local server (if null, then the network layer will decide)</li> * <li>localOutputPort - the local port from which the socket should go out of (if null, then the network layer will * decide)</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, otherwise it is. 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> * <li>connectOnStart - if true, then when the connection is started, the connection to the remote address is * attempted. default is {@link SocketEndpointConstants#CONNECT_ON_START} * </ul> * * @see CommunicationEndpoint#configure(CommunicationConnection, IoMessageVisitor) */ @Override public void configure(CommunicationConnection connection, IoMessageVisitor messageVisitor) { if (this.connection != null && connection.getState().compareTo(ConnectionState.INITIALIZED) > 0) { String msg = "{0}:{1} already configured."; //$NON-NLS-1$ logger.warn(MessageFormat.format(msg, this.getClass().getSimpleName(), connection.getId())); 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.remoteInputAddressS = parameters.get(SocketEndpointConstants.PARAMETER_REMOTE_INPUT_ADDRESS); String remoteInputPortS = parameters.get(SocketEndpointConstants.PARAMETER_REMOTE_INPUT_PORT); this.localOutputAddressS = parameters.get(SocketEndpointConstants.PARAMETER_LOCAL_OUTPUT_ADDRESS); String localOutputPortS = parameters.get(SocketEndpointConstants.PARAMETER_LOCAL_OUTPUT_PORT); // parse remote input Address to InetAddress object try { this.remoteInputAddress = InetAddress.getByName(this.remoteInputAddressS); } catch (UnknownHostException e) { throw ConnectionMessages.throwInvalidParameter(ClientSocketEndpoint.class, SocketEndpointConstants.PARAMETER_REMOTE_INPUT_ADDRESS, this.remoteInputAddressS); } // parse remote input address port to integer try { this.remoteInputPort = Integer.parseInt(remoteInputPortS); } catch (NumberFormatException e) { throw ConnectionMessages.throwInvalidParameter(ClientSocketEndpoint.class, SocketEndpointConstants.PARAMETER_REMOTE_INPUT_PORT, remoteInputPortS); } // if local output address is not set, then we will use the localhost InetAddress if (this.localOutputAddressS == null || this.localOutputAddressS.length() == 0) { logger.debug("No localOutputAddress set. Using localhost"); //$NON-NLS-1$ } else { // parse local output address name to InetAddress object try { this.localOutputAddress = InetAddress.getByName(this.localOutputAddressS); } catch (UnknownHostException e) { String msg = "The host name ''{0}'' can not be evaluated to an internet address"; //$NON-NLS-1$ msg = MessageFormat.format(msg, this.localOutputAddressS); throw new ConnectionException(msg, e); } // parse local output address port to integer try { this.localOutputPort = Integer.parseInt(localOutputPortS); } catch (NumberFormatException e) { throw ConnectionMessages.throwInvalidParameter(ClientSocketEndpoint.class, SocketEndpointConstants.PARAMETER_LOCAL_OUTPUT_PORT, localOutputPortS); } } // configure retry wait time String retryS = parameters.get(SocketEndpointConstants.PARAMETER_RETRY); if (retryS == null || retryS.length() == 0) { ConnectionMessages.warnUnsetParameter(ClientSocketEndpoint.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(ClientSocketEndpoint.class, SocketEndpointConstants.PARAMETER_RETRY, retryS); } } // configure connect on start String connectOnStartS = parameters.get(SocketEndpointConstants.PARAMETER_CONNECT_ON_START); if (StringHelper.isNotEmpty(connectOnStartS)) { this.connectOnStart = StringHelper.parseBoolean(connectOnStartS); } else { ConnectionMessages.warnUnsetParameter(ClientSocketEndpoint.class, SocketEndpointConstants.PARAMETER_CONNECT_ON_START, String.valueOf(SocketEndpointConstants.CONNECT_ON_START)); this.connectOnStart = SocketEndpointConstants.CONNECT_ON_START; } // configure closeAfterSend String closeAfterSendS = parameters.get(SocketEndpointConstants.PARAMETER_CLOSE_AFTER_SEND); if (StringHelper.isNotEmpty(closeAfterSendS)) { this.closeAfterSend = StringHelper.parseBoolean(closeAfterSendS); } else { ConnectionMessages.warnUnsetParameter(ClientSocketEndpoint.class, SocketEndpointConstants.PARAMETER_CLOSE_AFTER_SEND, String.valueOf(SocketEndpointConstants.CLOSE_AFTER_SEND)); this.closeAfterSend = SocketEndpointConstants.CLOSE_AFTER_SEND; } // configure if timeout on connection should be activated String useTimeoutS = parameters.get(SocketEndpointConstants.PARAMETER_USE_TIMEOUT); if (useTimeoutS == null || useTimeoutS.length() == 0) { ConnectionMessages.warnUnsetParameter(ClientSocketEndpoint.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(ClientSocketEndpoint.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(ClientSocketEndpoint.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(ClientSocketEndpoint.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 ClientSocketEndpoint} 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.localOutputAddress != null) { return this.localOutputAddress.getHostAddress() + StringHelper.COLON + this.localOutputPort; } return "0.0.0.0:0"; //$NON-NLS-1$ } /** * @return the uri as String to which this {@link ClientSocketEndpoint} is connecting to */ @Override public String getRemoteUri() { if (this.socket != null) { InetAddress remoteAddress = this.socket.getInetAddress(); return remoteAddress.getHostAddress() + StringHelper.COLON + this.socket.getPort(); } else if (this.remoteInputAddress != null) { return this.remoteInputAddress.getHostAddress() + StringHelper.COLON + this.remoteInputPort; } return this.remoteInputAddressS + StringHelper.COLON + this.remoteInputPort; } /** * Allows this end point to connect and then opens the connection to the defined remote server * * @see CommunicationEndpoint#start() */ @Override public void start() { if (!this.closed) { 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.connection.notifyStateChange(ConnectionState.INITIALIZED, ConnectionState.INITIALIZED.toString()); if (this.connectOnStart) { openConnection(); } } } /** * Closes this connection and disallows this end point to reconnect * * @see CommunicationEndpoint#stop() */ @Override public void stop() { this.closed = true; 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() { this.closed = true; closeConnection(); this.connection.notifyStateChange(ConnectionState.INITIALIZED, ConnectionState.INITIALIZED.toString()); } @Override public void simulate(IoMessage message) throws Exception { this.messageVisitor.simulate(message); } @Override public void send(IoMessage message) throws Exception { while (!this.closed && message.getState() == State.PENDING) { try { // open the connection if (!checkConnection()) openConnection(); // read and write to the client socket this.messageVisitor.visit(this.inputStream, this.outputStream, message); message.setState(State.DONE, State.DONE.name()); } catch (Exception e) { if (this.closed) { logger.warn("Socket has been closed!"); //$NON-NLS-1$ message.setState(State.FATAL, "Socket has been closed!"); //$NON-NLS-1$ } else { closeConnection(); logger.error(e.getMessage(), e); message.setState(State.FATAL, e.getLocalizedMessage()); this.connection.notifyStateChange(ConnectionState.BROKEN, e.getLocalizedMessage()); } } finally { if (this.closeAfterSend && !this.closed) { closeConnection(); } } } } }