/*
* Copyright 2001-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.BufferedOutputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.serializer.Deserializer;
import org.springframework.core.serializer.Serializer;
import org.springframework.integration.ip.tcp.serializer.SoftEndOfStreamException;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessagingException;
import org.springframework.scheduling.SchedulingAwareRunnable;
/**
* A TcpConnection that uses and underlying {@link Socket}.
*
* @author Gary Russell
* @since 2.0
*
*/
public class TcpNetConnection extends TcpConnectionSupport implements SchedulingAwareRunnable {
private final Socket socket;
private volatile OutputStream socketOutputStream;
private volatile long lastRead = System.currentTimeMillis();
private volatile long lastSend;
/**
* Constructs a TcpNetConnection for the socket.
* @param socket the socket
* @param server if true this connection was created as
* a result of an incoming request.
* @param lookupHost true if hostname lookup should be performed, otherwise the connection will
* be identified using the ip address.
* @param applicationEventPublisher the publisher to which OPEN, CLOSE and EXCEPTION events will
* be sent; may be null if event publishing is not required.
* @param connectionFactoryName the name of the connection factory creating this connection; used
* during event publishing, may be null, in which case "unknown" will be used.
*/
public TcpNetConnection(Socket socket, boolean server, boolean lookupHost,
ApplicationEventPublisher applicationEventPublisher, String connectionFactoryName) {
super(socket, server, lookupHost, applicationEventPublisher, connectionFactoryName);
this.socket = socket;
}
@Override
public boolean isLongLived() {
return true;
}
/**
* Closes this connection.
*/
@Override
public void close() {
this.setNoReadErrorOnClose(true);
try {
this.socket.close();
}
catch (Exception e) {
}
super.close();
}
@Override
public boolean isOpen() {
return !this.socket.isClosed();
}
@Override
@SuppressWarnings("unchecked")
public synchronized void send(Message<?> message) throws Exception {
if (this.socketOutputStream == null) {
int writeBufferSize = this.socket.getSendBufferSize();
this.socketOutputStream = new BufferedOutputStream(this.socket.getOutputStream(),
writeBufferSize > 0 ? writeBufferSize : 8192);
}
Object object = this.getMapper().fromMessage(message);
this.lastSend = System.currentTimeMillis();
try {
((Serializer<Object>) this.getSerializer()).serialize(object, this.socketOutputStream);
this.socketOutputStream.flush();
}
catch (Exception e) {
this.publishConnectionExceptionEvent(new MessagingException(message, "Failed TCP serialization", e));
this.closeConnection(true);
throw e;
}
if (logger.isDebugEnabled()) {
logger.debug(getConnectionId() + " Message sent " + message);
}
}
@Override
public Object getPayload() throws Exception {
return this.getDeserializer().deserialize(this.socket.getInputStream());
}
@Override
public int getPort() {
return this.socket.getPort();
}
@Override
public Object getDeserializerStateKey() {
try {
return this.socket.getInputStream();
}
catch (Exception e) {
return null;
}
}
@Override
public SSLSession getSslSession() {
if (this.socket instanceof SSLSocket) {
return ((SSLSocket) this.socket).getSession();
}
else {
return null;
}
}
/**
* If there is no listener,
* this method exits. When there is a listener, the method runs in a
* loop reading input from the connection's stream, data is converted
* to an object using the {@link Deserializer} and the listener's
* {@link TcpListener#onMessage(Message)} method is called.
*/
@Override
public void run() {
boolean okToRun = true;
if (logger.isDebugEnabled()) {
logger.debug(this.getConnectionId() + " Reading...");
}
while (okToRun) {
Message<?> message = null;
try {
message = this.getMapper().toMessage(this);
this.lastRead = System.currentTimeMillis();
}
catch (Exception e) {
this.publishConnectionExceptionEvent(e);
if (handleReadException(e)) {
okToRun = false;
}
}
if (okToRun && message != null) {
if (logger.isDebugEnabled()) {
logger.debug("Message received " + message);
}
try {
TcpListener listener = getListener();
if (listener == null) {
throw new NoListenerException("No listener");
}
listener.onMessage(message);
}
catch (NoListenerException nle) { // could also be thrown by an interceptor
if (logger.isWarnEnabled()) {
logger.warn("Unexpected message - no endpoint registered with connection interceptor: "
+ getConnectionId()
+ " - "
+ message);
}
}
catch (Exception e2) {
logger.error("Exception sending message: " + message, e2);
}
}
}
}
protected boolean handleReadException(Exception e) {
boolean doClose = true;
/*
* For client connections, we have to wait for 2 timeouts if the last
* send was within the current timeout.
*/
if (!this.isServer() && e instanceof SocketTimeoutException) {
long now = System.currentTimeMillis();
try {
int soTimeout = this.socket.getSoTimeout();
if (now - this.lastSend < soTimeout && now - this.lastRead < soTimeout * 2) {
doClose = false;
}
if (!doClose && logger.isDebugEnabled()) {
logger.debug("Skipping a socket timeout because we have a recent send " + this.getConnectionId());
}
}
catch (SocketException e1) {
logger.error("Error accessing soTimeout", e1);
}
}
if (doClose) {
boolean noReadErrorOnClose = this.isNoReadErrorOnClose();
closeConnection(true);
if (!(e instanceof SoftEndOfStreamException)) {
if (e instanceof SocketTimeoutException) {
if (logger.isDebugEnabled()) {
logger.debug("Closed socket after timeout:" + this.getConnectionId());
}
}
else {
if (noReadErrorOnClose) {
if (logger.isTraceEnabled()) {
logger.trace("Read exception " +
this.getConnectionId(), e);
}
else if (logger.isDebugEnabled()) {
logger.debug("Read exception " +
this.getConnectionId() + " " +
e.getClass().getSimpleName() +
":" + (e.getCause() != null ? e.getCause() + ":" : "") + e.getMessage());
}
}
else if (logger.isTraceEnabled()) {
logger.error("Read exception " +
this.getConnectionId(), e);
}
else {
logger.error("Read exception " +
this.getConnectionId() + " " +
e.getClass().getSimpleName() +
":" + (e.getCause() != null ? e.getCause() + ":" : "") + e.getMessage());
}
}
this.sendExceptionToListener(e);
}
}
return doClose;
}
}