/*
* Copyright 2002-2016 the original author or authors.
*
* 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 org.springframework.integration.ip.tcp.connection;
import java.io.EOFException;
import java.io.IOException;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.nio.channels.CancelledKeyException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.core.serializer.Deserializer;
import org.springframework.core.serializer.Serializer;
import org.springframework.integration.context.IntegrationObjectSupport;
import org.springframework.integration.ip.tcp.serializer.ByteArrayCrLfSerializer;
import org.springframework.messaging.MessagingException;
import org.springframework.util.Assert;
/**
* Base class for all connection factories.
*
* @author Gary Russell
* @since 2.0
*
*/
public abstract class AbstractConnectionFactory extends IntegrationObjectSupport
implements ConnectionFactory, ApplicationEventPublisherAware {
protected static final int DEFAULT_REPLY_TIMEOUT = 10000;
private static final int DEFAULT_NIO_HARVEST_INTERVAL = 2000;
private static final int DEFAULT_READ_DELAY = 100;
private volatile String host;
private volatile int port;
private volatile TcpListener listener;
private volatile TcpSender sender;
private volatile int soTimeout = -1;
private volatile int soSendBufferSize;
private volatile int soReceiveBufferSize;
private volatile boolean soTcpNoDelay;
private volatile int soLinger = -1; // don't set by default
private volatile boolean soKeepAlive;
private volatile int soTrafficClass = -1; // don't set by default
private volatile Executor taskExecutor;
private volatile boolean privateExecutor;
private volatile Deserializer<?> deserializer = new ByteArrayCrLfSerializer();
private volatile boolean deserializerSet;
private volatile Serializer<?> serializer = new ByteArrayCrLfSerializer();
private volatile TcpMessageMapper mapper = new TcpMessageMapper();
private volatile boolean mapperSet;
private volatile boolean singleUse;
private volatile boolean active;
private volatile TcpConnectionInterceptorFactoryChain interceptorFactoryChain;
private volatile boolean lookupHost = true;
private final Map<String, TcpConnectionSupport> connections = new ConcurrentHashMap<String, TcpConnectionSupport>();
private volatile TcpSocketSupport tcpSocketSupport = new DefaultTcpSocketSupport();
protected final Object lifecycleMonitor = new Object();
private volatile long nextCheckForClosedNioConnections;
private volatile int nioHarvestInterval = DEFAULT_NIO_HARVEST_INTERVAL;
private volatile ApplicationEventPublisher applicationEventPublisher;
private final BlockingQueue<PendingIO> delayedReads = new LinkedBlockingQueue<AbstractConnectionFactory.PendingIO>();
private volatile long readDelay = DEFAULT_READ_DELAY;
private volatile Integer sslHandshakeTimeout;
public AbstractConnectionFactory(int port) {
this.port = port;
}
public AbstractConnectionFactory(String host, int port) {
Assert.notNull(host, "host must not be null");
this.host = host;
this.port = port;
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
if (!this.deserializerSet && this.deserializer instanceof ApplicationEventPublisherAware) {
((ApplicationEventPublisherAware) this.deserializer)
.setApplicationEventPublisher(applicationEventPublisher);
}
}
public ApplicationEventPublisher getApplicationEventPublisher() {
return this.applicationEventPublisher;
}
/**
* Sets socket attributes on the socket.
* @param socket The socket.
* @throws SocketException Any SocketException.
*/
protected void setSocketAttributes(Socket socket) throws SocketException {
if (this.soTimeout >= 0) {
socket.setSoTimeout(this.soTimeout);
}
if (this.soSendBufferSize > 0) {
socket.setSendBufferSize(this.soSendBufferSize);
}
if (this.soReceiveBufferSize > 0) {
socket.setReceiveBufferSize(this.soReceiveBufferSize);
}
socket.setTcpNoDelay(this.soTcpNoDelay);
if (this.soLinger >= 0) {
socket.setSoLinger(true, this.soLinger);
}
if (this.soTrafficClass >= 0) {
socket.setTrafficClass(this.soTrafficClass);
}
socket.setKeepAlive(this.soKeepAlive);
this.tcpSocketSupport.postProcessSocket(socket);
}
/**
* @return the soTimeout
*/
public int getSoTimeout() {
return this.soTimeout;
}
/**
* @param soTimeout the soTimeout to set
*/
public void setSoTimeout(int soTimeout) {
this.soTimeout = soTimeout;
}
/**
* @return the soReceiveBufferSize
*/
public int getSoReceiveBufferSize() {
return this.soReceiveBufferSize;
}
/**
* @param soReceiveBufferSize the soReceiveBufferSize to set
*/
public void setSoReceiveBufferSize(int soReceiveBufferSize) {
this.soReceiveBufferSize = soReceiveBufferSize;
}
/**
* @return the soSendBufferSize
*/
public int getSoSendBufferSize() {
return this.soSendBufferSize;
}
/**
* @param soSendBufferSize the soSendBufferSize to set
*/
public void setSoSendBufferSize(int soSendBufferSize) {
this.soSendBufferSize = soSendBufferSize;
}
/**
* @return the soTcpNoDelay
*/
public boolean isSoTcpNoDelay() {
return this.soTcpNoDelay;
}
/**
* @param soTcpNoDelay the soTcpNoDelay to set
*/
public void setSoTcpNoDelay(boolean soTcpNoDelay) {
this.soTcpNoDelay = soTcpNoDelay;
}
/**
* @return the soLinger
*/
public int getSoLinger() {
return this.soLinger;
}
/**
* @param soLinger the soLinger to set
*/
public void setSoLinger(int soLinger) {
this.soLinger = soLinger;
}
/**
* @return the soKeepAlive
*/
public boolean isSoKeepAlive() {
return this.soKeepAlive;
}
/**
* @param soKeepAlive the soKeepAlive to set
*/
public void setSoKeepAlive(boolean soKeepAlive) {
this.soKeepAlive = soKeepAlive;
}
/**
* @return the soTrafficClass
*/
public int getSoTrafficClass() {
return this.soTrafficClass;
}
/**
* @param soTrafficClass the soTrafficClass to set
*/
public void setSoTrafficClass(int soTrafficClass) {
this.soTrafficClass = soTrafficClass;
}
/**
* Set the host; requires the factory to be stopped.
* @param host the host.
* @since 5.0
*/
public void setHost(String host) {
Assert.state(!isRunning(), "Cannot change the host while running");
this.host = host;
}
/**
* @return the host
*/
public String getHost() {
return this.host;
}
/**
* Set the port; requires the factory to be stopped.
* @param port the port.
* @since 5.0
*/
public void setPort(int port) {
Assert.state(!isRunning(), "Cannot change the host while running");
this.port = port;
}
/**
* @return the port
*/
public int getPort() {
return this.port;
}
/**
* @return the listener
*/
public TcpListener getListener() {
return this.listener;
}
/**
* @return the sender
*/
public TcpSender getSender() {
return this.sender;
}
/**
* @return the serializer
*/
public Serializer<?> getSerializer() {
return this.serializer;
}
/**
* @return the deserializer
*/
public Deserializer<?> getDeserializer() {
return this.deserializer;
}
/**
* @return the mapper
*/
public TcpMessageMapper getMapper() {
return this.mapper;
}
/**
* Registers a TcpListener to receive messages after
* the payload has been converted from the input data.
* @param listener the TcpListener.
*/
public void registerListener(TcpListener listener) {
Assert.isNull(this.listener, this.getClass().getName() +
" may only be used by one inbound adapter");
this.listener = listener;
}
/**
* Registers a TcpSender; for server sockets, used to
* provide connection information so a sender can be used
* to reply to incoming messages.
* @param sender The sender
*/
public void registerSender(TcpSender sender) {
Assert.isNull(this.sender, this.getClass().getName() +
" may only be used by one outbound adapter");
this.sender = sender;
}
/**
* @param taskExecutor the taskExecutor to set
*/
public void setTaskExecutor(Executor taskExecutor) {
this.taskExecutor = taskExecutor;
}
/**
*
* @param deserializer the deserializer to set
*/
public void setDeserializer(Deserializer<?> deserializer) {
this.deserializer = deserializer;
this.deserializerSet = true;
}
/**
*
* @param serializer the serializer to set
*/
public void setSerializer(Serializer<?> serializer) {
this.serializer = serializer;
}
/**
*
* @param mapper the mapper to set; defaults to a {@link TcpMessageMapper}
*/
public void setMapper(TcpMessageMapper mapper) {
this.mapper = mapper;
this.mapperSet = true;
}
/**
* @return the singleUse
*/
public boolean isSingleUse() {
return this.singleUse;
}
/**
* If true, sockets created by this factory will be used once.
* @param singleUse The singleUse to set.
*/
public void setSingleUse(boolean singleUse) {
this.singleUse = singleUse;
}
/**
* If true, sockets created by this factory will be reused.
* Inverse of {@link #setSingleUse(boolean)}.
* @param leaveOpen The keepOpen to set.
* @since 5.0
*/
public void setLeaveOpen(boolean leaveOpen) {
this.singleUse = !leaveOpen;
}
public void setInterceptorFactoryChain(TcpConnectionInterceptorFactoryChain interceptorFactoryChain) {
this.interceptorFactoryChain = interceptorFactoryChain;
}
/**
* If true, DNS reverse lookup is done on the remote ip address.
* Default true.
* @param lookupHost the lookupHost to set
*/
public void setLookupHost(boolean lookupHost) {
this.lookupHost = lookupHost;
}
/**
* @return the lookupHost
*/
public boolean isLookupHost() {
return this.lookupHost;
}
/**
* How often we clean up closed NIO connections if soTimeout is 0.
* Ignored when {@code soTimeout > 0} because the clean up
* process is run as part of the timeout handling.
* Default 2000 milliseconds.
* @param nioHarvestInterval The interval in milliseconds.
*/
public void setNioHarvestInterval(int nioHarvestInterval) {
Assert.isTrue(nioHarvestInterval > 0, "NIO Harvest interval must be > 0");
this.nioHarvestInterval = nioHarvestInterval;
}
/**
* Set the handshake timeout used when waiting for SSL handshake data; only applies
* to SSL connections, when using NIO.
* @param sslHandshakeTimeout the timeout.
* @since 4.3.6
*/
public void setSslHandshakeTimeout(int sslHandshakeTimeout) {
this.sslHandshakeTimeout = sslHandshakeTimeout;
}
/**
* @return the handshake timeout.
* @see #setSslHandshakeTimeout(int)
* @since 4.3.6
*/
protected Integer getSslHandshakeTimeout() {
return this.sslHandshakeTimeout;
}
protected BlockingQueue<PendingIO> getDelayedReads() {
return this.delayedReads;
}
protected long getReadDelay() {
return this.readDelay;
}
/**
* The delay (in milliseconds) before retrying a read after the previous attempt
* failed due to insufficient threads. Default 100.
* @param readDelay the readDelay to set.
*/
public void setReadDelay(long readDelay) {
Assert.isTrue(readDelay > 0, "'readDelay' must be positive");
this.readDelay = readDelay;
}
@Override
protected void onInit() throws Exception {
super.onInit();
if (!this.mapperSet) {
this.mapper.setBeanFactory(this.getBeanFactory());
}
}
@Override
public void start() {
if (logger.isInfoEnabled()) {
logger.info("started " + this);
}
}
/**
* Creates a taskExecutor (if one was not provided).
* @return The executor.
*/
protected Executor getTaskExecutor() {
if (!this.active) {
throw new MessagingException("Connection Factory not started");
}
synchronized (this.lifecycleMonitor) {
if (this.taskExecutor == null) {
this.privateExecutor = true;
this.taskExecutor = Executors.newCachedThreadPool();
}
return this.taskExecutor;
}
}
/**
* Stops the server.
*/
@Override
public void stop() {
this.active = false;
synchronized (this.connections) {
Iterator<Entry<String, TcpConnectionSupport>> iterator = this.connections.entrySet().iterator();
while (iterator.hasNext()) {
TcpConnection connection = iterator.next().getValue();
connection.close();
iterator.remove();
}
}
synchronized (this.lifecycleMonitor) {
if (this.privateExecutor) {
ExecutorService executorService = (ExecutorService) this.taskExecutor;
executorService.shutdown();
try {
if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
logger.debug("Forcing executor shutdown");
executorService.shutdownNow();
if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
logger.debug("Executor failed to shutdown");
}
}
}
catch (InterruptedException e) {
executorService.shutdownNow();
Thread.currentThread().interrupt();
}
finally {
this.taskExecutor = null;
this.privateExecutor = false;
}
}
}
if (logger.isInfoEnabled()) {
logger.info("stopped " + this);
}
}
protected TcpConnectionSupport wrapConnection(TcpConnectionSupport connection) throws Exception {
try {
if (this.interceptorFactoryChain == null) {
return connection;
}
TcpConnectionInterceptorFactory[] interceptorFactories =
this.interceptorFactoryChain.getInterceptorFactories();
if (interceptorFactories == null) {
return connection;
}
for (TcpConnectionInterceptorFactory factory : interceptorFactories) {
TcpConnectionInterceptorSupport wrapper = factory.getInterceptor();
wrapper.setTheConnection(connection);
// if no ultimate listener or sender, register each wrapper in turn
if (this.listener == null) {
connection.registerListener(wrapper);
}
if (this.sender == null) {
connection.registerSender(wrapper);
}
connection = wrapper;
}
return connection;
}
finally {
this.addConnection(connection);
}
}
/**
*
* Times out any expired connections then, if {@code selectionCount > 0},
* processes the selected keys.
* Removes closed connections from the connections field, and from the connections parameter.
*
* @param selectionCount Number of IO Events, if 0 we were probably woken up by a close.
* @param selector The selector.
* @param server The server socket channel.
* @param connections Map of connections.
* @throws IOException Any IOException.
*/
protected void processNioSelections(int selectionCount, final Selector selector, ServerSocketChannel server,
Map<SocketChannel, TcpNioConnection> connections) throws IOException {
final long now = System.currentTimeMillis();
rescheduleDelayedReads(selector, now);
if (this.soTimeout > 0 ||
now >= this.nextCheckForClosedNioConnections ||
selectionCount == 0) {
this.nextCheckForClosedNioConnections = now + this.nioHarvestInterval;
Iterator<Entry<SocketChannel, TcpNioConnection>> it = connections.entrySet().iterator();
while (it.hasNext()) {
SocketChannel channel = it.next().getKey();
if (!channel.isOpen()) {
logger.debug("Removing closed channel");
it.remove();
}
else if (this.soTimeout > 0) {
TcpNioConnection connection = connections.get(channel);
if (now - connection.getLastRead() >= this.soTimeout) {
/*
* For client connections, we have to wait for 2 timeouts if the last
* send was within the current timeout.
*/
if (!connection.isServer() &&
now - connection.getLastSend() < this.soTimeout &&
now - connection.getLastRead() < this.soTimeout * 2) {
if (logger.isDebugEnabled()) {
logger.debug("Skipping a connection timeout because we have a recent send " +
connection.getConnectionId());
}
}
else {
if (logger.isWarnEnabled()) {
logger.warn("Timing out TcpNioConnection " + connection.getConnectionId());
}
SocketTimeoutException exception = new SocketTimeoutException("Timing out connection");
connection.publishConnectionExceptionEvent(exception);
connection.timeout();
connection.sendExceptionToListener(exception);
}
}
}
}
}
this.harvestClosedConnections();
if (logger.isTraceEnabled()) {
if (this.host == null) {
logger.trace("Port " + this.port + " SelectionCount: " + selectionCount);
}
else {
logger.trace("Host " + this.host + " port " + this.port + " SelectionCount: " + selectionCount);
}
}
if (selectionCount > 0) {
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()) {
final SelectionKey key = iterator.next();
iterator.remove();
try {
if (!key.isValid()) {
logger.debug("Selection key no longer valid");
}
else if (key.isReadable()) {
key.interestOps(key.interestOps() - SelectionKey.OP_READ);
final TcpNioConnection connection;
connection = (TcpNioConnection) key.attachment();
connection.setLastRead(System.currentTimeMillis());
try {
this.taskExecutor.execute(() -> {
boolean delayed = false;
try {
connection.readPacket();
}
catch (RejectedExecutionException e1) {
delayRead(selector, now, key);
delayed = true;
}
catch (Exception e2) {
if (connection.isOpen()) {
logger.error("Exception on read " +
connection.getConnectionId() + " " +
e2.getMessage());
connection.close();
}
else {
logger.debug("Connection closed");
}
}
if (!delayed) {
if (key.channel().isOpen()) {
key.interestOps(SelectionKey.OP_READ);
selector.wakeup();
}
else {
connection.sendExceptionToListener(new EOFException("Connection is closed"));
}
}
});
}
catch (RejectedExecutionException e) {
delayRead(selector, now, key);
}
}
else if (key.isAcceptable()) {
try {
doAccept(selector, server, now);
}
catch (Exception e) {
logger.error("Exception accepting new connection", e);
}
}
else {
logger.error("Unexpected key: " + key);
}
}
catch (CancelledKeyException e) {
if (logger.isDebugEnabled()) {
logger.debug("Selection key " + key + " cancelled");
}
}
catch (Exception e) {
logger.error("Exception on selection key " + key, e);
}
}
}
}
protected void delayRead(Selector selector, long now, final SelectionKey key) {
TcpNioConnection connection = (TcpNioConnection) key.attachment();
if (!this.delayedReads.add(new PendingIO(now, key))) { // should never happen - unbounded queue
logger.error("Failed to delay read; closing " + connection.getConnectionId());
connection.close();
}
else {
if (logger.isDebugEnabled()) {
logger.debug("No threads available, delaying read for " + connection.getConnectionId());
}
// wake the selector in case it is currently blocked, and waiting for longer than readDelay
selector.wakeup();
}
}
/**
* If any reads were delayed due to insufficient threads, reschedule them if
* the readDelay has passed.
* @param selector the selector to wake if necessary.
* @param now the current time.
*/
private void rescheduleDelayedReads(Selector selector, long now) {
boolean wakeSelector = false;
try {
while (this.delayedReads.size() > 0) {
if (this.delayedReads.peek().failedAt + this.readDelay < now) {
PendingIO pendingRead = this.delayedReads.take();
if (pendingRead.key.channel().isOpen()) {
pendingRead.key.interestOps(SelectionKey.OP_READ);
wakeSelector = true;
if (logger.isDebugEnabled()) {
logger.debug("Rescheduling delayed read for " + ((TcpNioConnection) pendingRead.key.attachment()).getConnectionId());
}
}
else {
((TcpNioConnection) pendingRead.key.attachment()).sendExceptionToListener(new EOFException("Connection is closed"));
}
}
else {
// remaining delayed reads have not expired yet.
break;
}
}
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
finally {
if (wakeSelector) {
selector.wakeup();
}
}
}
/**
* @param selector The selector.
* @param server The server socket channel.
* @param now The current time.
* @throws IOException Any IOException.
*/
protected void doAccept(final Selector selector, ServerSocketChannel server, long now) throws IOException {
throw new UnsupportedOperationException("Nio server factory must override this method");
}
protected void addConnection(TcpConnectionSupport connection) {
synchronized (this.connections) {
if (!this.active) {
connection.close();
return;
}
this.connections.put(connection.getConnectionId(), connection);
if (logger.isDebugEnabled()) {
logger.debug(getComponentName() + ": Added new connection: " + connection.getConnectionId());
}
}
}
/**
* Cleans up this.connections by removing any closed connections.
* @return a list of open connection ids.
*/
private List<String> removeClosedConnectionsAndReturnOpenConnectionIds() {
synchronized (this.connections) {
List<String> openConnectionIds = new ArrayList<String>();
Iterator<Entry<String, TcpConnectionSupport>> iterator = this.connections.entrySet().iterator();
while (iterator.hasNext()) {
Entry<String, TcpConnectionSupport> entry = iterator.next();
TcpConnectionSupport connection = entry.getValue();
if (!connection.isOpen()) {
iterator.remove();
if (logger.isDebugEnabled()) {
logger.debug(getComponentName() + ": Removed closed connection: " + connection.getConnectionId());
}
}
else {
openConnectionIds.add(entry.getKey());
if (logger.isTraceEnabled()) {
logger.trace(getComponentName() + ": Connection is open: " + connection.getConnectionId());
}
}
}
return openConnectionIds;
}
}
/**
* Cleans up this.connections by removing any closed connections.
*/
protected void harvestClosedConnections() {
this.removeClosedConnectionsAndReturnOpenConnectionIds();
}
@Override
public boolean isRunning() {
return this.active;
}
/**
* @return the active
*/
protected boolean isActive() {
return this.active;
}
/**
* @param active the active to set
*/
protected void setActive(boolean active) {
this.active = active;
}
protected void checkActive() throws IOException {
if (!this.isActive()) {
throw new IOException(this + " connection factory has not been started");
}
}
protected TcpSocketSupport getTcpSocketSupport() {
return this.tcpSocketSupport;
}
public void setTcpSocketSupport(TcpSocketSupport tcpSocketSupport) {
Assert.notNull(tcpSocketSupport, "TcpSocketSupport must not be null");
this.tcpSocketSupport = tcpSocketSupport;
}
/**
* Returns a list of (currently) open {@link TcpConnection} connection ids; allows,
* for example, broadcast operations to all open connections.
* @return the list of connection ids.
*/
public List<String> getOpenConnectionIds() {
return Collections.unmodifiableList(this.removeClosedConnectionsAndReturnOpenConnectionIds());
}
/**
* Close a connection with the specified connection id.
* @param connectionId the connection id.
* @return true if the connection was closed.
*/
public boolean closeConnection(String connectionId) {
Assert.notNull(connectionId, "'connectionId' to close must not be null");
// closed connections are removed from #connections in #harvestClosedConnections()
synchronized (this.connections) {
boolean closed = false;
TcpConnectionSupport connection = this.connections.get(connectionId);
if (connection != null) {
try {
connection.close();
closed = true;
}
catch (Exception e) {
if (logger.isDebugEnabled()) {
logger.debug("Failed to close connection " + connectionId, e);
}
connection.publishConnectionExceptionEvent(e);
}
}
return closed;
}
}
@Override
public String toString() {
return super.toString()
+ (this.host != null ? ", host=" + this.host : "")
+ ", port=" + getPort();
}
private static final class PendingIO {
private final long failedAt;
private final SelectionKey key;
private PendingIO(long failedAt, SelectionKey key) {
this.failedAt = failedAt;
this.key = key;
}
}
}