/* * Copyright (c) 2011-2013 The original author or authors * ------------------------------------------------------ * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * and Apache License v2.0 which accompanies this distribution. * * The Eclipse Public License is available at * http://www.eclipse.org/legal/epl-v10.html * * The Apache License v2.0 is available at * http://www.opensource.org/licenses/apache2.0.php * * You may elect to redistribute this code under either of these licenses. */ package io.vertx.core.net.impl; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBufAllocator; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; import io.netty.channel.EventLoop; import io.netty.channel.FixedRecvByteBufAllocator; import io.netty.channel.group.ChannelGroup; import io.netty.channel.group.ChannelGroupFuture; import io.netty.channel.group.DefaultChannelGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.ssl.SslHandler; import io.netty.util.concurrent.GlobalEventExecutor; import io.vertx.core.AsyncResult; import io.vertx.core.Closeable; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.impl.ContextImpl; import io.vertx.core.impl.VertxInternal; import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; import io.vertx.core.net.NetServerOptions; import io.vertx.core.spi.metrics.Metrics; import io.vertx.core.spi.metrics.MetricsProvider; import io.vertx.core.spi.metrics.TCPMetrics; import java.net.InetSocketAddress; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * * This class is thread-safe * * @author <a href="http://tfox.org">Tim Fox</a> */ public abstract class NetServerBase<C extends ConnectionBase> implements Closeable, MetricsProvider { private static final Logger log = LoggerFactory.getLogger(NetServerBase.class); protected final VertxInternal vertx; protected final NetServerOptions options; protected final ContextImpl creatingContext; protected final SSLHelper sslHelper; protected final boolean logEnabled; private final Map<Channel, C> socketMap = new ConcurrentHashMap<>(); private final VertxEventLoopGroup availableWorkers = new VertxEventLoopGroup(); private final HandlerManager<Handler<? super C>> handlerManager = new HandlerManager<>(availableWorkers); private ChannelGroup serverChannelGroup; private boolean paused; private volatile boolean listening; private Handler<? super C> registeredHandler; private volatile ServerID id; private NetServerBase actualServer; private AsyncResolveConnectHelper bindFuture; private volatile int actualPort; private ContextImpl listenContext; private TCPMetrics metrics; public NetServerBase(VertxInternal vertx, NetServerOptions options) { this.vertx = vertx; this.options = new NetServerOptions(options); this.sslHelper = new SSLHelper(options, options.getKeyCertOptions(), options.getTrustOptions()); this.creatingContext = vertx.getContext(); this.logEnabled = options.getLogActivity(); if (creatingContext != null) { if (creatingContext.isMultiThreadedWorkerContext()) { throw new IllegalStateException("Cannot use NetServer in a multi-threaded worker verticle"); } creatingContext.addCloseHook(this); } } protected synchronized void pauseAccepting() { paused = true; } protected synchronized void resumeAccepting() { paused = false; } protected synchronized boolean isPaused() { return paused; } protected boolean isListening() { return listening; } protected abstract void initChannel(ChannelPipeline pipeline); public synchronized void listen(Handler<? super C> handler, int port, String host, Handler<AsyncResult<Void>> listenHandler) { if (handler == null) { throw new IllegalStateException("Set connect handler first"); } if (listening) { throw new IllegalStateException("Listen already called"); } listening = true; listenContext = vertx.getOrCreateContext(); registeredHandler = handler; synchronized (vertx.sharedNetServers()) { this.actualPort = port; // Will be updated on bind for a wildcard port id = new ServerID(port, host); NetServerBase shared = vertx.sharedNetServers().get(id); if (shared == null || port == 0) { // Wildcard port will imply a new actual server each time serverChannelGroup = new DefaultChannelGroup("vertx-acceptor-channels", GlobalEventExecutor.INSTANCE); ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(availableWorkers); bootstrap.channel(NioServerSocketChannel.class); sslHelper.validate(vertx); bootstrap.childHandler(new ChannelInitializer<Channel>() { @Override protected void initChannel(Channel ch) throws Exception { if (isPaused()) { ch.close(); return; } ChannelPipeline pipeline = ch.pipeline(); NetServerBase.this.initChannel(ch.pipeline()); pipeline.addLast("handler", new ServerHandler(ch)); } }); applyConnectionOptions(bootstrap); handlerManager.addHandler(handler, listenContext); try { bindFuture = AsyncResolveConnectHelper.doBind(vertx, port, host, bootstrap); bindFuture.addListener(res -> { if (res.succeeded()) { Channel ch = res.result(); log.trace("Net server listening on " + host + ":" + ch.localAddress()); // Update port to actual port - wildcard port 0 might have been used NetServerBase.this.actualPort = ((InetSocketAddress)ch.localAddress()).getPort(); NetServerBase.this.id = new ServerID(NetServerBase.this.actualPort, id.host); serverChannelGroup.add(ch); vertx.sharedNetServers().put(id, NetServerBase.this); metrics = vertx.metricsSPI().createMetrics(new SocketAddressImpl(id.port, id.host), options); } else { vertx.sharedNetServers().remove(id); } }); } catch (Throwable t) { // Make sure we send the exception back through the handler (if any) if (listenHandler != null) { vertx.runOnContext(v -> listenHandler.handle(Future.failedFuture(t))); } else { // No handler - log so user can see failure log.error(t); } listening = false; return; } if (port != 0) { vertx.sharedNetServers().put(id, this); } actualServer = this; } else { // Server already exists with that host/port - we will use that actualServer = shared; this.actualPort = shared.actualPort(); metrics = vertx.metricsSPI().createMetrics(new SocketAddressImpl(id.port, id.host), options); actualServer.handlerManager.addHandler(handler, listenContext); } // just add it to the future so it gets notified once the bind is complete actualServer.bindFuture.addListener(res -> { if (listenHandler != null) { AsyncResult<Void> ares; if (res.succeeded()) { ares = Future.succeededFuture(); } else { listening = false; ares = Future.failedFuture(res.cause()); } // Call with expectRightThread = false as if server is already listening // Netty will call future handler immediately with calling thread // which might be a non Vert.x thread (if running embedded) listenContext.runOnContext(v -> listenHandler.handle(ares)); } else if (res.failed()) { // No handler - log so user can see failure log.error("Failed to listen", res.cause()); listening = false; } }); } return; } public synchronized void close() { close(null); } @Override public synchronized void close(Handler<AsyncResult<Void>> done) { ContextImpl context = vertx.getOrCreateContext(); if (!listening) { if (done != null) { executeCloseDone(context, done, null); } return; } listening = false; synchronized (vertx.sharedNetServers()) { if (actualServer != null) { actualServer.handlerManager.removeHandler(registeredHandler, listenContext); if (actualServer.handlerManager.hasHandlers()) { // The actual server still has handlers so we don't actually close it if (done != null) { executeCloseDone(context, done, null); } } else { // No Handlers left so close the actual server // The done handler needs to be executed on the context that calls close, NOT the context // of the actual server actualServer.actualClose(context, done); } } } if (creatingContext != null) { creatingContext.removeCloseHook(this); } } public synchronized int actualPort() { return actualPort; } @Override public boolean isMetricsEnabled() { return metrics != null && metrics.isEnabled(); } @Override public Metrics getMetrics() { return metrics; } private void actualClose(ContextImpl closeContext, Handler<AsyncResult<Void>> done) { if (id != null) { vertx.sharedNetServers().remove(id); } ContextImpl currCon = vertx.getContext(); for (C sock : socketMap.values()) { sock.close(); } // Sanity check if (vertx.getContext() != currCon) { throw new IllegalStateException("Context was changed"); } ChannelGroupFuture fut = serverChannelGroup.close(); fut.addListener(cg -> { if (metrics != null) { metrics.close(); } executeCloseDone(closeContext, done, fut.cause()); }); } private void executeCloseDone(ContextImpl closeContext, Handler<AsyncResult<Void>> done, Exception e) { if (done != null) { Future<Void> fut = e == null ? Future.succeededFuture() : Future.failedFuture(e); closeContext.runOnContext(v -> done.handle(fut)); } } private class ServerHandler extends VertxNetHandler<C> { public ServerHandler(Channel ch) { super(ch, socketMap); } @Override protected void handleMsgReceived(C conn, Object msg) { NetServerBase.this.handleMsgReceived(conn, msg); } @Override protected Object safeObject(Object msg, ByteBufAllocator allocator) throws Exception { return NetServerBase.this.safeObject(msg, allocator); } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { Channel ch = ctx.channel(); EventLoop worker = ch.eventLoop(); //Choose a handler HandlerHolder<Handler<? super C>> handler = handlerManager.chooseHandler(worker); if (handler == null) { //Ignore return; } if (sslHelper.isSSL()) { io.netty.util.concurrent.Future<Channel> handshakeFuture; if (options.isSni()) { VertxSniHandler sniHandler = new VertxSniHandler(sslHelper, vertx); handshakeFuture = sniHandler.handshakeFuture(); ch.pipeline().addFirst("ssl", sniHandler); } else { SslHandler sslHandler = new SslHandler(sslHelper.createEngine(vertx)); handshakeFuture = sslHandler.handshakeFuture(); ch.pipeline().addFirst("ssl", sslHandler); } handshakeFuture.addListener(future -> { if (future.isSuccess()) { connected(ch, handler); } else { log.error("Client from origin " + ch.remoteAddress() + " failed to connect over ssl: " + future.cause()); } }); } else { connected(ch, handler); } } private void connected(Channel ch, HandlerHolder<Handler<? super C>> handler) { // Need to set context before constructor is called as writehandler registration needs this ContextImpl.setContext(handler.context); C sock = createConnection(vertx, ch, handler.context, sslHelper, metrics); socketMap.put(ch, sock); VertxNetHandler netHandler = ch.pipeline().get(VertxNetHandler.class); netHandler.conn = sock; handler.context.executeFromIO(() -> { sock.metric(metrics.connected(sock.remoteAddress(), sock.remoteName())); handler.handler.handle(sock); }); } } protected abstract void handleMsgReceived(C conn, Object msg); protected abstract Object safeObject(Object msg, ByteBufAllocator allocator); /** * Create a connection for a channel. * * @return the created connection */ protected abstract C createConnection(VertxInternal vertx, Channel channel, ContextImpl context, SSLHelper helper, TCPMetrics metrics); /** * Apply the connection option to the server. * * @param bootstrap the Netty server bootstrap */ protected void applyConnectionOptions(ServerBootstrap bootstrap) { bootstrap.childOption(ChannelOption.TCP_NODELAY, options.isTcpNoDelay()); if (options.getSendBufferSize() != -1) { bootstrap.childOption(ChannelOption.SO_SNDBUF, options.getSendBufferSize()); } if (options.getReceiveBufferSize() != -1) { bootstrap.childOption(ChannelOption.SO_RCVBUF, options.getReceiveBufferSize()); bootstrap.childOption(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(options.getReceiveBufferSize())); } if (options.getSoLinger() != -1) { bootstrap.option(ChannelOption.SO_LINGER, options.getSoLinger()); } if (options.getTrafficClass() != -1) { bootstrap.childOption(ChannelOption.IP_TOS, options.getTrafficClass()); } bootstrap.childOption(ChannelOption.ALLOCATOR, PartialPooledByteBufAllocator.INSTANCE); bootstrap.childOption(ChannelOption.SO_KEEPALIVE, options.isTcpKeepAlive()); bootstrap.option(ChannelOption.SO_REUSEADDR, options.isReuseAddress()); if (options.getAcceptBacklog() != -1) { bootstrap.option(ChannelOption.SO_BACKLOG, options.getAcceptBacklog()); } } @Override protected void finalize() throws Throwable { // Make sure this gets cleaned up if there are no more references to it // so as not to leave connections and resources dangling until the system is shutdown // which could make the JVM run out of file handles. close(); super.finalize(); } }