/*
* 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.connection;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.util.concurrent.GenericFutureListener;
import lombok.Getter;
import lombok.experimental.Accessors;
import net.tomp2p.futures.BaseFutureAdapter;
import net.tomp2p.futures.Cancel;
import net.tomp2p.futures.FutureResponse;
import net.tomp2p.message.DataFilter;
import net.tomp2p.message.DataFilterTTL;
import net.tomp2p.message.Message;
import net.tomp2p.message.Message.Type;
import net.tomp2p.message.TomP2PCumulationTCP;
import net.tomp2p.message.TomP2POutbound;
import net.tomp2p.message.TomP2PSinglePacketUDP;
import net.tomp2p.peers.Number160;
import net.tomp2p.peers.PeerAddress;
import net.tomp2p.peers.PeerSocketAddress;
import net.tomp2p.peers.PeerSocketAddress.PeerSocket4Address;
import net.tomp2p.peers.PeerStatusListener;
import net.tomp2p.rpc.RPC;
import net.tomp2p.utils.ConcurrentCacheMap;
import net.tomp2p.utils.Pair;
import net.tomp2p.utils.Utils;
/**
* The class that sends out messages.
*
* @author Thomas Bocek
*
*/
@Accessors(chain = true, fluent = true)
public class Connect {
public static final PeerConnection SELF_PEERCONNECTION_MARKER = new PeerConnection();
private static final Logger LOG = LoggerFactory.getLogger(Connect.class);
private final ChannelClientConfiguration channelClientConfiguration;
private final Dispatcher dispatcher;
private final SendBehavior sendBehavior;
private final Random random;
@Getter private final CountConnectionOutboundHandler counterUDP = new CountConnectionOutboundHandler();
@Getter private final CountConnectionOutboundHandler counterTCP = new CountConnectionOutboundHandler();
// this map caches all messages which are meant to be sent by a reverse
// connection setup
private final ConcurrentCacheMap<Integer, Message> cachedRequests = new ConcurrentCacheMap<Integer, Message>(30, 1024);
/**
* 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
* @param dispatcher
* @param sendBehavior
* @param peerBean
*/
public Connect(final Number160 peerId, final ChannelClientConfiguration channelClientConfiguration,
Dispatcher dispatcher, SendBehavior sendBehavior) {
this.channelClientConfiguration = channelClientConfiguration;
this.dispatcher = dispatcher;
this.sendBehavior = sendBehavior;
this.random = new Random(peerId.hashCode());
}
public ChannelClientConfiguration channelClientConfiguration() {
return channelClientConfiguration;
}
//final FutureDone<Pair<PeerConnection,Message>> future = new FutureDone<Pair<PeerConnection,Message>>();
public SendBehavior.SendMethod connectTCP(final SimpleChannelInboundHandler<Message> replHandler,
final int connectTimeoutMillis, final PeerAddress sender,
final PeerConnection peerConnection, final boolean isReflected) {
if (peerConnection.isExisting() || peerConnection.isOpen()) {
LOG.debug("go for peer connection / TCP");
return SendBehavior.SendMethod.EXISTING_CONNECTION;
} else {
IdleStateHandler timeoutHandler = new IdleStateHandler(peerConnection.idleMillis() / 1000, 0, 0);
LOG.debug("Direct TCP connection to : {}, {}", peerConnection.remotePeer(), peerConnection.isKeepAlive());
switch (sendBehavior.tcpSendBehavior(dispatcher, sender, peerConnection.remotePeer(), isReflected)) {
case DIRECT:
Pair<ChannelCreator.ChannelCloseListener, ChannelFuture> pair1 = createChannelTCP(
peerConnection.remotePeer().createTCPSocket(sender),
peerConnection.channelCreator(), replHandler, timeoutHandler,
connectTimeoutMillis, peerConnection.isKeepAlive());
if(pair1 == null) {
return SendBehavior.SendMethod.CANNOT_CREATE_TCP;
}
peerConnection.channelFuture(pair1.element1(), pair1.element0());
return SendBehavior.SendMethod.DIRECT;
case RCON:
Pair<ChannelCreator.ChannelCloseListener, ChannelFuture> pair2 = createChannelUDP(peerConnection.remotePeer().createUDPSocket(
sender), peerConnection.channelCreator(), null, timeoutHandler, true);
peerConnection.channelFuture(pair2.element1(), pair2.element0());
return SendBehavior.SendMethod.RCON;
/*final PeerSocketAddress peerSocketAddress = prepareRelaySend(
message, peerBean.serverPeerAddress().ipInternalSocket());
if(peerSocketAddress == null) {
throw new IllegalArgumentException("Illegal sending behavior: no relay provided, but relay indicated RCON");
}
Message rconMessage = createRconMessage(peerSocketAddress, message);
if(peerSocketAddress.equals(peerBean.serverPeerAddress().ipv4Socket())) {
LOG.debug("Send to self-relay RCON");
return new Pair<PeerConnection,Message>(SELF_PEERCONNECTION_MARKER, rconMessage);
}
return handleRcon(peerSocketAddress, message, rconMessage, channelCreator, connectTimeoutMillis,
peerConnection);*/
/*case HOLEP_RELAY:
//handleHolePunch(futureResponse, message, channelCreator, idleTCPMillis, handler, true, handler, channelFuture);
return doRelayFallbackTCP(replHandler, futureResponse, message, channelCreator,
connectTimeoutMillis, peerConnection, timeoutHandler);*/
case SELF:
return SendBehavior.SendMethod.SELF;
default:
throw new IllegalArgumentException("Illegal sending behavior (1)");
}
}
}
/**
* This method initiates the reverse connection setup (or short: rconSetup).
* It creates a new Message and sends it via relay to the unreachable peer
* which then connects to this peer again. After the connectMessage from the
* unreachable peer this peer will send the original Message and its content
* directly.
*
* @param handler
* @param futureResponse
* @param message
* @param channelCreator
* @param connectTimeoutMillis
* @param peerConnection
* @param timeoutHandler
*/
/*private Pair<PeerConnection,Message> handleRcon(PeerSocketAddress ps, final Message message, final Message rconMessage,
final ChannelCreator channelCreator, final int connectTimeoutMillis,
final PeerConnection peerConnection) {
LOG.debug("initiate reverse connection setup to peer with peerAddress {}", message.recipient());
message.keepAlive(true);
// cache the original message until the connection is established
cachedRequests.put(message.messageId(), rconMessage);
SimpleChannelInboundHandler<Message> rconInboundHandler = new SimpleChannelInboundHandler<Message>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, Message msg) throws Exception {
if (msg.command() == Commands.RCON.getNr() && msg.type() == Type.OK) {
LOG.debug("Successfully set up the reverse connection to peer {}", message.recipient().peerId());
} else {
LOG.debug("Could not acquire a reverse connection, msg: {}", message);
cachedRequests.remove(message.messageId());
}
}
};
// send reverse connection request instead of normal message
return connectTCP(rconInboundHandler, rconMessage, channelCreator, connectTimeoutMillis,
connectTimeoutMillis, peerConnection);
}*/
/**
* This method was extracted from createRconMessage(...), in order to avoid
* duplicate code in createHolePMessage(...).
*
* @param originalMessage
* @param socketAddress
* @param newMessage
* @param RPCCommand
* @param messageType
*/
private static void readyToSend(final Message originalMessage, PeerSocketAddress socketAddress, Message newMessage, byte RPCCommand,
Type messageType) {
PeerSocket4Address psa = originalMessage.recipient().ipv4Socket();
if(socketAddress instanceof PeerSocket4Address) {
PeerSocket4Address socketAddress4 = (PeerSocket4Address) socketAddress;
psa = psa.withIpv4(socketAddress4.ipv4())
.withTcpPort(socketAddress4.tcpPort())
.withUdpPort(socketAddress4.udpPort());
}
PeerAddress recipient = originalMessage.recipient().withIpv4Socket(psa)
.withRelaySize(0);
newMessage.recipient(recipient);
newMessage.command(RPCCommand);
newMessage.type(messageType);
}
/**
* Both peers are relayed, thus sending directly or over reverse connection
* is not possible. Send the message to one of the receiver's relays.
*
* @param handler
* @param futureResponse
* @param message
* @param channelCreator
* @param idleTCPSeconds
* @param connectTimeoutMillis
* @param peerConnection
* @param timeoutHandler
*/
private Pair<ChannelCreator.ChannelCloseListener, ChannelFuture> createChannelTCP(InetSocketAddress recipient, ChannelCreator channelCreator,
ChannelHandler handler, IdleStateHandler timeoutHandler, int connectTimeoutMillis, boolean isKeepAlive) {
final Map<String, ChannelHandler> handlers = new LinkedHashMap<String, ChannelHandler>();
if (timeoutHandler != null) {
handlers.put("timeout", timeoutHandler);
}
handlers.put("decoder",
new TomP2PCumulationTCP(channelClientConfiguration.signatureFactory(),
channelClientConfiguration.byteBufAllocator()));
handlers.put(
"encoder",
new TomP2POutbound(channelClientConfiguration.signatureFactory(),
channelClientConfiguration.byteBufAllocator()));
if (isKeepAlive) {
// we expect replies on this connection
handlers.put("dispatcher", dispatcher);
}
handlers.put("initiater-counter", counterTCP);
if (timeoutHandler != null) {
handlers.put("handler", handler);
}
return channelCreator.createTCP(recipient, connectTimeoutMillis, handlers);
}
/**
* Sends a message via UDP.
*
* @param handler
* The handler to deal with a response message
* @param futureResponse
* The future to set the response
* @param message
* The message to send
* @param channelCreator
* The channel creator for the UDP channel
* @param idleUDPSeconds
* The idle time of a message until fail
* @param broadcast
* True, if the message is to be sent via layer 2 broadcast
*/
// TODO: if message.getRecipient() is me, than call dispatcher directly
// without sending over Internet.
public SendBehavior.SendMethod connectUDP(final SimpleChannelInboundHandler<Message> handler,
final ChannelCreator channelCreator, final PeerAddress sender,
final PeerConnection peerConnection, final boolean isReflected, final boolean broadcast) {
final boolean isFireAndForget = handler == null;
// RTT calculation
//futureResponse.startRTTMeasurement(true);
LOG.debug("Direct UDP connection to : {}, {}", peerConnection.remotePeer(), isFireAndForget);
switch (sendBehavior.udpSendBehavior(dispatcher, sender, peerConnection.remotePeer(), isReflected)) {
case DIRECT:
final IdleStateHandler timeoutHandler = new IdleStateHandler(peerConnection.idleMillis() /1000, 0, 0);
Pair<ChannelCreator.ChannelCloseListener, ChannelFuture> pair = createChannelUDP(peerConnection.remotePeer().createUDPSocket(
sender), channelCreator, handler, timeoutHandler, isFireAndForget);
peerConnection.channelFuture(pair.element1(), pair.element0());
return SendBehavior.SendMethod.DIRECT;
/*case HOLEP_RELAY:
if (peerBean.holePunchInitiator() != null) {
handleHolePunch(futureResponse, message, channelCreator, idleUDPMillis, handler, broadcast, handlers);
// all the send mechanics are done in a
// AbstractHolePuncherStrategy class.
// Therefore we must execute this return statement.
return;
}
LOG.debug("No hole punching possible, because There is no PeerNAT. New Attempt with Relaying");
doRelayFallbackUDP(futureResponse, message, broadcast, handlers, channelCreator, handler);
return;*/
case SELF:
LOG.debug("Send to self");
return SendBehavior.SendMethod.SELF;
default:
throw new IllegalArgumentException("UDP messages are not allowed to send over RCON");
}
}
private void handleReflection(final Message message) {
PeerSocket4Address reflectedRecipient = Utils.natReflection(message.recipient(), dispatcher.peerBean().serverPeerAddress());
if(reflectedRecipient != null) {
message.recipientReflected(message.recipient().withIpv4Socket(reflectedRecipient));
LOG.debug("reflect recipient UDP {}", message);
}
}
/**
* This method needed to be extracted from sendUDP(...), because it is also
* needed by the method handleHolePunch(...).
*
* @param futureResponse
* @param message
* @param channelCreator
* @param broadcast
* @param handlers
* @param channelFuture
* @return
* @throws Exception
*/
private PeerSocketAddress prepareRelaySend(final Message message, final PeerSocket4Address preferredAddress) {
List<PeerSocketAddress> psa = new ArrayList<PeerSocketAddress>(message.recipient().relays());
if (psa.size() > 0) {
if(psa.contains(preferredAddress)) {
LOG.debug("send neighbor request to preferred relay peer {} out of {}", preferredAddress, psa);
return preferredAddress;
}
while(!psa.isEmpty()) {
PeerSocketAddress ps = psa.remove(random.nextInt(psa.size()));
//TODO: prefer local ones
message.recipientRelay(message.recipient().withIPSocket(ps));
LOG.debug("send neighbor request to random relay peer {} out of {}", ps, psa);
return ps;
}
LOG.error("no non-reflected relays found");
return null;
} else {
LOG.error("Peer is relayed, but no relay given");
return null;
}
}
/*private FutureDone<Message> handleHolePunch(final FutureResponse futureResponse, final Message message,
final ChannelCreator channelCreator, final int idleUDPMillis, final SimpleChannelInboundHandler<Message> handler,
final boolean broadcast, final Map<String, Pair<EventExecutorGroup, ChannelHandler>> handlers) {
// start hole punching
FutureDone<Message> fDone = peerBean.holePunchInitiator().handleHolePunch(idleUDPMillis, futureResponse, message);
fDone.addListener(new BaseFutureAdapter<FutureDone<Message>>() {
@Override
public void operationComplete(FutureDone<Message> future) throws Exception {
if (future.isSuccess()) {
futureResponse.response(future.object());
} else {
LOG.error(future.failedReason());
LOG.error("Message could not be sent with hole punching! New send attempt with relaying.");
// futureResponse.failed(future.failedReason());
// throw new Exception(future.failedReason());
doRelayFallbackUDP(futureResponse, message, broadcast, handlers, channelCreator, handler);
}
}
@Override
public void exceptionCaught(Throwable t) throws Exception {
// futureResponse.failed(t);
// throw new Exception(t);
LOG.error("The setup of a connection via has been canceled, because an error was thrown");
t.printStackTrace();
doRelayFallbackUDP(futureResponse, message, broadcast, handlers, channelCreator, handler);
}
});
return fDone;
}*/
/*private void doRelayFallbackUDP(final FutureResponse futureResponse, final Message message, final boolean broadcast,
final Map<String, Pair<EventExecutorGroup, ChannelHandler>> handlers,
final ChannelCreator channelCreator, final SimpleChannelInboundHandler<Message> handler) {
PeerSocketAddress ps = prepareRelaySend(message, peerBean.serverPeerAddress().ipv4Socket());
if(ps == null) {
futureResponse.failed("no relay provided, but relay indicated HP");
return;
}
if(ps.equals(peerBean.serverPeerAddress().ipv4Socket())) {
LOG.debug("Send to self-relay HP");
sendSelf(futureResponse, message);
return;
}
ChannelFuture channelFuture = channelCreator.createUDP(broadcast, handlers, futureResponse, false);
sendMessage(futureResponse, message, channelFuture, handler == null);
}
private void doRelayFallbackTCP(final SimpleChannelInboundHandler<Message> handler, final FutureResponse futureResponse,
final Message message, final ChannelCreator channelCreator, final int connectTimeoutMillis,
final PeerConnection peerConnection, final TimeoutFactory timeoutHandler) {
PeerSocketAddress ps = prepareRelaySend(message, peerBean.serverPeerAddress().ipv4Socket());
if(ps == null) {
futureResponse.failed("no relay provided, but relay indicated TCP");
return;
}
if(ps.equals(peerBean.serverPeerAddress().ipv4Socket())) {
LOG.debug("Send to self-relay TCP");
sendSelf(futureResponse, message);
return;
}
InetSocketAddress recipient = ps.createTCPSocket();
ChannelFuture channelFuture = sendTCPCreateChannel(recipient, channelCreator, peerConnection, handler, timeoutHandler, connectTimeoutMillis, futureResponse);
sendMessage(futureResponse, message, channelFuture, handler == null);
}*/
/**
* This method was extracted in order to avoid duplicate code in the
* {@link HolePInitiator} and in the initHolePunch(...) method.
*
* @param handler
* @param futureResponse
* @param idleUDPSeconds
* @param isFireAndForget
* @return handlers
*/
private Pair<ChannelCreator.ChannelCloseListener, ChannelFuture> createChannelUDP(InetSocketAddress recipient, ChannelCreator channelCreator,
ChannelHandler handler, IdleStateHandler timeoutHandler, boolean isFireAndForget) {
final Map<String, ChannelHandler> handlers =
new LinkedHashMap<String, ChannelHandler>();
if (!isFireAndForget) {
handlers.put("timeout", timeoutHandler);
}
handlers.put(
"decoder",
new TomP2PSinglePacketUDP(channelClientConfiguration.signatureFactory()));
handlers.put(
"encoder",
new TomP2POutbound(channelClientConfiguration.signatureFactory(), channelClientConfiguration.byteBufAllocator()));
handlers.put("initiater-counter", counterUDP);
if (!isFireAndForget) {
handlers.put("handler", handler);
}
return channelCreator.createUDP(recipient, handlers, isFireAndForget);
}
}