Java tutorial
/* * 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 io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPipeline; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.util.concurrent.EventExecutorGroup; import io.netty.util.concurrent.GenericFutureListener; import java.net.ConnectException; import java.net.InetSocketAddress; import java.nio.channels.ClosedChannelException; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Random; import java.util.concurrent.CancellationException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReferenceArray; import net.tomp2p.futures.BaseFutureAdapter; import net.tomp2p.futures.Cancel; import net.tomp2p.futures.FutureDone; import net.tomp2p.futures.FutureForkJoin; import net.tomp2p.futures.FuturePing; 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.p2p.builder.PingBuilder; import net.tomp2p.peers.LocalMap; import net.tomp2p.peers.Number160; import net.tomp2p.peers.PeerAddress; import net.tomp2p.peers.PeerSocketAddress; import net.tomp2p.peers.PeerStatistic; import net.tomp2p.peers.PeerStatusListener; import net.tomp2p.rpc.DispatchHandler; import net.tomp2p.rpc.RPC; import net.tomp2p.rpc.RPC.Commands; import net.tomp2p.storage.Data; import net.tomp2p.utils.Pair; import net.tomp2p.utils.Utils; 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 List<PeerStatusListener> peerStatusListeners; private final ChannelClientConfiguration channelClientConfiguration; private final Dispatcher dispatcher; private final SendBehavior sendBehavior; private final Random random; private final PeerBean peerBean; private final DataFilter dataFilterTTL = new DataFilterTTL(); // this map caches all messages which are meant to be sent by a reverse private final ConcurrentHashMap<Integer, Pair<FutureResponse, FutureResponse>> cachedRequests = new ConcurrentHashMap<Integer, Pair<FutureResponse, FutureResponse>>(); private PingBuilderFactory pingBuilderFactory; /** * 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 concurrentHashMap */ public Sender(final Number160 peerId, final List<PeerStatusListener> peerStatusListeners, final ChannelClientConfiguration channelClientConfiguration, Dispatcher dispatcher, SendBehavior sendBehavior, PeerBean peerBean) { this.peerStatusListeners = peerStatusListeners; this.channelClientConfiguration = channelClientConfiguration; this.dispatcher = dispatcher; this.sendBehavior = sendBehavior; this.random = new Random(peerId.hashCode()); this.peerBean = peerBean; } public ChannelClientConfiguration channelClientConfiguration() { return channelClientConfiguration; } public PingBuilderFactory pingBuilderFactory() { return pingBuilderFactory; } public Sender pingBuilderFactory(PingBuilderFactory pingBuilderFactory) { this.pingBuilderFactory = pingBuilderFactory; return this; } /** * 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<Message> handler, final FutureResponse futureResponse, final Message 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; } // NAT reflection - rewrite recipient if we found a local address for // the recipient LocalMap localMap = peerBean.localMap(); if (localMap != null) { PeerStatistic peerStatistic = localMap.translate(message.recipient()); if (peerStatistic != null) { message.recipient(peerStatistic.peerAddress()); } } removePeerIfFailed(futureResponse, message); // RTT calculation futureResponse.startRTTMeasurement(false); final ChannelFuture channelFuture; if (peerConnection != null && peerConnection.channelFuture() != null && peerConnection.channelFuture().channel().isActive()) { channelFuture = sendTCPPeerConnection(peerConnection, handler, channelCreator, futureResponse); afterConnect(futureResponse, message, channelFuture, handler == null); } else if (channelCreator != null) { final TimeoutFactory timeoutHandler = createTimeoutHandler(futureResponse, idleTCPSeconds, handler == null); switch (sendBehavior.tcpSendBehavior(message)) { case DIRECT: connectAndSend(handler, futureResponse, channelCreator, connectTimeoutMillis, peerConnection, timeoutHandler, message); break; case RCON: handleRcon(handler, futureResponse, message, channelCreator, connectTimeoutMillis, peerConnection, timeoutHandler); break; case RELAY: handleRelay(handler, futureResponse, message, channelCreator, idleTCPSeconds, connectTimeoutMillis, peerConnection, timeoutHandler); break; case SELF: sendSelf(futureResponse, message); break; default: throw new IllegalArgumentException("Illegal sending behavior"); } } } /** * 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 void handleRcon(final SimpleChannelInboundHandler<Message> handler, final FutureResponse futureResponse, final Message message, final ChannelCreator channelCreator, final int connectTimeoutMillis, final PeerConnection peerConnection, final TimeoutFactory timeoutHandler) { message.keepAlive(true); LOG.debug("initiate reverse connection setup to peer with peerAddress {}", message.recipient()); Message rconMessage = createRconMessage(message); final FutureResponse rconResponse = new FutureResponse(rconMessage); // cache the original message until the connection is established cachedRequests.put(message.messageId(), new Pair<FutureResponse, FutureResponse>(futureResponse, rconResponse)); // wait for response (whether the reverse connection setup was // successful) 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()); rconResponse.response(msg); } else { LOG.debug("Could not acquire a reverse connection, msg: {}", message); rconResponse.failed("Could not acquire a reverse connection, msg: " + message); futureResponse.failed(rconResponse); cachedRequests.remove(message.messageId()); } } }; // send reverse connection request instead of normal message sendTCP(rconInboundHandler, rconResponse, rconMessage, channelCreator, connectTimeoutMillis, connectTimeoutMillis, peerConnection); } /** * This method makes a copy of the original Message and prepares it for * sending it to the relay. * * @param message * @return rconMessage */ private static Message createRconMessage(final Message message) { // get Relay InetAddress from unreachable peer PeerSocketAddress socketAddress = Utils.extractRandomRelay(message); // we need to make a copy of the original message Message rconMessage = new Message(); rconMessage.sender(message.sender()); rconMessage.version(message.version()); // store the message id in the payload to get the cached message later rconMessage.intValue(message.messageId()); // the message must have set the keepAlive Flag true. If not, the relay // peer will close the PeerConnection to the unreachable peer. rconMessage.keepAlive(true); // making the message ready to send readyToSend(message, socketAddress, rconMessage, RPC.Commands.RCON.getNr(), Message.Type.REQUEST_1); return rconMessage; } /** * 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) { PeerAddress recipient = originalMessage.recipient().changeAddress(socketAddress.inetAddress()) .changePorts(socketAddress.tcpPort(), socketAddress.udpPort()).changeRelayed(false); newMessage.recipient(recipient); newMessage.command(RPCCommand); newMessage.type(messageType); } /** * This method is extracted by @author jonaswagner to ensure that no * duplicate code exist. * * @param handler * @param futureResponse * @param channelCreator * @param connectTimeoutMillis * @param peerConnection * @param timeoutHandler * @param message */ private void connectAndSend(final SimpleChannelInboundHandler<Message> handler, final FutureResponse futureResponse, final ChannelCreator channelCreator, final int connectTimeoutMillis, final PeerConnection peerConnection, final TimeoutFactory timeoutHandler, final Message message) { InetSocketAddress recipient = message.recipient().createSocketTCP(); final ChannelFuture channelFuture = sendTCPCreateChannel(recipient, channelCreator, peerConnection, handler, timeoutHandler, connectTimeoutMillis, futureResponse); afterConnect(futureResponse, message, channelFuture, handler == null); } /** * 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 void handleRelay(final SimpleChannelInboundHandler<Message> handler, final FutureResponse futureResponse, final Message message, final ChannelCreator channelCreator, final int idleTCPSeconds, final int connectTimeoutMillis, final PeerConnection peerConnection, final TimeoutFactory timeoutHandler) { FutureDone<PeerSocketAddress> futurePing = pingFirst(message.recipient().peerSocketAddresses()); futurePing.addListener(new BaseFutureAdapter<FutureDone<PeerSocketAddress>>() { @Override public void operationComplete(final FutureDone<PeerSocketAddress> futureDone) throws Exception { if (futureDone.isSuccess()) { InetSocketAddress recipient = PeerSocketAddress.createSocketTCP(futureDone.object()); ChannelFuture channelFuture = sendTCPCreateChannel(recipient, channelCreator, peerConnection, handler, timeoutHandler, connectTimeoutMillis, futureResponse); afterConnect(futureResponse, message, channelFuture, handler == null); futureResponse.addListener(new BaseFutureAdapter<FutureResponse>() { @Override public void operationComplete(FutureResponse future) throws Exception { if (future.isFailed()) { if (future.responseMessage() != null && future.responseMessage().type() != Message.Type.DENIED) { // remove the failed relay and try again clearInactivePeerSocketAddress(futureDone); sendTCP(handler, futureResponse, message, channelCreator, idleTCPSeconds, connectTimeoutMillis, peerConnection); } } } private void clearInactivePeerSocketAddress( final FutureDone<PeerSocketAddress> futureDone) { Collection<PeerSocketAddress> tmp = new ArrayList<PeerSocketAddress>(); for (PeerSocketAddress psa : message.recipient().peerSocketAddresses()) { if (psa != null) { if (!psa.equals(futureDone.object())) { tmp.add(psa); } } } message.peerSocketAddresses(tmp); } }); } else { futureResponse.failed("no relay could be contacted", futureDone); } } }); } /** * Ping all relays of the receiver. The first one answering is picked as the * responsible relay for this message. * * @param peerSocketAddresses * a collection of relay addresses * @return */ private FutureDone<PeerSocketAddress> pingFirst(Collection<PeerSocketAddress> peerSocketAddresses) { final FutureDone<PeerSocketAddress> futureDone = new FutureDone<PeerSocketAddress>(); FuturePing[] forks = new FuturePing[peerSocketAddresses.size()]; int index = 0; for (PeerSocketAddress psa : peerSocketAddresses) { if (psa != null) { InetSocketAddress inetSocketAddress = PeerSocketAddress.createSocketUDP(psa); PingBuilder pingBuilder = pingBuilderFactory.create(); forks[index++] = pingBuilder.inetAddress(inetSocketAddress.getAddress()) .port(inetSocketAddress.getPort()).start(); } } FutureForkJoin<FuturePing> ffk = new FutureForkJoin<FuturePing>(1, true, new AtomicReferenceArray<FuturePing>(forks)); ffk.addListener(new BaseFutureAdapter<FutureForkJoin<FuturePing>>() { @Override public void operationComplete(FutureForkJoin<FuturePing> future) throws Exception { if (future.isSuccess()) { futureDone.done(future.first().remotePeer().peerSocketAddress()); } else { futureDone.failed(future); } } }); return futureDone; } /** * In case a message is sent to the sender itself, this is the cutoff. * * @param futureResponse * the future to respond as soon as the proper handler returns it * @param message * the request */ public void sendSelf(final FutureResponse futureResponse, final Message message) { LOG.debug("Handle message that is intended for the sender itself {}", message); message.sendSelf(); Message copy = message.duplicate(new DataFilter() { @Override public Data filter(Data data, boolean isConvertMeta, boolean isReply) { Data copyData = data.duplicate(); if (copyData.isSigned() && copyData.signature() == null) { copyData.protectEntry(message.privateKey()); } // set new valid from as this data item might have an old one copyData.validFromMillis(System.currentTimeMillis()); return copyData; } }); final DispatchHandler handler = dispatcher.associatedHandler(copy); handler.forwardMessage(copy, null, new Responder() { @Override public FutureDone<Void> response(final Message responseMessage) { Message copy = responseMessage.duplicate(dataFilterTTL); futureResponse.response(copy); return new FutureDone<Void>().done(); } @Override public void failed(Type type, String reason) { futureResponse.failed("Failed with type " + type.name() + ". Reason: " + reason); } @Override public void responseFireAndForget() { futureResponse.emptyResponse(); } }); } private ChannelFuture sendTCPCreateChannel(InetSocketAddress recipient, ChannelCreator channelCreator, PeerConnection peerConnection, ChannelHandler handler, TimeoutFactory timeoutHandler, int connectTimeoutMillis, FutureResponse futureResponse) { final Map<String, Pair<EventExecutorGroup, ChannelHandler>> handlers; if (timeoutHandler != null) { handlers = new LinkedHashMap<String, Pair<EventExecutorGroup, ChannelHandler>>(); handlers.put("timeout0", new Pair<EventExecutorGroup, ChannelHandler>(null, timeoutHandler.idleStateHandlerTomP2P())); handlers.put("timeout1", new Pair<EventExecutorGroup, ChannelHandler>(null, timeoutHandler.timeHandler())); } else { handlers = new LinkedHashMap<String, Pair<EventExecutorGroup, ChannelHandler>>(); } handlers.put("decoder", new Pair<EventExecutorGroup, ChannelHandler>(null, new TomP2PCumulationTCP( channelClientConfiguration.signatureFactory(), channelClientConfiguration.byteBufAllocator()))); handlers.put("encoder", new Pair<EventExecutorGroup, ChannelHandler>(null, new TomP2POutbound( channelClientConfiguration.signatureFactory(), channelClientConfiguration.byteBufAllocator()))); if (peerConnection != null) { // we expect replies on this connection handlers.put("dispatcher", new Pair<EventExecutorGroup, ChannelHandler>(null, dispatcher)); } if (timeoutHandler != null) { handlers.put("handler", new Pair<EventExecutorGroup, ChannelHandler>(null, handler)); } HeartBeat heartBeat = null; if (peerConnection != null) { heartBeat = new HeartBeat(peerConnection.heartBeatMillis(), TimeUnit.MILLISECONDS, pingBuilderFactory); handlers.put("heartbeat", new Pair<EventExecutorGroup, ChannelHandler>(null, heartBeat)); } InetSocketAddress reflectedRecipient = Utils.natReflection(recipient, false, dispatcher.peerBean().serverPeerAddress()); ChannelFuture channelFuture = channelCreator.createTCP(reflectedRecipient, connectTimeoutMillis, handlers, futureResponse); if (peerConnection != null && channelFuture != null) { peerConnection.channelFuture(channelFuture); heartBeat.peerConnection(peerConnection); } return channelFuture; } private ChannelFuture sendTCPPeerConnection(PeerConnection peerConnection, ChannelHandler handler, final ChannelCreator channelCreator, final FutureResponse futureResponse) { // if the channel gets closed, the future should get notified ChannelFuture channelFuture = peerConnection.channelFuture(); // channelCreator can be null if we don't need to create any channels if (channelCreator != null) { channelCreator.setupCloseListener(channelFuture, futureResponse); } ChannelPipeline pipeline = channelFuture.channel().pipeline(); // we need to replace the handler if this comes from the peer that // create a peerConnection, otherwise we // need to add a handler addOrReplace(pipeline, "dispatcher", "handler", handler); // uncomment this if the recipient should also heartbeat // addIfAbsent(pipeline, "handler", "heartbeat", // new HeartBeat(2, pingBuilder).peerConnection(peerConnection)); return channelFuture; } // private boolean addIfAbsent(ChannelPipeline pipeline, String before, // String name, // ChannelHandler channelHandler) { // List<String> names = pipeline.names(); // if (names.contains(name)) { // return false; // } else { // if (before == null) { // pipeline.addFirst(name, channelHandler); // } else { // pipeline.addBefore(before, name, channelHandler); // } // return true; // } // } private boolean addOrReplace(ChannelPipeline pipeline, String before, String name, ChannelHandler channelHandler) { List<String> names = pipeline.names(); if (names.contains(name)) { pipeline.replace(name, name, channelHandler); return false; } else { if (before == null) { pipeline.addFirst(name, channelHandler); } else { pipeline.addBefore(before, name, channelHandler); } return true; } } /** * 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 */ // TODO: if message.getRecipient() is me, than call dispatcher directly // without sending over Internet. public void sendUDP(final SimpleChannelInboundHandler<Message> handler, final FutureResponse futureResponse, final Message message, final ChannelCreator channelCreator, final int idleUDPSeconds, final boolean broadcast) { // no need to continue if we already finished if (futureResponse.isCompleted()) { return; } // NAT reflection - rewrite recipient if we found a local address for // the recipient LocalMap localMap = peerBean.localMap(); if (localMap != null) { PeerStatistic peerStatistic = localMap.translate(message.recipient()); if (peerStatistic != null) { message.recipient(peerStatistic.peerAddress()); } } removePeerIfFailed(futureResponse, message); if (message.sender().isRelayed()) { message.peerSocketAddresses(message.sender().peerSocketAddresses()); } boolean isFireAndForget = handler == null; final Map<String, Pair<EventExecutorGroup, ChannelHandler>> handlers = configureHandlers(handler, futureResponse, idleUDPSeconds, isFireAndForget); // RTT calculation futureResponse.startRTTMeasurement(true); try { ChannelFuture channelFuture = null; switch (sendBehavior.udpSendBehavior(message)) { case DIRECT: channelFuture = channelCreator.createUDP(broadcast, handlers, futureResponse); break; case HOLEP: if (peerBean.holePunchInitiator() != null) { handleHolePunch(futureResponse, message, channelCreator, idleUDPSeconds, handler, broadcast, handlers, channelFuture); // all the send mechanics are done in a // AbstractHolePuncherStrategy class. // Therefore we must execute this return statement. return; } else { LOG.debug("No hole punching possible, because There is no PeerNAT. New Attempt with Relaying"); } case RELAY: try { channelFuture = prepareRelaySendUDP(futureResponse, message, channelCreator, broadcast, handlers, channelFuture); } catch (Exception e) { e.printStackTrace(); return; } break; case SELF: sendSelf(futureResponse, message); channelFuture = null; break; default: throw new IllegalArgumentException("UDP messages are not allowed to send over RCON"); } afterConnect(futureResponse, message, channelFuture, handler == null); } catch (UnsupportedOperationException e) { LOG.warn(e.getMessage()); futureResponse.failed(e); } } /** * 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 ChannelFuture prepareRelaySendUDP(final FutureResponse futureResponse, final Message message, final ChannelCreator channelCreator, final boolean broadcast, final Map<String, Pair<EventExecutorGroup, ChannelHandler>> handlers, ChannelFuture channelFuture) throws Exception { List<PeerSocketAddress> psa = new ArrayList<PeerSocketAddress>(message.recipient().peerSocketAddresses()); LOG.debug("send neighbor request to random relay peer {}", psa); if (psa.size() > 0) { PeerSocketAddress ps = psa.get(random.nextInt(psa.size())); message.recipientRelay(message.recipient().changePeerSocketAddress(ps).changeRelayed(true)); channelFuture = channelCreator.createUDP(broadcast, handlers, futureResponse); } else { String failMessage = "Peer is relayed, but no relay given"; futureResponse.failed(failMessage); throw new Exception(failMessage); } return channelFuture; } private FutureDone<Message> handleHolePunch(final FutureResponse futureResponse, final Message message, final ChannelCreator channelCreator, final int idleUDPSeconds, final SimpleChannelInboundHandler<Message> handler, final boolean broadcast, final Map<String, Pair<EventExecutorGroup, ChannelHandler>> handlers, final ChannelFuture channelFuture) { FutureDone<Message> fDone = peerBean.holePunchInitiator().handleHolePunch(idleUDPSeconds, 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()); doRelayFallback(futureResponse, message, broadcast, handlers, channelFuture); } } @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(); doRelayFallback(futureResponse, message, broadcast, handlers, channelFuture); } private void doRelayFallback(final FutureResponse futureResponse, final Message message, final boolean broadcast, final Map<String, Pair<EventExecutorGroup, ChannelHandler>> handlers, ChannelFuture channelFuture) { try { channelFuture = prepareRelaySendUDP(futureResponse, message, channelCreator, broadcast, handlers, channelFuture); afterConnect(futureResponse, message, channelFuture, handler == null); } catch (Exception e) { e.printStackTrace(); return; } } }); return fDone; } /** * 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 */ public Map<String, Pair<EventExecutorGroup, ChannelHandler>> configureHandlers( final SimpleChannelInboundHandler<Message> handler, final FutureResponse futureResponse, final int idleUDPSeconds, boolean isFireAndForget) { final Map<String, Pair<EventExecutorGroup, ChannelHandler>> handlers; if (isFireAndForget) { final int nrTCPHandlers = 3; // 2 / 0.75 handlers = new LinkedHashMap<String, Pair<EventExecutorGroup, ChannelHandler>>(nrTCPHandlers); } else { final int nrTCPHandlers = 7; // 5 / 0.75 handlers = new LinkedHashMap<String, Pair<EventExecutorGroup, ChannelHandler>>(nrTCPHandlers); final TimeoutFactory timeoutHandler = createTimeoutHandler(futureResponse, idleUDPSeconds, isFireAndForget); handlers.put("timeout0", new Pair<EventExecutorGroup, ChannelHandler>(null, timeoutHandler.idleStateHandlerTomP2P())); handlers.put("timeout1", new Pair<EventExecutorGroup, ChannelHandler>(null, timeoutHandler.timeHandler())); } handlers.put("decoder", new Pair<EventExecutorGroup, ChannelHandler>(null, new TomP2PSinglePacketUDP( channelClientConfiguration.signatureFactory(), channelClientConfiguration.byteBufAllocator()))); handlers.put("encoder", new Pair<EventExecutorGroup, ChannelHandler>(null, new TomP2POutbound( channelClientConfiguration.signatureFactory(), channelClientConfiguration.byteBufAllocator()))); if (!isFireAndForget) { handlers.put("handler", new Pair<EventExecutorGroup, ChannelHandler>(null, handler)); } return handlers; } /** * 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, "Sender"); } /** * 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 */ public void afterConnect(final FutureResponse futureResponse, final Message message, final ChannelFuture channelFuture, final boolean fireAndForget) { if (channelFuture == null) { futureResponse.failed("could not create a " + (message.isUdp() ? "UDP" : "TCP") + " channel"); return; } LOG.debug("about to connect to {} with channel {}, ff={}", message.recipient(), channelFuture.channel(), 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()) { final ChannelFuture writeFuture = future.channel().writeAndFlush(message); afterSend(writeFuture, futureResponse, fireAndForget); } else { LOG.debug("Channel creation failed", future.cause()); futureResponse.failed("Channel creation failed " + future.channel() + "/" + future.cause()); // may have been closed by the other side, // or it may have been canceled from this side if (!(future.cause() instanceof CancellationException) && !(future.cause() instanceof ClosedChannelException) && !(future.cause() instanceof ConnectException)) { LOG.warn("Channel creation failed to {} for {}", future.channel(), message); } } } }); } /** * 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()) { futureResponse.failedLater(future.cause()); reportFailed(futureResponse, future.channel().close()); LOG.warn("Failed to write channel the request {} {}", futureResponse.request(), future.cause()); } if (fireAndForget) { futureResponse.responseLater(null); LOG.debug("fire and forget, close channel now {}, {}", futureResponse.request(), future.channel()); reportMessage(futureResponse, future.channel().close()); } } }); } /** * Report a failure after the channel was closed. * * @param futureResponse * The future to set the response * @param close * The close future */ private void reportFailed(final FutureResponse futureResponse, final ChannelFuture close) { close.addListener(new GenericFutureListener<ChannelFuture>() { @Override public void operationComplete(final ChannelFuture arg0) throws Exception { futureResponse.responseNow(); } }); } /** * Report a successful response after the channel was closed. * * @param futureResponse * The future to set the response * @param close * The close future */ private void reportMessage(final FutureResponse futureResponse, final ChannelFuture close) { close.addListener(new GenericFutureListener<ChannelFuture>() { @Override public void operationComplete(final ChannelFuture arg0) throws Exception { futureResponse.responseNow(); } }); } /** * @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); } }; } private void removePeerIfFailed(final FutureResponse futureResponse, final Message message) { futureResponse.addListener(new BaseFutureAdapter<FutureResponse>() { @Override public void operationComplete(FutureResponse future) throws Exception { if (future.isFailed()) { if (message.recipient().isRelayed()) { // TODO: make the relay go away if failed } else if (message.command() == RPC.Commands.HOLEP.getNr() && message.type().ordinal() == Message.Type.REQUEST_3.ordinal()) { //do nothing, because such a (dummy) message will never reach its target the first time } synchronized (peerStatusListeners) { for (PeerStatusListener peerStatusListener : peerStatusListeners) { peerStatusListener.peerFailed(message.recipient(), new PeerException(future)); } } } } }); } /** * Get currently cached requests. They are cached because for example the * receiver is behind a NAT. Instead of sending the message directly, a * reverse connection is set up beforehand. After a successful connection * establishment, the cached messages are sent through the direct channel. */ public ConcurrentHashMap<Integer, Pair<FutureResponse, FutureResponse>> cachedRequests() { return cachedRequests; } public List<PeerStatusListener> peerStatusListeners() { return peerStatusListeners; } }