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.bootstrap.Bootstrap; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelException; 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.socket.nio.NioDatagramChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.util.concurrent.EventExecutorGroup; import io.netty.util.concurrent.GenericFutureListener; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.atomic.AtomicInteger; import net.tomp2p.futures.FutureDone; import net.tomp2p.message.TomP2PCumulationTCP; import net.tomp2p.message.TomP2POutbound; import net.tomp2p.message.TomP2PSinglePacketUDP; import net.tomp2p.peers.PeerStatusListener; import net.tomp2p.utils.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The "server" part that accepts connections. * * @author Thomas Bocek * */ public final class ChannelServer implements DiscoverNetworkListener { private static final Logger LOG = LoggerFactory.getLogger(ChannelServer.class); // private static final int BACKLOG = 128; private final EventLoopGroup bossGroup; private final EventLoopGroup workerGroup; private final Map<InetAddress, Channel> channelsTCP = Collections .synchronizedMap(new HashMap<InetAddress, Channel>()); private final Map<InetAddress, Channel> channelsUDP = Collections .synchronizedMap(new HashMap<InetAddress, Channel>()); private final FutureDone<Void> futureServerDone = new FutureDone<Void>(); private final ChannelServerConfiguration channelServerConfiguration; private final Dispatcher dispatcher; private final List<PeerStatusListener> peerStatusListeners; private final DropConnectionInboundHandler tcpDropConnectionInboundHandler; private final DropConnectionInboundHandler udpDropConnectionInboundHandler; private final ChannelHandler udpDecoderHandler; private final DiscoverNetworks discoverNetworks; private boolean shutdown = false; private boolean broadcastAddressSupported = false; private boolean broadcastAddressTried = false; /** * Sets parameters and starts network device discovery. * * @param bossGroup * * @param workerGroup * * @param channelServerConfiguration * The server configuration, that contains e.g. the handlers * @param dispatcher * The shared dispatcher * @param peerStatusListeners * The status listener for offline peers * @param timer * * @throws IOException * If device discovery failed. */ public ChannelServer(final EventLoopGroup bossGroup, final EventLoopGroup workerGroup, final ChannelServerConfiguration channelServerConfiguration, final Dispatcher dispatcher, final List<PeerStatusListener> peerStatusListeners, final ScheduledExecutorService timer) throws IOException { this.bossGroup = bossGroup; this.workerGroup = workerGroup; this.channelServerConfiguration = channelServerConfiguration; this.dispatcher = dispatcher; this.peerStatusListeners = peerStatusListeners; this.discoverNetworks = new DiscoverNetworks(5000, channelServerConfiguration.bindings(), timer); this.tcpDropConnectionInboundHandler = new DropConnectionInboundHandler( channelServerConfiguration.maxTCPIncomingConnections()); this.udpDropConnectionInboundHandler = new DropConnectionInboundHandler( channelServerConfiguration.maxUDPIncomingConnections()); this.udpDecoderHandler = new TomP2PSinglePacketUDP(channelServerConfiguration.signatureFactory(), channelServerConfiguration.byteBufAllocator()); discoverNetworks.addDiscoverNetworkListener(this); if (timer != null) { discoverNetworks.start().awaitUninterruptibly(); } } public DiscoverNetworks discoverNetworks() { return discoverNetworks; } /** * @return The channel server configuration. */ public ChannelServerConfiguration channelServerConfiguration() { return channelServerConfiguration; } @Override public void discoverNetwork(DiscoverResults discoverResults) { if (!channelServerConfiguration.isDisableBind()) { synchronized (ChannelServer.this) { if (shutdown) { return; } if (discoverResults.isListenAny()) { listenAny(); } else { listenSpecificInetAddresses(discoverResults); } } } } private void listenAny() { final InetSocketAddress tcpSocket = new InetSocketAddress(channelServerConfiguration.ports().tcpPort()); final boolean tcpStart = startupTCP(tcpSocket, channelServerConfiguration); if (!tcpStart) { LOG.warn("cannot bind TCP on socket {}", tcpSocket); } else { LOG.info("Listening TCP on socket {}", tcpSocket); } final InetSocketAddress udpSocket = new InetSocketAddress(channelServerConfiguration.ports().udpPort()); final boolean udpStart = startupUDP(udpSocket, channelServerConfiguration, true); if (!udpStart) { LOG.warn("cannot bind UDP on socket {}", udpSocket); } else { LOG.info("Listening UDP on socket {}", udpSocket); } } //this method has blocking calls in it private void listenSpecificInetAddresses(DiscoverResults discoverResults) { for (InetAddress inetAddress : discoverResults.newBroadcastAddresses()) { InetSocketAddress udpBroadcastSocket = new InetSocketAddress(inetAddress, channelServerConfiguration.ports().udpPort()); broadcastAddressTried = true; boolean udpStartBroadcast = startupUDP(udpBroadcastSocket, channelServerConfiguration, false); if (udpStartBroadcast) { //if one broadcast address was found, then we don't need to bind to 0.0.0.0 broadcastAddressSupported = true; LOG.info("Listening on broadcast address: {} on port udp: {}", udpBroadcastSocket, channelServerConfiguration.ports().udpPort()); } else { LOG.warn("cannot bind broadcast UDP {}", udpBroadcastSocket); } } for (InetAddress inetAddress : discoverResults.removedFoundBroadcastAddresses()) { Channel channelUDP = channelsUDP.remove(inetAddress); if (channelUDP != null) { channelUDP.close().awaitUninterruptibly(); } } boolean udpStartBroadcast = false; //if we tried but could not bind to a broadcast address. Happens on Windows, not on Mac/Linux if (!broadcastAddressSupported && broadcastAddressTried) { InetSocketAddress udpBroadcastSocket = new InetSocketAddress( channelServerConfiguration.ports().udpPort()); LOG.info("Listening on wildcard broadcast address {}", udpBroadcastSocket); udpStartBroadcast = startupUDP(udpBroadcastSocket, channelServerConfiguration, true); if (!udpStartBroadcast) { LOG.warn("cannot bind wildcard broadcast UDP on socket {}", udpBroadcastSocket); } } for (InetAddress inetAddress : discoverResults.newAddresses()) { InetSocketAddress tcpSocket = new InetSocketAddress(inetAddress, channelServerConfiguration.ports().tcpPort()); boolean tcpStart = startupTCP(tcpSocket, channelServerConfiguration); if (!tcpStart) { LOG.warn("cannot bind TCP on socket {}", tcpSocket); } else { LOG.info("Listening on address: {} on port tcp: {}", inetAddress, channelServerConfiguration.ports().tcpPort()); } //as we are listening to anything on UDP, we don't need to listen to any other interfaces if (!udpStartBroadcast) { InetSocketAddress udpSocket = new InetSocketAddress(inetAddress, channelServerConfiguration.ports().udpPort()); boolean udpStart = startupUDP(udpSocket, channelServerConfiguration, false); if (!udpStart) { LOG.warn("cannot bind UDP on socket {}", udpSocket); } else { LOG.info("Listening on address: {} on port udp: {}", inetAddress, channelServerConfiguration.ports().udpPort()); } } } for (InetAddress inetAddress : discoverResults.removedFoundAddresses()) { Channel channelTCP = channelsTCP.remove(inetAddress); if (channelTCP != null) { channelTCP.close().awaitUninterruptibly(); } Channel channelUDP = channelsUDP.remove(inetAddress); if (channelUDP != null) { channelUDP.close().awaitUninterruptibly(); } } } @Override public void exception(Throwable throwable) { LOG.error("discovery problem", throwable); } /** * 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 * @param broadcastFlag * @return True if startup was successful */ boolean startupUDP(final InetSocketAddress listenAddresses, final ChannelServerConfiguration config, boolean broadcastFlag) { Bootstrap b = new Bootstrap(); b.group(workerGroup); b.channel(NioDatagramChannel.class); //option broadcast only required as we not listen to the broadcast address directly if (broadcastFlag) { 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 { ch.config().setAllocator(channelServerConfiguration.byteBufAllocator()); for (Map.Entry<String, Pair<EventExecutorGroup, ChannelHandler>> entry : handlers(false) .entrySet()) { if (!entry.getValue().isEmpty()) { ch.pipeline().addLast(entry.getValue().element0(), entry.getKey(), entry.getValue().element1()); } else if (entry.getValue().element1() != null) { ch.pipeline().addLast(entry.getKey(), entry.getValue().element1()); } } } }); ChannelFuture future = b.bind(listenAddresses); channelsUDP.put(listenAddresses.getAddress(), 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 ChannelServerConfiguration 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 { ch.config().setAllocator(channelServerConfiguration.byteBufAllocator()); // b.option(ChannelOption.SO_BACKLOG, BACKLOG); bestEffortOptions(ch, ChannelOption.SO_LINGER, 0); bestEffortOptions(ch, ChannelOption.TCP_NODELAY, true); for (Map.Entry<String, Pair<EventExecutorGroup, ChannelHandler>> entry : handlers(true) .entrySet()) { if (!entry.getValue().isEmpty()) { ch.pipeline().addLast(entry.getValue().element0(), entry.getKey(), entry.getValue().element1()); } else if (entry.getValue().element1() != null) { ch.pipeline().addLast(entry.getKey(), entry.getValue().element1()); } } } }); ChannelFuture future = b.bind(listenAddresses); channelsTCP.put(listenAddresses.getAddress(), future.channel()); return handleFuture(future); } private static <T> void bestEffortOptions(final Channel ch, ChannelOption<T> option, T value) { try { ch.config().setOption(option, value); } catch (ChannelException e) { // Ignore } } /** * 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, Pair<EventExecutorGroup, ChannelHandler>> handlers(final boolean tcp) { TimeoutFactory timeoutFactory = new TimeoutFactory(null, channelServerConfiguration.idleTCPSeconds(), peerStatusListeners, "Server"); final Map<String, Pair<EventExecutorGroup, ChannelHandler>> handlers; if (tcp) { final int nrTCPHandlers = 8; // 6 / 0.75 = 7; handlers = new LinkedHashMap<String, Pair<EventExecutorGroup, ChannelHandler>>(nrTCPHandlers); handlers.put("dropconnection", new Pair<EventExecutorGroup, ChannelHandler>(null, tcpDropConnectionInboundHandler)); handlers.put("timeout0", new Pair<EventExecutorGroup, ChannelHandler>(null, timeoutFactory.idleStateHandlerTomP2P())); handlers.put("timeout1", new Pair<EventExecutorGroup, ChannelHandler>(null, timeoutFactory.timeHandler())); handlers.put("decoder", new Pair<EventExecutorGroup, ChannelHandler>(null, new TomP2PCumulationTCP( channelServerConfiguration.signatureFactory(), channelServerConfiguration.byteBufAllocator()))); } 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 = 6; // 4 = 0.75 = 4 handlers = new LinkedHashMap<String, Pair<EventExecutorGroup, ChannelHandler>>(nrUDPHandlers); handlers.put("dropconnection", new Pair<EventExecutorGroup, ChannelHandler>(null, udpDropConnectionInboundHandler)); handlers.put("decoder", new Pair<EventExecutorGroup, ChannelHandler>(null, udpDecoderHandler)); } handlers.put("encoder", new Pair<EventExecutorGroup, ChannelHandler>(null, new TomP2POutbound( channelServerConfiguration.signatureFactory(), channelServerConfiguration.byteBufAllocator()))); handlers.put("dispatcher", new Pair<EventExecutorGroup, ChannelHandler>(null, dispatcher)); return channelServerConfiguration.pipelineFilter().filter(handlers, tcp, false); } /** * 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 { LOG.debug("binding not successful", future.cause()); 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() { synchronized (this) { shutdown = true; } discoverNetworks.stop(); final int maxListeners = channelsTCP.size() + channelsUDP.size(); if (maxListeners == 0) { shutdownFuture().done(); } // we have two things to shut down: UDP and TCP final AtomicInteger listenerCounter = new AtomicInteger(0); LOG.debug("shutdown servers"); synchronized (channelsUDP) { for (Channel channelUDP : channelsUDP.values()) { channelUDP.close().addListener(new GenericFutureListener<ChannelFuture>() { @Override public void operationComplete(final ChannelFuture future) throws Exception { LOG.debug("shutdown TCP server"); if (listenerCounter.incrementAndGet() == maxListeners) { futureServerDone.done(); } } }); } } synchronized (channelsTCP) { for (Channel channelTCP : channelsTCP.values()) { channelTCP.close().addListener(new GenericFutureListener<ChannelFuture>() { @Override public void operationComplete(final ChannelFuture future) throws Exception { LOG.debug("shutdown TCP channels"); if (listenerCounter.incrementAndGet() == maxListeners) { futureServerDone.done(); } } }); } } return shutdownFuture(); } /** * @return The shutdown future that is used when calling {@link #shutdown()} */ public FutureDone<Void> shutdownFuture() { return futureServerDone; } }