/* * 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.ConnectException; import java.nio.channels.ClosedChannelException; import java.util.List; import java.util.concurrent.CancellationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelPipeline; import io.netty.util.concurrent.GenericFutureListener; import net.tomp2p.futures.Cancel; import net.tomp2p.futures.FutureDone; 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.peers.PeerStatusListener; import net.tomp2p.rpc.DispatchHandler; import net.tomp2p.storage.Data; import net.tomp2p.utils.ConcurrentCacheMap; import net.tomp2p.utils.Pair; /** * 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 Dispatcher dispatcher; private final DataFilter dataFilterTTL = new DataFilterTTL(); // this map caches all messages which are meant to be sent by a reverse // connection setup private final ConcurrentCacheMap<Integer, Pair<FutureResponse, FutureResponse>> cachedRequests = new ConcurrentCacheMap<Integer, Pair<FutureResponse, FutureResponse>>(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 Sender(final List<PeerStatusListener> peerStatusListeners, Dispatcher dispatcher) { this.peerStatusListeners = peerStatusListeners; this.dispatcher = dispatcher; } /** * 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) { if (data.isSigned() && data.signature() == null) { data.protectEntry(message.privateKey()); } // set new valid from as this data item might have an old one data.validFromMillis(System.currentTimeMillis()); return data.duplicate(); } }); final DispatchHandler handler = dispatcher.associatedHandler(copy); if(handler == null) { LOG.error("No handler found for self message {}", message); return; } 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(); } }); } /** * 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 FutureResponse sendMessage(final FutureResponse futureResponse, final Message message, final PeerConnection peerConnection, final boolean fireAndForget) { if (peerConnection.channelFuture() == null) { return futureResponse.failed("could not create a " + (message.isUdp() ? "UDP" : "TCP") + " channel"); } LOG.debug("about to connect to {} with channel {}, ff={}, msg={}", message.recipient(), peerConnection.channelFuture().channel(), fireAndForget, message); final Cancel connectCancel = createCancel(peerConnection.channelFuture()); futureResponse.setCancel(connectCancel); peerConnection.channelFuture().addListener(new GenericFutureListener<ChannelFuture>() { @Override public void operationComplete(final ChannelFuture future) throws Exception { if (future.isSuccess()) { final ChannelFuture writeFuture = future.channel().writeAndFlush(message); afterSend(writeFuture, futureResponse, fireAndForget, peerConnection); } else { LOG.warn("Channel creation failed", future.cause()); LOG.warn("Faild message {}", message); 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); } } } }); return futureResponse; } public ChannelFuture sendTCPPeerConnection(PeerConnection peerConnection, ChannelHandler replHandler) { // 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 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", replHandler); // 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; } } /** * 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 PeerConnection peerConnection) { final Cancel writeCancel = createCancel(writeFuture); futureResponse.setCancel(writeCancel); writeFuture.addListener(new GenericFutureListener<ChannelFuture>() { @Override public void operationComplete(final ChannelFuture future) throws Exception { if (!future.isSuccess()) { LOG.warn("Failed to write to channel - request {} {}.", futureResponse.request(), future.cause()); future.channel().close(); peerConnection.closeListener().failAfterSemaphoreRelease(futureResponse, future.cause()); } if (fireAndForget) { LOG.debug("fire and forget, close channel {} now. {}", futureResponse.request(), future.channel()); future.channel().close(); futureResponse.response(null); } } }); } /** * @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); } }; } /** * 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 ConcurrentCacheMap<Integer, Pair<FutureResponse, FutureResponse>> cachedRequests() { return cachedRequests; } public List<PeerStatusListener> peerStatusListeners() { return peerStatusListeners; } }