Java tutorial
/* * Copyright (c) 2011-2014 Pivotal Software, Inc. * * 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 reactor.io.net.netty.tcp; import io.netty.bootstrap.Bootstrap; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.ssl.SslHandler; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.FutureListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.Environment; import reactor.bus.EventBus; import reactor.core.support.NamedDaemonThreadFactory; import reactor.fn.Consumer; import reactor.fn.Supplier; import reactor.fn.tuple.Tuple2; import reactor.io.buffer.Buffer; import reactor.io.codec.Codec; import reactor.io.net.NetChannel; import reactor.io.net.Reconnect; import reactor.io.net.config.ClientSocketOptions; import reactor.io.net.config.SslOptions; import reactor.io.net.netty.*; import reactor.io.net.tcp.TcpClient; import reactor.io.net.tcp.ssl.SSLEngineSupplier; import reactor.rx.Promise; import reactor.rx.Promises; import reactor.rx.Stream; import reactor.rx.Streams; import reactor.rx.stream.Broadcaster; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.net.ssl.SSLEngine; import java.net.InetSocketAddress; import java.util.Collection; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; /** * A Netty-based {@code TcpClient}. * * @param <IN> The type that will be received by this client * @param <OUT> The type that will be sent by this client * @author Jon Brisbin * @author Stephane Maldini */ public class NettyTcpClient<IN, OUT> extends TcpClient<IN, OUT> { private final Logger log = LoggerFactory.getLogger(NettyTcpClient.class); private final NettyClientSocketOptions nettyOptions; private final Bootstrap bootstrap; private final EventLoopGroup ioGroup; private final Supplier<ChannelFuture> connectionSupplier; private volatile InetSocketAddress connectAddress; private volatile boolean closing; /** * Creates a new NettyTcpClient that will use the given {@code env} for configuration and the given {@code * reactor} to * send events. The number of IO threads used by the client is configured by the environment's {@code * reactor.tcp.ioThreadCount} property. In its absence the number of IO threads will be equal to the {@link * Environment#PROCESSORS number of available processors}. </p> The client will connect to the given {@code * connectAddress}, configuring its socket using the given {@code opts}. The given {@code codec} will be used for * encoding and decoding of data. * * @param env The configuration environment * @param reactor The reactor used to send events * @param connectAddress The address the client will connect to * @param options The configuration options for the client's socket * @param sslOptions The SSL configuration options for the client's socket * @param codec The codec used to encode and decode data * @param consumers The consumers that will interact with the connection */ public NettyTcpClient(@Nonnull Environment env, @Nonnull EventBus reactor, @Nonnull InetSocketAddress connectAddress, @Nonnull final ClientSocketOptions options, @Nullable final SslOptions sslOptions, @Nullable Codec<Buffer, IN, OUT> codec, @Nonnull Collection<Consumer<NetChannel<IN, OUT>>> consumers) { super(env, reactor, connectAddress, options, sslOptions, codec, consumers); this.connectAddress = connectAddress; if (options instanceof NettyClientSocketOptions) { this.nettyOptions = (NettyClientSocketOptions) options; } else { this.nettyOptions = null; } if (null != nettyOptions && null != nettyOptions.eventLoopGroup()) { this.ioGroup = nettyOptions.eventLoopGroup(); } else { int ioThreadCount = env.getProperty("reactor.tcp.ioThreadCount", Integer.class, Environment.PROCESSORS); this.ioGroup = new NioEventLoopGroup(ioThreadCount, new NamedDaemonThreadFactory("reactor-tcp-io")); } this.bootstrap = new Bootstrap().group(ioGroup).channel(NioSocketChannel.class) .option(ChannelOption.SO_RCVBUF, options.rcvbuf()).option(ChannelOption.SO_SNDBUF, options.sndbuf()) .option(ChannelOption.SO_KEEPALIVE, options.keepAlive()) .option(ChannelOption.SO_LINGER, options.linger()) .option(ChannelOption.TCP_NODELAY, options.tcpNoDelay()).remoteAddress(this.connectAddress) .handler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(final SocketChannel ch) throws Exception { ch.config().setConnectTimeoutMillis(options.timeout()); if (null != sslOptions) { SSLEngine ssl = new SSLEngineSupplier(sslOptions, true).get(); if (log.isDebugEnabled()) { log.debug("SSL enabled using keystore {}", (null != sslOptions.keystoreFile() ? sslOptions.keystoreFile() : "<DEFAULT>")); } ch.pipeline().addLast(new SslHandler(ssl)); } if (null != nettyOptions && null != nettyOptions.pipelineConfigurer()) { nettyOptions.pipelineConfigurer().accept(ch.pipeline()); } ch.pipeline().addLast(createChannelHandlers(ch)); } }); this.connectionSupplier = new Supplier<ChannelFuture>() { @Override public ChannelFuture get() { if (!closing) { return bootstrap.connect(getConnectAddress()); } else { return null; } } }; } @Override public Promise<NetChannel<IN, OUT>> open() { final Promise<NetChannel<IN, OUT>> connection = Promises.ready(getEnvironment(), getReactor().getDispatcher()); openChannel(new ConnectingChannelListener(connection)); return connection; } @Override public Stream<NetChannel<IN, OUT>> open(final Reconnect reconnect) { final Broadcaster<NetChannel<IN, OUT>> connections = Streams.broadcast(getEnvironment(), getReactor().getDispatcher()); openChannel(new ReconnectingChannelListener(connectAddress, reconnect, connections)); return connections; } @Override public void close(@Nullable final Consumer<Boolean> onClose) { if (null != nettyOptions && null != nettyOptions.eventLoopGroup()) { ioGroup.submit(new Runnable() { @Override public void run() { if (null != onClose) { onClose.accept(true); } } }); } else { ioGroup.shutdownGracefully().addListener(new FutureListener<Object>() { @Override public void operationComplete(Future<Object> future) throws Exception { if (null != onClose) { onClose.accept(future.isDone() && future.isSuccess()); } } }); } } @Override protected <C> NetChannel<IN, OUT> createChannel(C ioChannel) { SocketChannel ch = (SocketChannel) ioChannel; int backlog = getEnvironment().getProperty("reactor.tcp.connectionReactorBacklog", Integer.class, 128); return new NettyNetChannel<IN, OUT>(getEnvironment(), getCodec(), new NettyEventLoopDispatcher(ch.eventLoop(), backlog), getReactor(), ch); } protected ChannelHandler[] createChannelHandlers(SocketChannel ioChannel) { NettyNetChannel<IN, OUT> conn = (NettyNetChannel<IN, OUT>) createChannel(ioChannel); NettyNetChannelInboundHandler readHandler = new NettyNetChannelInboundHandler().setNetChannel(conn); NettyNetChannelOutboundHandler writeHandler = new NettyNetChannelOutboundHandler(); return new ChannelHandler[] { readHandler, writeHandler }; } private void openChannel(ChannelFutureListener listener) { ChannelFuture channel = connectionSupplier.get(); if (null != channel && null != listener) { channel.addListener(listener); } } private class ConnectingChannelListener implements ChannelFutureListener { private final Promise<NetChannel<IN, OUT>> connection; private ConnectingChannelListener(Promise<NetChannel<IN, OUT>> connection) { this.connection = connection; } @SuppressWarnings("unchecked") @Override public void operationComplete(ChannelFuture future) throws Exception { if (!future.isSuccess()) { if (log.isErrorEnabled()) { log.error(future.cause().getMessage(), future.cause()); } connection.onError(future.cause()); return; } if (log.isInfoEnabled()) { log.info("CONNECTED: " + future.channel()); } NettyNetChannelInboundHandler inboundHandler = future.channel().pipeline() .get(NettyNetChannelInboundHandler.class); final NetChannel<IN, OUT> ch = inboundHandler.getNetChannel(); future.channel().closeFuture().addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { if (log.isInfoEnabled()) { log.info("CLOSED: " + future.channel()); } notifyClose(ch); } }); future.channel().eventLoop().submit(new Runnable() { @Override public void run() { connection.onNext(ch); } }); } } private class ReconnectingChannelListener implements ChannelFutureListener { private final AtomicInteger attempts = new AtomicInteger(0); private final Reconnect reconnect; private final Broadcaster<NetChannel<IN, OUT>> connections; private volatile InetSocketAddress connectAddress; private ReconnectingChannelListener(InetSocketAddress connectAddress, Reconnect reconnect, Broadcaster<NetChannel<IN, OUT>> connections) { this.connectAddress = connectAddress; this.reconnect = reconnect; this.connections = connections; } @SuppressWarnings("unchecked") @Override public void operationComplete(final ChannelFuture future) throws Exception { if (!future.isSuccess()) { int attempt = attempts.incrementAndGet(); Tuple2<InetSocketAddress, Long> tup = reconnect.reconnect(connectAddress, attempt); if (null == tup) { // do not attempt a reconnect if (log.isErrorEnabled()) { log.error("Reconnection to {} failed after {} attempts. Giving up.", connectAddress, attempt - 1); } future.channel().eventLoop().submit(new Runnable() { @Override public void run() { connections.onError(future.cause()); } }); return; } attemptReconnect(tup); } else { // connected if (log.isInfoEnabled()) { log.info("CONNECTED: " + future.channel()); } final Channel ioCh = future.channel(); final ChannelPipeline ioChPipline = ioCh.pipeline(); final NetChannel<IN, OUT> ch = ioChPipline.get(NettyNetChannelInboundHandler.class).getNetChannel(); ioChPipline.addLast(new ChannelDuplexHandler() { @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { if (log.isInfoEnabled()) { log.info("CLOSED: " + ioCh); } notifyClose(ch); Tuple2<InetSocketAddress, Long> tup = reconnect.reconnect(connectAddress, attempts.incrementAndGet()); if (null == tup) { // do not attempt a reconnect return; } if (!((NettyNetChannel) ch).isClosing()) { attemptReconnect(tup); } else { closing = true; } super.channelInactive(ctx); } }); ioCh.eventLoop().submit(new Runnable() { @Override public void run() { connections.onNext(ch); } }); } } private void attemptReconnect(Tuple2<InetSocketAddress, Long> tup) { connectAddress = tup.getT1(); bootstrap.remoteAddress(connectAddress); long delay = tup.getT2(); if (log.isInfoEnabled()) { log.info("Failed to connect to {}. Attempting reconnect in {}ms.", connectAddress, delay); } getEnvironment().getTimer().submit(new Consumer<Long>() { @Override public void accept(Long now) { openChannel(ReconnectingChannelListener.this); } }, delay, TimeUnit.MILLISECONDS).cancelAfterUse(); } } }