/*
* Copyright 2013 Thomas Bocek
*
* 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.tomp2p.connection2;
import java.util.LinkedHashMap;
import java.util.Map;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.concurrent.GenericFutureListener;
import net.tomp2p.futures.Cancel;
import net.tomp2p.futures.FutureResponse;
import net.tomp2p.message.Message2;
import net.tomp2p.message.TomP2PCumulationTCP;
import net.tomp2p.message.TomP2POutbound;
import net.tomp2p.message.TomP2PSinglePacketUDP;
import net.tomp2p.peers.PeerStatusListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The class that sends out messages.
*
* @author Thomas Bocek
*
*/
public class Sender {
private static final Logger LOG = LoggerFactory.getLogger(Sender.class);
private final PeerStatusListener[] peerStatusListeners;
private final ChannelClientConfiguration channelClientConfiguration;
/**
* Creates a new sender with the listeners for offline peers.
*
* @param peerStatusListeners
* The listener for offline peers
* @param channelClientConfiguration
* The configuration used to get the signature factory
*/
public Sender(final PeerStatusListener[] peerStatusListeners,
final ChannelClientConfiguration channelClientConfiguration) {
this.peerStatusListeners = peerStatusListeners;
this.channelClientConfiguration = channelClientConfiguration;
}
/**
* Send a message via TCP.
*
* @param handler
* The handler to deal with a reply message
* @param futureResponse
* The future to set the response
* @param message
* The message to send
* @param channelCreator
* The channel creator for the UPD channel
* @param idleTCPSeconds
* The idle time of a message until we fail
* @param connectTimeoutMillis
* The idle we set for the connection setup
*/
public void sendTCP(final SimpleChannelInboundHandler<Message2> handler,
final FutureResponse futureResponse, final Message2 message, final ChannelCreator channelCreator,
final int idleTCPSeconds, final int connectTimeoutMillis, final PeerConnection peerConnection) {
// no need to continue if we already finished
if (futureResponse.isCompleted()) {
return;
}
final TimeoutFactory timeoutHandler = createTimeoutHandler(futureResponse, idleTCPSeconds,
handler == null);
final ChannelFuture channelFuture;
if (peerConnection != null && peerConnection.channelFuture() != null
&& peerConnection.channelFuture().channel().isActive()) {
ChannelHandler[] c = timeoutHandler.twoTimeoutHandlers();
channelFuture = peerConnection.channelFuture();
channelFuture.channel().pipeline().replace("timeout0", "timeout0", c[0]);
channelFuture.channel().pipeline().replace("timeout1", "timeout1", c[1]);
channelFuture
.channel()
.pipeline()
.replace("decoder", "decoder",
new TomP2PCumulationTCP(channelClientConfiguration.signatureFactory()));
channelFuture
.channel()
.pipeline()
.replace("encoder", "encoder",
new TomP2POutbound(false, channelClientConfiguration.signatureFactory()));
channelFuture.channel().pipeline().replace("handler", "handler", handler);
} else {
final int nrTCPHandlers;
final ChannelHandler[] c;
if (timeoutHandler != null) {
c = timeoutHandler.twoTimeoutHandlers();
nrTCPHandlers = 7; // 5 / 0.75;
} else {
c = null;
nrTCPHandlers = 3; // 2 / 0.75;
}
final Map<String, ChannelHandler> handlers = new LinkedHashMap<String, ChannelHandler>(
nrTCPHandlers);
if (timeoutHandler != null) {
handlers.put("timeout0", c[0]);
handlers.put("timeout1", c[1]);
}
handlers.put("decoder", new TomP2PCumulationTCP(channelClientConfiguration.signatureFactory()));
handlers.put("encoder", new TomP2POutbound(false, channelClientConfiguration.signatureFactory()));
if (timeoutHandler != null) {
handlers.put("handler", handler);
}
channelFuture = channelCreator.createTCP(message.getRecipient().createSocketUDP(),
connectTimeoutMillis, handlers);
if (peerConnection != null) {
peerConnection.channelFuture(channelFuture);
}
}
if (channelFuture == null) {
futureResponse.setFailed("could not create a TCP channel");
} else {
afterConnect(futureResponse, message, channelFuture, handler == null);
}
}
/**
* Send a message via UDP.
*
* @param handler
* The handler to deal with a reply message
* @param futureResponse
* The future to set the response
* @param message
* The message to send
* @param channelCreator
* The channel creator for the UPD channel
* @param idleUDPSeconds
* The idle time of a message until we fail
* @param broadcast
* True to send via layer 2 broadcast
*/
public void sendUDP(final SimpleChannelInboundHandler<Message2> handler,
final FutureResponse futureResponse, final Message2 message, final ChannelCreator channelCreator,
final int idleUDPSeconds, final boolean broadcast) {
// no need to continue if we already finished
if (futureResponse.isCompleted()) {
return;
}
boolean isFireAndForget = handler == null;
final TimeoutFactory timeoutHandler = createTimeoutHandler(futureResponse, idleUDPSeconds,
isFireAndForget);
final Map<String, ChannelHandler> handlers;
if (isFireAndForget) {
final int nrTCPHandlers = 3; // 2 / 0.75
handlers = new LinkedHashMap<String, ChannelHandler>(nrTCPHandlers);
} else {
final int nrTCPHandlers = 7; // 5 / 0.75
handlers = new LinkedHashMap<String, ChannelHandler>(nrTCPHandlers);
ChannelHandler[] c = timeoutHandler.twoTimeoutHandlers();
handlers.put("timeout0", c[0]);
handlers.put("timeout1", c[1]);
}
handlers.put("decoder", new TomP2PSinglePacketUDP(channelClientConfiguration.signatureFactory()));
handlers.put("encoder", new TomP2POutbound(false, channelClientConfiguration.signatureFactory()));
if (!isFireAndForget) {
handlers.put("handler", handler);
}
final ChannelFuture channelFuture = channelCreator.createUDP(
message.getRecipient().createSocketUDP(), broadcast, handlers);
if (channelFuture == null) {
futureResponse.setFailed("could not create a UDP channel");
} else {
afterConnect(futureResponse, message, channelFuture, handler == null);
}
}
/**
* Create a timeout handler or null if its a fire and forget. In this case we don't expect a reply and we don't need
* a timeout.
*
* @param futureResponse
* The future to set the response
* @param idleMillis
* The timeout
* @param fireAndForget
* True, if we don't expect a message
* @return The timeout creator that will create timeout handlers
*/
private TimeoutFactory createTimeoutHandler(final FutureResponse futureResponse, final int idleMillis,
final boolean fireAndForget) {
return fireAndForget ? null : new TimeoutFactory(futureResponse, idleMillis, peerStatusListeners);
}
/**
* After connecting, we check if the connect was successful.
*
* @param futureResponse
* The future to set the response
* @param message
* The message to send
* @param channelFuture
* the future of the connect
* @param fireAndForget
* True, if we don't expect a message
*/
private void afterConnect(final FutureResponse futureResponse, final Message2 message,
final ChannelFuture channelFuture, final boolean fireAndForget) {
final Cancel connectCancel = createCancel(channelFuture);
futureResponse.addCancel(connectCancel);
channelFuture.addListener(new GenericFutureListener<ChannelFuture>() {
@Override
public void operationComplete(final ChannelFuture future) throws Exception {
futureResponse.removeCancel(connectCancel);
if (future.isSuccess()) {
futureResponse.setProgressHandler(new ProgresHandler() {
@Override
public void progres() {
final ChannelFuture writeFuture = future.channel().writeAndFlush(message);
afterSend(writeFuture, futureResponse, fireAndForget);
}
});
// this needs to be called first before all other progress
futureResponse.progressFirst();
} else {
futureResponse.setFailed("Channel creation failed " + future.cause());
}
}
});
}
/**
* After sending, we check if the write was successful or if it was a fire and forget.
*
* @param writeFuture
* The future of the write operation. Can be UDP or TCP
* @param futureResponse
* The future to set the response
* @param fireAndForget
* True, if we don't expect a message
*/
private void afterSend(final ChannelFuture writeFuture, final FutureResponse futureResponse,
final boolean fireAndForget) {
final Cancel writeCancel = createCancel(writeFuture);
writeFuture.addListener(new GenericFutureListener<ChannelFuture>() {
@Override
public void operationComplete(final ChannelFuture future) throws Exception {
futureResponse.removeCancel(writeCancel);
if (!future.isSuccess()) {
reportFailed(futureResponse, future.channel().close(), future.cause());
if (LOG.isWarnEnabled()) {
LOG.warn("Failed to write channel the request " + futureResponse.getRequest());
future.cause().printStackTrace();
}
}
if (fireAndForget) {
reportMessage(futureResponse, future.channel().close(), null);
}
}
});
}
/**
* Report a failure after the channel was closed.
*
* @param futureResponse
* The future to set the response
* @param close
* The close future
* @param cause
* The response message
*/
private void reportFailed(final FutureResponse futureResponse, final ChannelFuture close,
final Throwable cause) {
close.addListener(new GenericFutureListener<ChannelFuture>() {
@Override
public void operationComplete(final ChannelFuture arg0) throws Exception {
futureResponse.setFailed(cause);
}
});
}
/**
* Report a successful response after the channel was closed.
*
* @param futureResponse
* The future to set the response
* @param close
* The close future
* @param responseMessage
* The response message
*/
private void reportMessage(final FutureResponse futureResponse, final ChannelFuture close,
final Message2 responseMessage) {
close.addListener(new GenericFutureListener<ChannelFuture>() {
@Override
public void operationComplete(final ChannelFuture arg0) throws Exception {
futureResponse.setResponse(responseMessage);
}
});
}
/**
* @param channelFuture
* The channel future that can be canceled
* @return Create a cancel class for the channel future
*/
private static Cancel createCancel(final ChannelFuture channelFuture) {
return new Cancel() {
@Override
public void cancel() {
channelFuture.cancel(true);
}
};
}
}