Java tutorial
/* * * Copyright (C) 2013-2016 Matt Baxter http://kitteh.org * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, copy, * modify, merge, publish, distribute, sublicense, and/or sell copies * of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package org.kitteh.irc.client.library.implementation; import io.netty.bootstrap.Bootstrap; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.ChannelOutboundHandlerAdapter; import io.netty.channel.EventLoopGroup; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.DelimiterBasedFrameDecoder; import io.netty.handler.codec.MessageToMessageEncoder; import io.netty.handler.codec.string.StringDecoder; import io.netty.handler.codec.string.StringEncoder; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.timeout.IdleState; import io.netty.handler.timeout.IdleStateEvent; import io.netty.handler.timeout.IdleStateHandler; import io.netty.util.CharsetUtil; import io.netty.util.concurrent.ScheduledFuture; import org.kitteh.irc.client.library.event.client.ClientConnectionClosedEvent; import org.kitteh.irc.client.library.exception.KittehConnectionException; import org.kitteh.irc.client.library.util.QueueProcessingThread; import org.kitteh.irc.client.library.util.ToStringer; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.net.ssl.SSLException; import javax.net.ssl.TrustManagerFactory; import java.io.File; import java.io.IOException; import java.net.SocketAddress; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.util.HashSet; import java.util.List; import java.util.Queue; import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.TimeUnit; final class NettyManager { static final class ClientConnection { private static final int MAX_LINE_LENGTH = 2048; private final InternalClient client; private final Channel channel; private final Queue<String> queue = new ConcurrentLinkedQueue<>(); private boolean reconnect = true; private ScheduledFuture<?> scheduledSending; private ScheduledFuture<?> scheduledPing; private final Object scheduledSendingLock = new Object(); private final Object immediateSendingLock = new Object(); private boolean immediateSendingReady = false; private final QueueProcessingThread<String> immediateSending; private ClientConnection(@Nonnull final InternalClient client, @Nonnull ChannelFuture channelFuture) { this.client = client; this.channel = channelFuture.channel(); this.immediateSending = new QueueProcessingThread<String>( "Kitteh IRC Client Immediate Sending Queue (" + client.getName() + ')') { @Override protected void processElement(String message) { synchronized (ClientConnection.this.immediateSendingLock) { if (!ClientConnection.this.immediateSendingReady) { try { ClientConnection.this.immediateSendingLock.wait(); } catch (InterruptedException e) { return; } } ClientConnection.this.channel.writeAndFlush(message); } } }; channelFuture.addListener(future -> { if (future.isSuccess()) { this.buildOurFutureTogether(); synchronized (ClientConnection.this.immediateSendingLock) { this.immediateSendingReady = true; this.immediateSendingLock.notify(); } } else { this.client.getExceptionListener().queue(new KittehConnectionException(future.cause(), false)); this.scheduleReconnect(); removeClientConnection(ClientConnection.this, ClientConnection.this.reconnect); } }); } private void buildOurFutureTogether() { // Outbound - Processed in pipeline back to front. this.channel.pipeline().addFirst("[OUTPUT] Output listener", new MessageToMessageEncoder<String>() { @Override protected void encode(ChannelHandlerContext ctx, String msg, List<Object> out) throws Exception { ClientConnection.this.client.getOutputListener().queue(msg); out.add(msg); } }); this.channel.pipeline().addFirst("[OUTPUT] Add line breaks", new MessageToMessageEncoder<String>() { @Override protected void encode(ChannelHandlerContext ctx, String msg, List<Object> out) throws Exception { out.add(msg + "\r\n"); } }); this.channel.pipeline().addFirst("[OUTPUT] String encoder", new StringEncoder(CharsetUtil.UTF_8)); // Handle timeout this.channel.pipeline().addLast("[INPUT] Idle state handler", new IdleStateHandler(250, 0, 0)); this.channel.pipeline().addLast("[INPUT] Catch idle", new ChannelDuplexHandler() { @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt instanceof IdleStateEvent) { IdleStateEvent e = (IdleStateEvent) evt; if ((e.state() == IdleState.READER_IDLE) && e.isFirst()) { ClientConnection.this.shutdown("Reconnecting...", true); } } } }); // Inbound this.channel.pipeline().addLast("[INPUT] Line splitter", new DelimiterBasedFrameDecoder(MAX_LINE_LENGTH, Unpooled.wrappedBuffer(new byte[] { (byte) '\r', (byte) '\n' }))); this.channel.pipeline().addLast("[INPUT] String decoder", new StringDecoder(CharsetUtil.UTF_8)); this.channel.pipeline().addLast("[INPUT] Send to client", new SimpleChannelInboundHandler<String>() { @Override protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception { if (msg == null) { return; } ClientConnection.this.client.getInputListener().queue(msg); ClientConnection.this.client.processLine(msg); } }); // SSL if (this.client.getConfig().getNotNull(Config.SSL)) { try { File keyCertChainFile = this.client.getConfig().get(Config.SSL_KEY_CERT_CHAIN); File keyFile = this.client.getConfig().get(Config.SSL_KEY); String keyPassword = this.client.getConfig().get(Config.SSL_KEY_PASSWORD); TrustManagerFactory factory = this.client.getConfig().get(Config.SSL_TRUST_MANAGER_FACTORY); if (factory == null) { factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); factory.init((KeyStore) null); } SslContext sslContext = SslContextBuilder.forClient().trustManager(factory) .keyManager(keyCertChainFile, keyFile, keyPassword).build(); this.channel.pipeline().addFirst(sslContext.newHandler(this.channel.alloc())); } catch (SSLException | NoSuchAlgorithmException | KeyStoreException e) { this.client.getExceptionListener().queue(new KittehConnectionException(e, true)); return; } } // Exception handling this.channel.pipeline().addLast("[INPUT] Exception handler", new ChannelInboundHandlerAdapter() { @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { if (cause instanceof Exception) { ClientConnection.this.handleException((Exception) cause); } } }); this.channel.pipeline().addFirst("[OUTPUT] Exception handler", new ChannelOutboundHandlerAdapter() { @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { if (cause instanceof Exception) { ClientConnection.this.handleException((Exception) cause); } } }); // Clean up on disconnect this.channel.closeFuture().addListener(futureListener -> { if (ClientConnection.this.reconnect) { this.scheduleReconnect(); } this.immediateSending.interrupt(); ClientConnection.this.client.getEventManager().callEvent(new ClientConnectionClosedEvent( ClientConnection.this.client, ClientConnection.this.reconnect)); removeClientConnection(ClientConnection.this, ClientConnection.this.reconnect); }); } private void scheduleReconnect() { ClientConnection.this.channel.eventLoop().schedule(ClientConnection.this.client::connect, 5, TimeUnit.SECONDS); } void sendMessage(@Nonnull String message, boolean priority) { this.sendMessage(message, priority, false); } void sendMessage(@Nonnull String message, boolean priority, boolean avoidDuplicates) { if (priority) { this.immediateSending.queue(message); } else if (!avoidDuplicates || !this.queue.contains(message)) { this.queue.add(message); } } void shutdown(@Nullable String message) { this.shutdown(message, false); } void startSending() { this.schedule(true); } void updateScheduling() { this.schedule(false); } private void handleException(Exception thrown) { this.client.getExceptionListener().queue(thrown); if (thrown instanceof IOException) { this.shutdown("IO Error. Reconnecting...", true); } } private void schedule(boolean force) { synchronized (this.scheduledSendingLock) { if (!force && (this.scheduledSending == null)) { return; } long delay = 0; if (this.scheduledSending != null) { delay = this.scheduledSending.getDelay(TimeUnit.MILLISECONDS); // Negligible added delay processing this this.scheduledSending.cancel(false); } if (this.scheduledPing != null) { this.scheduledPing.cancel(false); } this.scheduledSending = this.channel.eventLoop().scheduleAtFixedRate(() -> { String message = ClientConnection.this.queue.poll(); if (message != null) { ClientConnection.this.channel.writeAndFlush(message); } }, delay, this.client.getMessageDelay(), TimeUnit.MILLISECONDS); this.scheduledPing = this.channel.eventLoop().scheduleWithFixedDelay(this.client::ping, 60, 60, TimeUnit.SECONDS); } } private void shutdown(@Nullable String message, boolean reconnect) { this.reconnect = reconnect; this.sendMessage("QUIT" + ((message != null) ? (" :" + message) : ""), true); this.channel.close(); } @Nonnull @Override public String toString() { return new ToStringer(this).add("client", this.client).toString(); } } @Nullable private static Bootstrap bootstrap; @Nullable private static EventLoopGroup eventLoopGroup; private static final Set<ClientConnection> connections = new HashSet<>(); private NettyManager() { } private static synchronized void removeClientConnection(@Nonnull ClientConnection connection, boolean reconnecting) { connections.remove(connection); if (!reconnecting && connections.isEmpty()) { if (eventLoopGroup != null) { eventLoopGroup.shutdownGracefully(); } eventLoopGroup = null; bootstrap = null; } } static synchronized ClientConnection connect(@Nonnull InternalClient client) { if (bootstrap == null) { bootstrap = new Bootstrap(); bootstrap.channel(NioSocketChannel.class); bootstrap.handler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel channel) throws Exception { // NOOP } }); bootstrap.option(ChannelOption.TCP_NODELAY, true); eventLoopGroup = new NioEventLoopGroup(); bootstrap.group(eventLoopGroup); } SocketAddress bind = client.getConfig().get(Config.BIND_ADDRESS); SocketAddress server = client.getConfig().getNotNull(Config.SERVER_ADDRESS); ClientConnection clientConnection; if (bind == null) { clientConnection = new ClientConnection(client, bootstrap.connect(server)); } else { clientConnection = new ClientConnection(client, bootstrap.connect(server, bind)); } connections.add(clientConnection); return clientConnection; } @Nonnull @Override public String toString() { return new ToStringer(this).toString(); } }