/*
* 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 io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.FixedRecvByteBufAllocator;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioDatagramChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.util.concurrent.DefaultThreadFactory;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import net.tomp2p.futures.FutureDone;
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.PeerStatusListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The "server" part that accepts connections.
*
* @author Thomas Bocek
*
*/
public final class ChannelServer {
private static final Logger LOG = LoggerFactory.getLogger(ChannelServer.class);
// private static final int BACKLOG = 128;
// important to keep them low, since a too high value results in connection degradation
private final EventLoopGroup bossGroup = new NioEventLoopGroup(
Runtime.getRuntime().availableProcessors() / 2, new DefaultThreadFactory(
ConnectionBean.THREAD_NAME + "boss - "));
private final EventLoopGroup workerGroup = new NioEventLoopGroup(Runtime.getRuntime()
.availableProcessors() / 2, new DefaultThreadFactory(ConnectionBean.THREAD_NAME
+ "worker-server - "));
private Channel channelUDP;
private Channel channelTCP;
private final FutureDone<Void> futureServerDone = new FutureDone<Void>();
// setup
private final Bindings bindings;
private final int tcpPort;
private final int udpPort;
private final ChannelServerConficuration channelServerConfiguration;
private final Dispatcher dispatcher;
private final PeerStatusListener[] peerStatusListeners;
/**
* Sets parameters and starts network device discovery.
*
* @param channelServerConfiguration
* The server configuration, that contains e.g. the handlers
* @param dispatcher
* The shared dispatcher
* @param peerStatusListeners
* The status listener for offline peers
* @throws IOException
* If device discovery failed.
*/
public ChannelServer(final ChannelServerConficuration channelServerConfiguration,
final Dispatcher dispatcher, final PeerStatusListener[] peerStatusListeners) throws IOException {
this.bindings = channelServerConfiguration.getBindings();
this.tcpPort = channelServerConfiguration.getTcpPort();
this.udpPort = channelServerConfiguration.getUdpPort();
this.channelServerConfiguration = channelServerConfiguration;
this.dispatcher = dispatcher;
this.peerStatusListeners = peerStatusListeners;
final String status = DiscoverNetworks.discoverInterfaces(bindings);
if (LOG.isInfoEnabled()) {
LOG.info("Status of interface search: " + status);
}
}
/**
* @return The binding that was used to setup the incoming connections
*/
public Bindings bindings() {
return bindings;
}
/**
* @return The channel server configuration.
*/
public ChannelServerConficuration channelServerConfiguration() {
return channelServerConfiguration;
}
/**
* Creates the {@link PeerAddress} based on the network discovery that was done in
* {@link #ChannelServer(Bindings, int, int, ChannelServerConficuration)}.
*
* @param peerId
* The id of this peer
* @return The peer address of this peer
* @throws IOException
* If the address could not be determined
*/
public PeerAddress findPeerAddress(final Number160 peerId) throws IOException {
InetAddress outsideAddress = bindings.getExternalAddress();
final PeerAddress self;
if (outsideAddress == null) {
if (bindings.getFoundAddresses().size() == 0) {
throw new IOException("Not listening to anything. Maybe your binding information is wrong.");
}
outsideAddress = bindings.getFoundAddresses().get(0);
final PeerSocketAddress peerSocketAddress = new PeerSocketAddress(outsideAddress, tcpPort,
udpPort);
self = new PeerAddress(peerId, peerSocketAddress, channelServerConfiguration.isBehindFirewall(),
channelServerConfiguration.isBehindFirewall(), false, new PeerSocketAddress[] {});
} else {
final PeerSocketAddress peerSocketAddress = new PeerSocketAddress(outsideAddress,
bindings.getOutsideTCPPort(), bindings.getOutsideUDPPort());
self = new PeerAddress(peerId, peerSocketAddress, channelServerConfiguration.isBehindFirewall(),
channelServerConfiguration.isBehindFirewall(), false, new PeerSocketAddress[] {});
}
return self;
}
/**
* Starts to listen to UDP and TCP ports.
*
* @throws IOException
* If the starup fails, e.g, ports already in use
*/
public void startup() throws IOException {
if (!channelServerConfiguration.disableBind()) {
final boolean listenAll = bindings.isListenAll();
if (listenAll) {
if (LOG.isInfoEnabled()) {
LOG.info("Listening for broadcasts on port udp: " + udpPort + " and tcp:" + tcpPort);
}
if (!startupTCP(new InetSocketAddress(tcpPort), channelServerConfiguration)
|| !startupUDP(new InetSocketAddress(udpPort), channelServerConfiguration)) {
throw new IOException("cannot bind TCP or UDP");
}
} else {
for (InetAddress addr : bindings.getFoundAddresses()) {
if (LOG.isInfoEnabled()) {
LOG.info("Listening on address: " + addr + " on port udp: " + udpPort + " and tcp:"
+ tcpPort);
}
if (!startupTCP(new InetSocketAddress(addr, tcpPort), channelServerConfiguration)
|| !startupUDP(new InetSocketAddress(addr, udpPort), channelServerConfiguration)) {
throw new IOException("cannot bind TCP or UDP");
}
}
}
}
}
/**
* Start to listen on a UPD port.
*
* @param listenAddresses
* The address to listen to
* @param config
* Can create handlers to be attached to this port
* @return True if startup was successful
*/
boolean startupUDP(final InetSocketAddress listenAddresses, final ChannelServerConficuration config) {
Bootstrap b = new Bootstrap();
b.group(workerGroup);
b.channel(NioDatagramChannel.class);
b.option(ChannelOption.SO_BROADCAST, true);
b.option(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(ConnectionBean.UDP_LIMIT));
b.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(final Channel ch) throws Exception {
for (Map.Entry<String, ChannelHandler> entry : handlers(false).entrySet()) {
ch.pipeline().addLast(entry.getKey(), entry.getValue());
}
}
});
ChannelFuture future = b.bind(listenAddresses);
channelUDP = future.channel();
return handleFuture(future);
}
/**
* Start to listen on a TCP port.
*
* @param listenAddresses
* The address to listen to
* @param config
* Can create handlers to be attached to this port
* @return True if startup was successful
*/
boolean startupTCP(final InetSocketAddress listenAddresses, final ChannelServerConficuration config) {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup);
b.channel(NioServerSocketChannel.class);
b.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(final Channel ch) throws Exception {
for (Map.Entry<String, ChannelHandler> entry : handlers(true).entrySet()) {
ch.pipeline().addLast(entry.getKey(), entry.getValue());
}
}
});
// b.option(ChannelOption.SO_BACKLOG, BACKLOG);
b.childOption(ChannelOption.SO_LINGER, 0);
b.childOption(ChannelOption.TCP_NODELAY, true);
ChannelFuture future = b.bind(listenAddresses);
channelTCP = future.channel();
return handleFuture(future);
}
/**
* Creates the Netty handlers. After it sends it to the user, where the handlers can be modified. We add a couple or
* null handlers where the user can add its own handler.
*
* @param tcp
* Set to true if connection is TCP, false if UDP
* @return The channel handlers that may have been modified by the user
*/
private Map<String, ChannelHandler> handlers(final boolean tcp) {
final ChannelHandler[] c = new TimeoutFactory(null, channelServerConfiguration.idleTCPSeconds(),
peerStatusListeners).twoTimeoutHandlers();
final Map<String, ChannelHandler> handlers;
if (tcp) {
final int nrTCPHandlers = 5;
handlers = new LinkedHashMap<String, ChannelHandler>(nrTCPHandlers);
handlers.put("timeout-server0", c[0]);
handlers.put("timeout-server1", c[1]);
handlers.put("decoder", new TomP2PCumulationTCP(channelServerConfiguration.signatureFactory()));
} else {
// we don't need here a timeout since we receive a packet or nothing. It is different than with TCP where we
// may get a stream and in the middle of it, the other peer goes offline. This cannot happen with UDP
final int nrUDPHandlers = 3;
handlers = new LinkedHashMap<String, ChannelHandler>(nrUDPHandlers);
handlers.put("decoder", new TomP2PSinglePacketUDP(channelServerConfiguration.signatureFactory()));
}
handlers.put("encoder", new TomP2POutbound(false, channelServerConfiguration.signatureFactory()));
handlers.put("dispatcher", dispatcher);
channelServerConfiguration.pipelineFilter().filter(handlers, tcp, false);
return handlers;
}
/**
* Handles the waiting and returning the channel.
*
* @param future
* The future to wait for
* @return The channel or null if we failed to bind.
*/
private boolean handleFuture(final ChannelFuture future) {
try {
future.await();
} catch (InterruptedException e) {
if (LOG.isWarnEnabled()) {
LOG.warn("could not start UPD server", e);
}
return false;
}
boolean success = future.isSuccess();
if (success) {
return true;
} else {
future.cause().printStackTrace();
return false;
}
}
/**
* Shuts down the server.
*
* @return The future when the shutdown is complete. This includes the worker and boss event loop
*/
public FutureDone<Void> shutdown() {
LOG.debug("shutdown UPD server");
channelUDP.close().addListener(new GenericFutureListener<ChannelFuture>() {
@Override
public void operationComplete(final ChannelFuture future) throws Exception {
LOG.debug("shutdown TCP server");
channelTCP.close().addListener(new GenericFutureListener<ChannelFuture>() {
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public void operationComplete(final ChannelFuture future) throws Exception {
LOG.debug("shutdown TCP workergroup");
workerGroup.shutdownGracefully(0, 0, TimeUnit.SECONDS).addListener(
new GenericFutureListener() {
@Override
public void operationComplete(final Future future) throws Exception {
LOG.debug("shutdown TCP workergroup done!");
bossGroup.shutdownGracefully(0, 0, TimeUnit.SECONDS).addListener(
new GenericFutureListener() {
@Override
public void operationComplete(final Future future)
throws Exception {
futureServerDone.setDone();
}
});
}
});
}
});
}
});
return shutdownFuture();
}
/**
* @return The shutdown future that is used when calling {@link #shutdown()}
*/
public FutureDone<Void> shutdownFuture() {
return futureServerDone;
}
}