/*
* Copyright 2009 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.connection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import java.util.ArrayList;
import java.util.List;
import net.tomp2p.futures.BaseFutureAdapter;
import net.tomp2p.futures.FutureDone;
import net.tomp2p.futures.FutureResponse;
import net.tomp2p.futures.Futures;
import net.tomp2p.message.Message;
import net.tomp2p.message.MessageID;
import net.tomp2p.peers.PeerAddress;
import net.tomp2p.peers.PeerSocketAddress;
import net.tomp2p.peers.PeerStatusListener;
import net.tomp2p.rpc.RPC;
import net.tomp2p.utils.Pair;
import net.tomp2p.utils.Utils;
/**
* Is able to send TCP and UDP messages (as a request) and processes incoming responses. It is important that
* this class handles
* close() because if we shutdown the connections, then we need to notify the futures. In case of errors set
* the peer to
* offline.
*
* @author Thomas Bocek
*/
public class RequestHandler extends SimpleChannelInboundHandler<Message> {
private static final Logger LOG = LoggerFactory.getLogger(RequestHandler.class);
// The future response which is currently being waited for
private final FutureResponse futureResponse;
// The node with which this request handler is associated with
private final PeerBean peerBean;
private final ConnectionBean connectionBean;
private final Message message;
private final MessageID sendMessageID;
// modifiable variables
private final int idleTCPMillis; // = ConnectionBean.DEFAULT_TCP_IDLE_SECONDS;
private final int idleUDPMillis; // = ConnectionBean.DEFAULT_UDP_IDLE_SECONDS;
private final int connectionTimeoutTCPMillis; // = ConnectionBean.DEFAULT_CONNECTION_TIMEOUT_TCP;
private final int heartBeatSeconds; // = ConnectionBean.DEFAULT_SLOW_RESPONSE_TIMEOUT_SECONDS;
private final PeerAddress recipient;
private final PeerAddress sender;
private final boolean isReflected;
private final boolean isKeepAlive;
private volatile PeerConnection peerConnection;
/**
* Creates a request handler that can send TCP and UDP messages.
*
* @param futureResponse
* The future that will be called when we get an answer
* @param peerBean
* The peer bean
* @param connectionBean
* The connection bean
* @param configuration
* the client-side connection configuration
*/
public RequestHandler(final FutureResponse futureResponse, final PeerBean peerBean,
final ConnectionBean connectionBean, final ConnectionConfiguration configuration) {
this.peerBean = peerBean;
this.connectionBean = connectionBean;
this.futureResponse = futureResponse;
this.message = futureResponse.request();
this.sendMessageID = new MessageID(message);
this.idleTCPMillis = configuration.idleTCPMillis();
this.idleUDPMillis = configuration.idleUDPMillis();
this.connectionTimeoutTCPMillis = configuration.connectionTimeoutTCPMillis();
this.heartBeatSeconds = configuration.heartBeatSeconds();
// NAT reflection - rewrite recipient if we found a local address for
// the recipient
Pair<PeerAddress, Boolean> pair = handleReflection(message);
this.recipient = pair.element0();
this.sender = peerBean.serverPeerAddress();
this.isReflected = pair.element1();
this.isKeepAlive = message.isKeepAlive();
}
private Pair<PeerAddress, Boolean> handleReflection(final Message message) {
PeerSocketAddress.PeerSocket4Address reflectedRecipient = Utils.natReflection(message.recipient(), peerBean().serverPeerAddress());
if(reflectedRecipient != null) {
LOG.debug("reflect recipient {}", message);
return new Pair<PeerAddress, Boolean>(message.recipient().withIpv4Socket(reflectedRecipient), true);
}
return new Pair<PeerAddress, Boolean>(message.recipient(), false);
}
/**
* @return The future response that will be called when we get an answer
*/
public FutureResponse futureResponse() {
return futureResponse;
}
/**
* @return The peer bean
*/
public PeerBean peerBean() {
return peerBean;
}
/**
* @return The connection bean
*/
public ConnectionBean connectionBean() {
return connectionBean;
}
/**
* @return The time that a TCP connection can be idle
*/
public int idleTCPMillis() {
return idleTCPMillis;
}
/**
* @return The time that a UDP connection can be idle
*/
public int idleUDPMillis() {
return idleUDPMillis;
}
/**
* @return The time a TCP connection is allowed to be established
*/
public int connectionTimeoutTCPMillis() {
return connectionTimeoutTCPMillis;
}
/**
* @return The time when a slow response time outs
*/
public int heartBeatSeconds() {
return heartBeatSeconds;
}
public FutureResponse sendUDP(final ChannelCreator channelCreator) {
return sendUDP(channelCreator, recipient, message, futureResponse);
}
/**
* Sends a UDP message and expects a reply.
*
* @param channelCreator
* The channel creator will create a UDP connection
* @return The future that was added in the constructor
*/
private FutureResponse sendUDP(final ChannelCreator channelCreator, PeerAddress recipient, Message message,
FutureResponse futureResponse) {
final PeerConnection peerConnection = PeerConnection.newPeerConnectionUDP(channelCreator, recipient, idleTCPMillis);
final SendBehavior.SendMethod sendMethod = connectionBean.connect().connectUDP(this, channelCreator, sender,
peerConnection, isReflected, isReflected);
this.peerConnection = peerConnection;
switch(sendMethod) {
case DIRECT:
connectionBean.sender().sendMessage(futureResponse, message, peerConnection, false);
break;
case SELF:
connectionBean.sender().sendSelf(futureResponse, message);
break;
}
return futureResponse;
}
/**
* Broadcasts a UDP message (layer 2) and expects a response.
*
* @param channelCreator
* The channel creator will create a UDP connection
* @return The future that was added in the constructor
*/
public FutureResponse fireAndForgetUDP(final ChannelCreator channelCreator) {
final PeerConnection peerConnection = PeerConnection.newPeerConnectionUDP(channelCreator, recipient, idleTCPMillis);
final SendBehavior.SendMethod sendMethod = connectionBean.connect().connectUDP(this, channelCreator, sender,
peerConnection, isReflected, isReflected);
this.peerConnection = peerConnection;
switch(sendMethod) {
case DIRECT:
connectionBean.sender().sendMessage(futureResponse, message, peerConnection, true);
break;
case SELF:
connectionBean.sender().sendSelf(futureResponse, message);
break;
}
return futureResponse;
}
/**
* Sends a UDP message and doesn't expect a response.
*
* @param channelCreator
* The channel creator will create a UDP connection
* @return The future that was added in the constructor
*/
public FutureResponse fireAndForgetBroadcastUDP(final ChannelCreator channelCreator) {
//connectionBean.sender().sendUDP(null, futureResponse, message, channelCreator, 0, true);
return futureResponse;
}
/**
* Sends a TCP message and expects a response.
*
* @param channelCreator
* The channel creator will create a TCP connection
* @return The future that was added in the constructor
*/
public FutureResponse sendTCP(final ChannelCreator channelCreator) {
final PeerConnection peerConnection;
if(isKeepAlive) {
peerConnection = PeerConnection.newPeerConnectionTCP(channelCreator, recipient, idleTCPMillis);
} else {
peerConnection = PeerConnection.newPermanentPeerConnectionTCP(channelCreator, recipient, idleTCPMillis, heartBeatSeconds);
}
return sendTCP(peerConnection);
}
public FutureResponse sendTCP(final PeerConnection peerConnection) {
final SendBehavior.SendMethod sendMethod = connectionBean.connect().connectTCP(
this, connectionTimeoutTCPMillis, sender, peerConnection, isReflected);
this.peerConnection = peerConnection;
switch(sendMethod) {
case DIRECT:
connectionBean.sender().sendMessage(futureResponse, message, peerConnection, false);
break;
case SELF:
connectionBean.sender().sendSelf(futureResponse, message);
break;
case EXISTING_CONNECTION:
connectionBean.sender().sendTCPPeerConnection(peerConnection, this);
connectionBean.sender().sendMessage(futureResponse, message, peerConnection, false);
break;
case RCON:
if(peerBean.natHandler() == null) {
futureResponse.failed("cannot initiate RCON, no NAT handler");
break;
}
//TODO: finish dispatcher
List<FutureResponse> futures = peerBean.natHandler().handleRcon(connectionBean.dispatcher(), message,
futureResponse, peerConnection, idleUDPMillis, connectionBean.timer());
for(FutureResponse future:futures) {
//if all futures fail, natHandler will handle this
sendUDP(peerConnection.channelCreator(), future.request().recipient(),
future.request(), future);
}
break;
case HOLEPUNCHING:
if(peerBean.natHandler() == null) {
futureResponse.failed("cannot initiate HOLEP_RELAY, no NAT handler");
break;
}
//TODO: finish HOLEPUNCHING
break;
case RELAY:
if(peerBean.natHandler() == null) {
futureResponse.failed("cannot initiate HOLEP_RELAY, no NAT handler");
break;
}
//TODO: finish RELAY
break;
case CANNOT_CREATE_TCP:
futureResponse.failed("cannot create TCP channel");
break;
}
return futureResponse;
}
@Override
public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cause) throws Exception {
LOG.debug("Error originating from {}. Cause {}.", futureResponse.request(), cause);
if (futureResponse.isCompleted()) {
LOG.warn("Got exception, but ignored it. (FutureResponse completed.): {}.",
futureResponse.failedReason());
} else {
if (LOG.isDebugEnabled()) {
LOG.debug("Exception caught, but handled properly: " + cause.toString());
}
if (cause instanceof PeerException) {
PeerException pe = (PeerException) cause;
if (pe.abortCause() != PeerException.AbortCause.USER_ABORT) {
// do not force if we ran into a timeout, the peer may be
// busy
synchronized (peerBean.peerStatusListeners()) {
for (PeerStatusListener peerStatusListener : peerBean.peerStatusListeners()) {
peerStatusListener.peerFailed(futureResponse.request().recipient(), pe);
}
}
LOG.debug("Removed from map. Cause: {}. Message: {}.", pe.toString(), message);
} else {
LOG.warn("Error in request.", cause);
}
} else {
synchronized (peerBean.peerStatusListeners()) {
for (PeerStatusListener peerStatusListener : peerBean.peerStatusListeners()) {
peerStatusListener.peerFailed(futureResponse.request().recipient(), new PeerException(cause));
}
}
}
}
//futureResponse.failedLater(cause);
ctx.close();
peerConnection.closeListener().failAfterSemaphoreRelease(futureResponse, cause);
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
IdleStateEvent e = (IdleStateEvent) evt;
if (e.state() == IdleState.READER_IDLE) {
if(isKeepAlive) {
final Message ping = createMessage(recipient, RPC.Commands.PING.getNr(), Message.Type.REQUEST_5);
ctx.writeAndFlush(ping);
} else {
exceptionCaught(ctx, new PeerException(PeerException.AbortCause.TIMEOUT, "timetout in request"));
}
}
}
}
public Message createMessage(final PeerAddress recipient, final byte name, final Message.Type type) {
return new Message().recipient(recipient).sender(peerBean().serverPeerAddress())
.command(name).type(type).version(connectionBean().p2pId());
}
@Override
protected void channelRead0(final ChannelHandlerContext ctx, final Message responseMessage)
throws Exception {
MessageID recvMessageID = new MessageID(responseMessage);
// Error handling
if (responseMessage.type() == Message.Type.UNKNOWN_ID) {
String msg = "Message was not delivered successfully, unknown ID (peer may be offline or unknown RPC handler): "
+ this.message;
exceptionCaught(ctx, new PeerException(PeerException.AbortCause.PEER_ABORT, msg));
responseMessage.release();
return;
}
if (responseMessage.type() == Message.Type.EXCEPTION) {
String msg = "Message caused an exception on the other side, handle as peer_abort: "
+ this.message;
exceptionCaught(ctx, new PeerException(PeerException.AbortCause.PEER_ABORT, msg));
responseMessage.release();
return;
}
if (responseMessage.isRequest()) {
ctx.fireChannelRead(responseMessage);
return;
}
if(responseMessage.command() == RPC.Commands.PING.getNr()
&& responseMessage.type() == Message.Type.PARTIALLY_OK) {
//ignore interal ping
return;
}
if (!sendMessageID.equals(recvMessageID)) {
String msg = "Response message [" + responseMessage
+ "] sent to the node is not the same as we expect. We sent [" + this.message + "]";
exceptionCaught(ctx, new PeerException(PeerException.AbortCause.PEER_ABORT, msg));
responseMessage.release();
return;
}
// We need to exclude RCON Messages from the sanity check because we
// use this RequestHandler for sending a Type.REQUEST_1,
// RPC.Commands.RCON message on top of it. Therefore the response
// type will never be the same Type as the one the user initially
// used (e.g. DIRECT_DATA).
/*if (responseMessage.command() != RPC.Commands.RCON.getNr()
&& message.recipient().isRelayed() != responseMessage.sender().isRelayed()) {
String msg = "Response message [" + responseMessage + "] sent has a different relay flag than we sent with request message ["
+ this.message + "]. Recipient (" + message.recipient().isRelayed() + ") / Sender ("
+ responseMessage.sender().isRelayed() + ")";
LOG.warn(msg);
}*/
//NAT reflection, change it back, as this will be stored in our peer map that may be queried from other peers
if(message.recipientReflected() != null) {
responseMessage.sender(message.recipient().withIpv4Socket(message.recipient().ipv4Socket()));
}
// Stop time measurement of RTT
futureResponse.stopRTTMeasurement();
// We got a good answer, let's mark the sender as alive
//if its an announce, the peer status will be handled in the RPC
if (responseMessage.isOk() || responseMessage.isNotOk()) {
LOG.debug("Try adding peer {} to map, {}", responseMessage.sender(), responseMessage);
peerBean.notifyPeerFound(responseMessage.sender(), null, null, futureResponse.getRoundTripTime());
}
// call this for streaming support
if (!responseMessage.isDone()) {
LOG.debug("Good message is streaming. {}", responseMessage);
return;
}
if (!message.isKeepAlive()) {
LOG.debug("Good message {}. Close channel {}.", responseMessage, ctx.channel());
//set the success now, but trigger the notify when we closed the channel.
//futureResponse.responseLater(responseMessage);
// the channel creator adds a listener that sets futureResponse.setResponseNow, when the channel
// is closed
//final EventLoop loop = ctx.channel().eventLoop();
//loop.
/*ctx.close().addListener(new GenericFutureListener<Future<? super Void>>() {
@Override
public void operationComplete(
Future<? super Void> future) throws Exception {
futureResponse.responseNow();
}
});*/
ctx.close();
peerConnection.closeListener().successAfterSemaphoreRelease(futureResponse, responseMessage);
//futureResponse.response(responseMessage);
} else {
LOG.debug("Good message {}. Leave channel {} open.", responseMessage, ctx.channel());
futureResponse.response(responseMessage);
}
}
}