diff --git a/src/dorkbox/network/Broadcast.java b/src/dorkbox/network/Broadcast.java index a7295ebe..a0624fab 100644 --- a/src/dorkbox/network/Broadcast.java +++ b/src/dorkbox/network/Broadcast.java @@ -29,6 +29,8 @@ import java.util.concurrent.TimeUnit; import org.slf4j.Logger; +import dorkbox.network.pipeline.MagicBytes; +import dorkbox.network.pipeline.discovery.BroadcastResponse; import dorkbox.network.pipeline.discovery.ClientDiscoverHostHandler; import dorkbox.network.pipeline.discovery.ClientDiscoverHostInitializer; import dorkbox.util.OS; @@ -48,9 +50,6 @@ import io.netty.channel.socket.oio.OioDatagramChannel; @SuppressWarnings({"unused", "AutoBoxing"}) public final class Broadcast { - public static final byte broadcastID = (byte) 42; - public static final byte broadcastResponseID = (byte) 57; - private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(Client.class.getSimpleName()); /** @@ -74,10 +73,10 @@ class Broadcast { * @return the first server found, or null if no server responded. */ public static - String discoverHost(int udpPort, int discoverTimeoutMillis) { - InetAddress discoverHost = discoverHostAddress(udpPort, discoverTimeoutMillis); + BroadcastResponse discoverHost(int udpPort, int discoverTimeoutMillis) { + BroadcastResponse discoverHost = discoverHostAddress(udpPort, discoverTimeoutMillis); if (discoverHost != null) { - return discoverHost.getHostAddress(); + return discoverHost; } return null; } @@ -93,8 +92,8 @@ class Broadcast { * @return the first server found, or null if no server responded. */ public static - InetAddress discoverHostAddress(int udpPort, int discoverTimeoutMillis) { - List servers = discoverHost0(udpPort, discoverTimeoutMillis, false); + BroadcastResponse discoverHostAddress(int udpPort, int discoverTimeoutMillis) { + List servers = discoverHost0(udpPort, discoverTimeoutMillis, false); if (servers.isEmpty()) { return null; } @@ -114,18 +113,18 @@ class Broadcast { * @return the list of found servers (if they responded) */ public static - List discoverHosts(int udpPort, int discoverTimeoutMillis) { + List discoverHosts(int udpPort, int discoverTimeoutMillis) { return discoverHost0(udpPort, discoverTimeoutMillis, true); } private static - List discoverHost0(int udpPort, int discoverTimeoutMillis, boolean fetchAllServers) { + List discoverHost0(int udpPort, int discoverTimeoutMillis, boolean fetchAllServers) { // fetch a buffer that contains the serialized object. ByteBuf buffer = Unpooled.buffer(1); - buffer.writeByte(broadcastID); + buffer.writeByte(MagicBytes.broadcastID); - List servers = new ArrayList(); + List servers = new ArrayList(); Logger logger2 = logger; @@ -134,7 +133,7 @@ class Broadcast { networkInterfaces = NetworkInterface.getNetworkInterfaces(); } catch (SocketException e) { logger2.error("Host discovery failed.", e); - return new ArrayList(0); + return new ArrayList(0); } @@ -155,7 +154,7 @@ class Broadcast { try { if (logger2.isInfoEnabled()) { - logger2.info("Searching for host on {}:{}", address, udpPort); + logger2.info("Searching for host on [{}:{}]", address.getHostAddress(), udpPort); } EventLoopGroup group; @@ -207,9 +206,8 @@ class Broadcast { } } else { - InetSocketAddress attachment = channel1.attr(ClientDiscoverHostHandler.STATE) - .get(); - servers.add(attachment.getAddress()); + BroadcastResponse broadcastResponse = channel1.attr(ClientDiscoverHostHandler.STATE).get(); + servers.add(broadcastResponse); } @@ -244,9 +242,9 @@ class Broadcast { } } else { - InetSocketAddress attachment = channel1.attr(ClientDiscoverHostHandler.STATE) - .get(); - servers.add(attachment.getAddress()); + BroadcastResponse broadcastResponse = channel1.attr(ClientDiscoverHostHandler.STATE).get(); + servers.add(broadcastResponse); + if (!fetchAllServers) { break; } @@ -272,21 +270,48 @@ class Broadcast { if (logger2.isInfoEnabled() && !servers.isEmpty()) { + StringBuilder stringBuilder = new StringBuilder(256); + if (fetchAllServers) { - StringBuilder stringBuilder = new StringBuilder(256); stringBuilder.append("Discovered servers: (") .append(servers.size()) .append(")"); - for (InetAddress server : servers) { + + for (BroadcastResponse server : servers) { stringBuilder.append("/n") - .append(server) - .append(":") - .append(udpPort); + .append(server.remoteAddress) + .append(":"); + + if (server.tcpPort > 0) { + stringBuilder.append(server.tcpPort); + + if (server.udpPort > 0) { + stringBuilder.append(":"); + } + } + if (server.udpPort > 0) { + stringBuilder.append(udpPort); + } } logger2.info(stringBuilder.toString()); } else { - logger2.info("Discovered server: {}:{}", servers.get(0), udpPort); + BroadcastResponse server = servers.get(0); + stringBuilder.append(server.remoteAddress) + .append(":"); + + if (server.tcpPort > 0) { + stringBuilder.append(server.tcpPort); + + if (server.udpPort > 0) { + stringBuilder.append(":"); + } + } + if (server.udpPort > 0) { + stringBuilder.append(udpPort); + } + + logger2.info("Discovered server [{}]", stringBuilder.toString()); } } diff --git a/src/dorkbox/network/Client.java b/src/dorkbox/network/Client.java index 4f69cf6a..df781d46 100644 --- a/src/dorkbox/network/Client.java +++ b/src/dorkbox/network/Client.java @@ -15,6 +15,8 @@ */ package dorkbox.network; +import static dorkbox.network.pipeline.ConnectionType.LOCAL; + import java.io.IOException; import java.net.InetSocketAddress; @@ -30,26 +32,20 @@ import dorkbox.network.connection.registration.remote.RegistrationRemoteHandlerC import dorkbox.network.rmi.RemoteObject; import dorkbox.network.rmi.RemoteObjectCallback; import dorkbox.network.rmi.TimeoutException; -import dorkbox.util.NamedThreadFactory; import dorkbox.util.OS; import dorkbox.util.exceptions.SecurityException; import io.netty.bootstrap.Bootstrap; import io.netty.buffer.PooledByteBufAllocator; import io.netty.channel.ChannelOption; -import io.netty.channel.DefaultEventLoopGroup; import io.netty.channel.EventLoopGroup; import io.netty.channel.FixedRecvByteBufAllocator; import io.netty.channel.WriteBufferWaterMark; import io.netty.channel.epoll.EpollDatagramChannel; -import io.netty.channel.epoll.EpollEventLoopGroup; import io.netty.channel.epoll.EpollSocketChannel; import io.netty.channel.kqueue.KQueueDatagramChannel; -import io.netty.channel.kqueue.KQueueEventLoopGroup; import io.netty.channel.kqueue.KQueueSocketChannel; import io.netty.channel.local.LocalAddress; import io.netty.channel.local.LocalChannel; -import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.oio.OioEventLoopGroup; import io.netty.channel.socket.nio.NioDatagramChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.channel.socket.oio.OioDatagramChannel; @@ -112,41 +108,18 @@ class Client extends EndPointClient implements Connection localChannelName = config.localChannelName; hostName = config.host; + final EventLoopGroup workerEventLoop = newEventLoop(DEFAULT_THREAD_POOL_SIZE, threadName); - final EventLoopGroup boss; - - if (OS.isAndroid()) { - // android ONLY supports OIO (not NIO) - boss = new OioEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup)); - } - else if (OS.isLinux() && NativeLibrary.isAvailable()) { - // JNI network stack is MUCH faster (but only on linux) - boss = new EpollEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup)); - } - else if (OS.isMacOsX() && NativeLibrary.isAvailable()) { - // KQueue network stack is MUCH faster (but only on macosx) - boss = new KQueueEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup)); - } - else { - boss = new NioEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup)); - } - - manageForShutdown(boss); if (config.localChannelName != null && config.tcpPort <= 0 && config.udpPort <= 0) { // no networked bootstraps. LOCAL connection only Bootstrap localBootstrap = new Bootstrap(); bootstraps.add(new BootstrapWrapper("LOCAL", config.localChannelName, -1, localBootstrap)); - EventLoopGroup localBoss = new DefaultEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName + "-LOCAL", - threadGroup)); - - localBootstrap.group(localBoss) + localBootstrap.group(newEventLoop(LOCAL, 1, threadName + "-JVM-BOSS")) .channel(LocalChannel.class) .remoteAddress(new LocalAddress(config.localChannelName)) - .handler(new RegistrationLocalHandlerClient(threadName, registrationWrapper)); - - manageForShutdown(localBoss); + .handler(new RegistrationLocalHandlerClient(threadName, registrationWrapper, workerEventLoop)); } else { if (config.host == null) { @@ -166,7 +139,7 @@ class Client extends EndPointClient implements Connection tcpBootstrap.channel(OioSocketChannel.class); } else if (OS.isLinux() && NativeLibrary.isAvailable()) { - // JNI network stack is MUCH faster (but only on linux) + // epoll network stack is MUCH faster (but only on linux) tcpBootstrap.channel(EpollSocketChannel.class); } else if (OS.isMacOsX() && NativeLibrary.isAvailable()) { @@ -177,12 +150,11 @@ class Client extends EndPointClient implements Connection tcpBootstrap.channel(NioSocketChannel.class); } - tcpBootstrap.group(boss) + tcpBootstrap.group(newEventLoop(1, threadName + "-TCP-BOSS")) .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) .option(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(WRITE_BUFF_LOW, WRITE_BUFF_HIGH)) .remoteAddress(config.host, config.tcpPort) - .handler(new RegistrationRemoteHandlerClientTCP(threadName, - registrationWrapper)); + .handler(new RegistrationRemoteHandlerClientTCP(threadName, registrationWrapper, workerEventLoop)); // android screws up on this!! tcpBootstrap.option(ChannelOption.TCP_NODELAY, !OS.isAndroid()) @@ -199,7 +171,7 @@ class Client extends EndPointClient implements Connection udpBootstrap.channel(OioDatagramChannel.class); } else if (OS.isLinux() && NativeLibrary.isAvailable()) { - // JNI network stack is MUCH faster (but only on linux) + // epoll network stack is MUCH faster (but only on linux) udpBootstrap.channel(EpollDatagramChannel.class); } else if (OS.isMacOsX() && NativeLibrary.isAvailable()) { @@ -211,15 +183,14 @@ class Client extends EndPointClient implements Connection } - udpBootstrap.group(boss) + udpBootstrap.group(newEventLoop(1, threadName + "-UDP-BOSS")) .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) // Netty4 has a default of 2048 bytes as upper limit for datagram packets. .option(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(EndPoint.udpMaxSize)) .option(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(WRITE_BUFF_LOW, WRITE_BUFF_HIGH)) .localAddress(new InetSocketAddress(0)) // bind to wildcard .remoteAddress(new InetSocketAddress(config.host, config.udpPort)) - .handler(new RegistrationRemoteHandlerClientUDP(threadName, - registrationWrapper)); + .handler(new RegistrationRemoteHandlerClientUDP(threadName, registrationWrapper, workerEventLoop)); // Enable to READ and WRITE MULTICAST data (ie, 192.168.1.0) // in order to WRITE: write as normal, just make sure it ends in .255 @@ -253,8 +224,8 @@ class Client extends EndPointClient implements Connection */ public void reconnect(final int connectionTimeout) throws IOException { - // close out all old connections - closeConnections(); + // make sure we are closed first + close(); connect(connectionTimeout); } @@ -283,7 +254,7 @@ class Client extends EndPointClient implements Connection * if the client is unable to connect in the requested time */ public - void connect(int connectionTimeout) throws IOException { + void connect(final int connectionTimeout) throws IOException { this.connectionTimeout = connectionTimeout; // make sure we are not trying to connect during a close or stop event. @@ -291,6 +262,25 @@ class Client extends EndPointClient implements Connection synchronized (shutdownInProgress) { } + // if we are in the SAME thread as netty -- start in a new thread (otherwise we will deadlock) + if (isNettyThread()) { + runNewThread("Restart Thread", new Runnable(){ + @Override + public + void run() { + try { + connect(connectionTimeout); + } catch (IOException e) { + e.printStackTrace(); + } + } + }); + + return; + } + + + if (isShutdown()) { throw new IOException("Unable to connect when shutdown..."); } @@ -300,14 +290,25 @@ class Client extends EndPointClient implements Connection } else { if (config.tcpPort > 0 && config.udpPort > 0) { - logger.info("Connecting to server: {} at TCP/UDP port: {}", hostName, config.tcpPort, config.udpPort); - } else { - logger.info("Connecting to server: {} at TCP port: {}", hostName, config.tcpPort); + logger.info("Connecting to TCP/UDP server [{}:{}]", hostName, config.tcpPort, config.udpPort); + } + else if (config.tcpPort > 0) { + logger.info("Connecting to TCP server [{}:{}]", hostName, config.tcpPort); + } + else { + logger.info("Connecting to UDP server [{}:{}]", hostName, config.udpPort); } } // have to start the registration process. This will wait until registration is complete and RMI methods are initialized + // if this is called in the event dispatch thread for netty, it will deadlock! startRegistration(); + + + if (config.tcpPort == 0 && config.udpPort > 0) { + // AFTER registration is complete, if we are UDP only -- setup a heartbeat (must be the larger of 2x the idle timeout OR 10 seconds) + startUdpHeartbeat(); + } } @Override @@ -471,14 +472,33 @@ class Client extends EndPointClient implements Connection } /** - * Closes all connections ONLY (keeps the client running). To STOP the client, use stop(). + * Closes all connections ONLY (keeps the client running), does not remove any listeners. To STOP the client, use stop(). *

* This is used, for example, when reconnecting to a server. */ @Override public void close() { - closeConnections(); + closeConnection(); + + // String threadName = Client.class.getSimpleName(); + // synchronized (bootstraps) { + // ArrayList newList = new ArrayList(bootstraps.size()); + // + // for (BootstrapWrapper bootstrap : bootstraps) { + // EventLoopGroup group = bootstrap.bootstrap.group(); + // + // removeFromShutdown(group); + // group.shutdownGracefully(); + // + // String name = threadName + "-" + bootstrap.type + "-BOSS"; + // + // newList.add(bootstrap.clone(newEventLoop(1, name))); + // } + // + // bootstraps.clear(); + // bootstraps.addAll(newList); + // } } } diff --git a/src/dorkbox/network/DnsClient.java b/src/dorkbox/network/DnsClient.java index c2717d0d..05f2b991 100644 --- a/src/dorkbox/network/DnsClient.java +++ b/src/dorkbox/network/DnsClient.java @@ -245,7 +245,7 @@ class DnsClient extends Shutdownable { channelType = OioDatagramChannel.class; } else if (OS.isLinux() && NativeLibrary.isAvailable()) { - // JNI network stack is MUCH faster (but only on linux) + // epoll network stack is MUCH faster (but only on linux) eventLoopGroup = new EpollEventLoopGroup(1, new NamedThreadFactory(THREAD_NAME + "-DNS", threadGroup)); channelType = EpollDatagramChannel.class; } @@ -661,8 +661,8 @@ class DnsClient extends Shutdownable { } } - String msg = "Could not ask question to DNS server for A/AAAA record: {}"; - logger.error(msg, hostname); + String msg = "Could not ask question to DNS server for A/AAAA record: " + hostname; + logger.error(msg); UnknownHostException cause = (UnknownHostException) resolve.cause(); if (cause != null) { diff --git a/src/dorkbox/network/DnsServer.java b/src/dorkbox/network/DnsServer.java index d47cd92e..924841bf 100644 --- a/src/dorkbox/network/DnsServer.java +++ b/src/dorkbox/network/DnsServer.java @@ -23,6 +23,7 @@ import dorkbox.network.connection.EndPoint; import dorkbox.network.connection.Shutdownable; import dorkbox.network.dns.DnsQuestion; import dorkbox.network.dns.Name; +import dorkbox.network.dns.constants.DnsClass; import dorkbox.network.dns.constants.DnsRecordType; import dorkbox.network.dns.records.ARecord; import dorkbox.network.dns.serverHandlers.DnsServerHandler; @@ -84,7 +85,10 @@ class DnsServer extends Shutdownable { void main(String[] args) { DnsServer server = new DnsServer("localhost", 2053); - // server.aRecord("google.com", DnsClass.IN, 10, "127.0.0.1"); + // MasterZone zone = new MasterZone(); + + + server.aRecord("google.com", DnsClass.IN, 10, "127.0.0.1"); // server.bind(false); server.bind(); @@ -123,32 +127,31 @@ class DnsServer extends Shutdownable { final EventLoopGroup boss; - final EventLoopGroup worker; + final EventLoopGroup work; if (OS.isAndroid()) { // android ONLY supports OIO (not NIO) - boss = new OioEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName + "-boss", threadGroup)); - worker = new OioEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup)); + boss = new OioEventLoopGroup(1, new NamedThreadFactory(threadName + "-boss", threadGroup)); + work = new OioEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup)); } else if (OS.isLinux() && NativeLibrary.isAvailable()) { - // JNI network stack is MUCH faster (but only on linux) - boss = new EpollEventLoopGroup(EndPoint.DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName + "-boss", threadGroup)); - worker = new EpollEventLoopGroup(EndPoint.DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup)); + // epoll network stack is MUCH faster (but only on linux) + boss = new EpollEventLoopGroup(1, new NamedThreadFactory(threadName + "-boss", threadGroup)); + work = new EpollEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup)); } else if (OS.isMacOsX() && NativeLibrary.isAvailable()) { // KQueue network stack is MUCH faster (but only on macosx) - boss = new KQueueEventLoopGroup(EndPoint.DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName + "-boss", threadGroup)); - worker = new KQueueEventLoopGroup(EndPoint.DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup)); + boss = new KQueueEventLoopGroup(1, new NamedThreadFactory(threadName + "-boss", threadGroup)); + work = new KQueueEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup)); } else { // sometimes the native libraries cannot be loaded, so fall back to NIO - boss = new NioEventLoopGroup(EndPoint.DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName + "-boss", threadGroup)); - worker = new NioEventLoopGroup(EndPoint.DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup)); + boss = new NioEventLoopGroup(1, new NamedThreadFactory(threadName + "-boss", threadGroup)); + work = new NioEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup)); } - manageForShutdown(boss); - manageForShutdown(worker); + manageForShutdown(work); tcpBootstrap = new ServerBootstrap(); @@ -160,7 +163,7 @@ class DnsServer extends Shutdownable { tcpBootstrap.channel(OioServerSocketChannel.class); } else if (OS.isLinux() && NativeLibrary.isAvailable()) { - // JNI network stack is MUCH faster (but only on linux) + // epoll network stack is MUCH faster (but only on linux) tcpBootstrap.channel(EpollServerSocketChannel.class); } else if (OS.isMacOsX() && NativeLibrary.isAvailable()) { @@ -174,7 +177,7 @@ class DnsServer extends Shutdownable { // TODO: If we use netty for an HTTP server, // Beside the usual ChannelOptions the Native Transport allows to enable TCP_CORK which may come in handy if you implement a HTTP Server. - tcpBootstrap.group(boss, worker) + tcpBootstrap.group(boss, work) .option(ChannelOption.SO_BACKLOG, backlogConnectionCount) .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) .childOption(ChannelOption.SO_KEEPALIVE, true) @@ -200,18 +203,18 @@ class DnsServer extends Shutdownable { udpBootstrap.channel(OioDatagramChannel.class); } else if (OS.isLinux() && NativeLibrary.isAvailable()) { - // JNI network stack is MUCH faster (but only on linux) + // epoll network stack is MUCH faster (but only on linux) udpBootstrap.channel(EpollDatagramChannel.class); } else if (OS.isMacOsX() && NativeLibrary.isAvailable()) { - // JNI network stack is MUCH faster (but only on macosx) + // KQueue network stack is MUCH faster (but only on macosx) udpBootstrap.channel(KQueueDatagramChannel.class); } else { udpBootstrap.channel(NioDatagramChannel.class); } - udpBootstrap.group(worker) + udpBootstrap.group(work) .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) .option(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(EndPoint.WRITE_BUFF_LOW, EndPoint.WRITE_BUFF_HIGH)) diff --git a/src/dorkbox/network/Server.java b/src/dorkbox/network/Server.java index 85052e76..93b26d73 100644 --- a/src/dorkbox/network/Server.java +++ b/src/dorkbox/network/Server.java @@ -15,6 +15,8 @@ */ package dorkbox.network; +import static dorkbox.network.pipeline.ConnectionType.LOCAL; + import java.io.IOException; import java.net.Socket; @@ -24,7 +26,6 @@ import dorkbox.network.connection.EndPointServer; import dorkbox.network.connection.registration.local.RegistrationLocalHandlerServer; import dorkbox.network.connection.registration.remote.RegistrationRemoteHandlerServerTCP; import dorkbox.network.connection.registration.remote.RegistrationRemoteHandlerServerUDP; -import dorkbox.util.NamedThreadFactory; import dorkbox.util.OS; import dorkbox.util.Property; import dorkbox.util.exceptions.SecurityException; @@ -33,20 +34,15 @@ import io.netty.bootstrap.SessionBootstrap; import io.netty.buffer.PooledByteBufAllocator; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelOption; -import io.netty.channel.DefaultEventLoopGroup; import io.netty.channel.EventLoopGroup; import io.netty.channel.FixedRecvByteBufAllocator; import io.netty.channel.WriteBufferWaterMark; import io.netty.channel.epoll.EpollDatagramChannel; -import io.netty.channel.epoll.EpollEventLoopGroup; import io.netty.channel.epoll.EpollServerSocketChannel; import io.netty.channel.kqueue.KQueueDatagramChannel; -import io.netty.channel.kqueue.KQueueEventLoopGroup; import io.netty.channel.kqueue.KQueueServerSocketChannel; import io.netty.channel.local.LocalAddress; import io.netty.channel.local.LocalServerChannel; -import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.oio.OioEventLoopGroup; import io.netty.channel.socket.nio.NioDatagramChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.channel.socket.oio.OioDatagramChannel; @@ -142,7 +138,8 @@ class Server extends EndPointServer { if (udpPort > 0) { // This is what allows us to have UDP behave "similar" to TCP, in that a session is established based on the port/ip of the // remote connection. This allows us to reuse channels and have "state" for a UDP connection that normally wouldn't exist. - udpBootstrap = new SessionBootstrap(); + // Additionally, this is what responds to discovery broadcast packets + udpBootstrap = new SessionBootstrap(tcpPort, udpPort); } else { udpBootstrap = null; @@ -150,64 +147,34 @@ class Server extends EndPointServer { String threadName = Server.class.getSimpleName(); + final EventLoopGroup workerEventLoop = newEventLoop(DEFAULT_THREAD_POOL_SIZE, threadName); - final EventLoopGroup boss; - final EventLoopGroup worker; - - if (OS.isAndroid()) { - // android ONLY supports OIO (not NIO) - boss = new OioEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName + "-boss", threadGroup)); - worker = new OioEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup)); - } - else if (OS.isLinux() && NativeLibrary.isAvailable()) { - // JNI network stack is MUCH faster (but only on linux) - boss = new EpollEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName + "-boss", threadGroup)); - worker = new EpollEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup)); - } - else if (OS.isMacOsX() && NativeLibrary.isAvailable()) { - // KQueue network stack is MUCH faster (but only on macosx) - boss = new KQueueEventLoopGroup(EndPoint.DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName + "-boss", threadGroup)); - worker = new KQueueEventLoopGroup(EndPoint.DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup)); - } - else { - boss = new NioEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName + "-boss", threadGroup)); - worker = new NioEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup)); - } - - manageForShutdown(boss); - manageForShutdown(worker); - // always use local channels on the server. - { - EventLoopGroup localBoss; - EventLoopGroup localWorker; - - if (localBootstrap != null) { - localBoss = new DefaultEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName + "-boss-LOCAL", - threadGroup)); - localWorker = new DefaultEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName + "-worker-LOCAL", - threadGroup)); - - localBootstrap.group(localBoss, localWorker) - .channel(LocalServerChannel.class) - .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) - .option(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(WRITE_BUFF_LOW, WRITE_BUFF_HIGH)) - .localAddress(new LocalAddress(localChannelName)) - .childHandler(new RegistrationLocalHandlerServer(threadName, registrationWrapper)); - - manageForShutdown(localBoss); - manageForShutdown(localWorker); - } + if (localBootstrap != null) { + localBootstrap.group(newEventLoop(LOCAL, 1, threadName + "-JVM-BOSS"), + newEventLoop(LOCAL, 1, threadName + "-JVM-HAND")) + .channel(LocalServerChannel.class) + .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) + .option(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(WRITE_BUFF_LOW, WRITE_BUFF_HIGH)) + .localAddress(new LocalAddress(localChannelName)) + .childHandler(new RegistrationLocalHandlerServer(threadName, registrationWrapper, workerEventLoop)); } + // don't even bother with TCP/UDP if it's not enabled + if (tcpBootstrap == null && udpBootstrap == null) { + return; + } + + + if (tcpBootstrap != null) { if (OS.isAndroid()) { // android ONLY supports OIO (not NIO) tcpBootstrap.channel(OioServerSocketChannel.class); } else if (OS.isLinux() && NativeLibrary.isAvailable()) { - // JNI network stack is MUCH faster (but only on linux) + // epoll network stack is MUCH faster (but only on linux) tcpBootstrap.channel(EpollServerSocketChannel.class); } else if (OS.isMacOsX() && NativeLibrary.isAvailable()) { @@ -221,15 +188,15 @@ class Server extends EndPointServer { // TODO: If we use netty for an HTTP server, // Beside the usual ChannelOptions the Native Transport allows to enable TCP_CORK which may come in handy if you implement a HTTP Server. - tcpBootstrap.group(boss, worker) + tcpBootstrap.group(newEventLoop(1, threadName + "-TCP-BOSS"), + newEventLoop(1, threadName + "-TCP-HAND")) .option(ChannelOption.SO_BACKLOG, backlogConnectionCount) .option(ChannelOption.SO_REUSEADDR, true) .option(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(WRITE_BUFF_LOW, WRITE_BUFF_HIGH)) .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) .childOption(ChannelOption.SO_KEEPALIVE, true) - .childHandler(new RegistrationRemoteHandlerServerTCP(threadName, - registrationWrapper)); + .childHandler(new RegistrationRemoteHandlerServerTCP(threadName, registrationWrapper, workerEventLoop)); // have to check options.host for "0.0.0.0". we don't bind to "0.0.0.0", we bind to "null" to get the "any" address! if (hostName.equals("0.0.0.0")) { @@ -252,7 +219,7 @@ class Server extends EndPointServer { udpBootstrap.channel(OioDatagramChannel.class); } else if (OS.isLinux() && NativeLibrary.isAvailable()) { - // JNI network stack is MUCH faster (but only on linux) + // epoll network stack is MUCH faster (but only on linux) udpBootstrap.channel(EpollDatagramChannel.class); } else if (OS.isMacOsX() && NativeLibrary.isAvailable()) { @@ -267,7 +234,9 @@ class Server extends EndPointServer { // Netty4 has a default of 2048 bytes as upper limit for datagram packets, we want this to be whatever we specify FixedRecvByteBufAllocator recvByteBufAllocator = new FixedRecvByteBufAllocator(EndPoint.udpMaxSize); - udpBootstrap.group(worker) + + udpBootstrap.group(newEventLoop(1, threadName + "-UDP-BOSS"), + newEventLoop(1, threadName + "-UDP-HAND")) .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) .option(ChannelOption.RCVBUF_ALLOCATOR, recvByteBufAllocator) .option(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(WRITE_BUFF_LOW, WRITE_BUFF_HIGH)) @@ -276,7 +245,7 @@ class Server extends EndPointServer { // TODO: move broadcast to it's own handler, and have UDP server be able to be bound to a specific IP // OF NOTE: At the end in my case I decided to bind to .255 broadcast address on Linux systems. (to receive broadcast packets) .localAddress(udpPort) // if you bind to a specific interface, Linux will be unable to receive broadcast packets! see: http://developerweb.net/viewtopic.php?id=5722 - .childHandler(new RegistrationRemoteHandlerServerUDP(threadName, registrationWrapper)); + .childHandler(new RegistrationRemoteHandlerServerUDP(threadName, registrationWrapper, workerEventLoop)); // // have to check options.host for null. we don't bind to 0.0.0.0, we bind to "null" to get the "any" address! // if (hostName.equals("0.0.0.0")) { @@ -344,7 +313,7 @@ class Server extends EndPointServer { throw new IllegalArgumentException("Could not bind to LOCAL address '" + localChannelName + "' on the server.", future.cause()); } - logger.info("Listening on LOCAL address: '{}'", localChannelName); + logger.info("Listening on LOCAL address: [{}]", localChannelName); manageForShutdown(future); } @@ -365,7 +334,7 @@ class Server extends EndPointServer { throw new IllegalArgumentException("Could not bind to address " + hostName + " TCP port " + tcpPort + " on the server.", future.cause()); } - logger.info("Listening on TCP at {}:{}", hostName, tcpPort); + logger.info("TCP server listen address [{}:{}]", hostName, tcpPort); manageForShutdown(future); } @@ -384,7 +353,7 @@ class Server extends EndPointServer { future.cause()); } - logger.info("Listening on UDP at {}:{}", hostName, udpPort); + logger.info("UDP server listen address [{}:{}]", hostName, udpPort); manageForShutdown(future); } diff --git a/src/dorkbox/network/connection/BootstrapWrapper.java b/src/dorkbox/network/connection/BootstrapWrapper.java index 6c538b8e..57c69ad6 100644 --- a/src/dorkbox/network/connection/BootstrapWrapper.java +++ b/src/dorkbox/network/connection/BootstrapWrapper.java @@ -16,6 +16,7 @@ package dorkbox.network.connection; import io.netty.bootstrap.Bootstrap; +import io.netty.channel.EventLoopGroup; public class BootstrapWrapper { @@ -32,6 +33,11 @@ class BootstrapWrapper { this.bootstrap = bootstrap; } + public + BootstrapWrapper clone(EventLoopGroup group) { + return new BootstrapWrapper(type, address, port, bootstrap.clone(group)); + } + @Override public String toString() { diff --git a/src/dorkbox/network/connection/Connection.java b/src/dorkbox/network/connection/Connection.java index 066de7fb..938d6be3 100644 --- a/src/dorkbox/network/connection/Connection.java +++ b/src/dorkbox/network/connection/Connection.java @@ -67,6 +67,12 @@ interface Connection { */ ConnectionBridge send(); + /** + * Safely sends objects to a destination (such as a custom object or a standard ping). This will automatically choose which protocol + * is available to use. If you want specify the protocol, use {@link #send()}, followed by the protocol you wish to use. + */ + ConnectionPoint send(Object message); + /** * Expose methods to send objects to a destination when the connection has become idle. */ @@ -83,7 +89,7 @@ interface Connection { Listeners listeners(); /** - * Closes the connection + * Closes the connection, but does not remove any listeners */ void close(); diff --git a/src/dorkbox/network/connection/ConnectionImpl.java b/src/dorkbox/network/connection/ConnectionImpl.java index 0f3412c6..df3f9418 100644 --- a/src/dorkbox/network/connection/ConnectionImpl.java +++ b/src/dorkbox/network/connection/ConnectionImpl.java @@ -23,12 +23,16 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import org.bouncycastle.crypto.params.ParametersWithIV; import org.slf4j.Logger; +import dorkbox.network.Client; +import dorkbox.network.connection.Listener.OnMessageReceived; import dorkbox.network.connection.bridge.ConnectionBridge; import dorkbox.network.connection.idle.IdleBridge; import dorkbox.network.connection.idle.IdleSender; @@ -54,6 +58,7 @@ import dorkbox.network.serialization.CryptoSerializationManager; import dorkbox.util.collections.LockFreeHashMap; import dorkbox.util.collections.LockFreeIntMap; import dorkbox.util.generics.ClassHelper; +import io.netty.bootstrap.DatagramCloseMessage; import io.netty.bootstrap.DatagramSessionChannel; import io.netty.channel.Channel; import io.netty.channel.ChannelHandler.Sharable; @@ -80,9 +85,9 @@ import io.netty.util.concurrent.Promise; @SuppressWarnings("unused") @Sharable public -class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConnection, Connection, Listeners, ConnectionBridge { +class ConnectionImpl extends ChannelInboundHandlerAdapter implements CryptoConnection, Connection, Listeners, ConnectionBridge { public static - boolean isTcp(Class channelClass) { + boolean isTcpChannel(Class channelClass) { return channelClass == OioSocketChannel.class || channelClass == NioSocketChannel.class || channelClass == KQueueSocketChannel.class || @@ -90,7 +95,7 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn } public static - boolean isUdp(Class channelClass) { + boolean isUdpChannel(Class channelClass) { return channelClass == OioDatagramChannel.class || channelClass == NioDatagramChannel.class || channelClass == KQueueDatagramChannel.class || @@ -99,7 +104,7 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn } public static - boolean isLocal(Class channelClass) { + boolean isLocalChannel(Class channelClass) { return channelClass == LocalChannel.class; } @@ -111,15 +116,13 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn private final Object writeLock = new Object(); private final AtomicBoolean closeInProgress = new AtomicBoolean(false); - private final AtomicBoolean alreadyClosed = new AtomicBoolean(false); - private final Object closeInProgressLock = new Object(); + private final AtomicBoolean channelIsClosed = new AtomicBoolean(false); private final Object messageInProgressLock = new Object(); private final AtomicBoolean messageInProgress = new AtomicBoolean(false); private ISessionManager sessionManager; private ChannelWrapper channelWrapper; - private boolean isLoopback; private volatile PingFuture pingFuture = null; @@ -140,6 +143,11 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn // counter, which is also transmitted as an optimized int. (which is why it starts at 0, so the transmitted bytes are small) private final AtomicLong aes_gcm_iv = new AtomicLong(0); + + // when closing this connection, HOW MANY endpoints need to be closed? + private CountDownLatch closeLatch; + + // // RMI fields // @@ -150,6 +158,7 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn private final LockFreeIntMap rmiRegistrationCallbacks; private volatile int rmiCallbackId = 0; + /** * All of the parameters can be null, when metaChannel wants to get the base class type */ @@ -175,8 +184,6 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn /** * Initialize the connection with any extra info that is needed but was unavailable at the channel construction. - *

- * This happens BEFORE prep. */ final void init(final ChannelWrapper channelWrapper, final ISessionManager sessionManager) { @@ -186,23 +193,33 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn //noinspection SimplifiableIfStatement if (this.channelWrapper instanceof ChannelNetworkWrapper) { this.remoteKeyChanged = ((ChannelNetworkWrapper) this.channelWrapper).remoteKeyChanged(); + + int count = 0; + if (channelWrapper.tcp() != null) { + count++; + } + + if (channelWrapper.udp() != null) { + count++; + + // we received a hint to close this channel from the remote end. + add(new OnMessageReceived() { + @Override + public + void received(final Connection connection, final DatagramCloseMessage message) { + connection.close(); + } + }); + } + + // when closing this connection, HOW MANY endpoints need to be closed? + closeLatch = new CountDownLatch(count); } else { this.remoteKeyChanged = false; - } - isLoopback = channelWrapper.isLoopback(); - } - - /** - * Prepare the channel wrapper, since it doesn't have access to certain fields during it's initialization. - *

- * This happens AFTER init. - */ - final - void prep() { - if (this.channelWrapper != null) { - this.channelWrapper.init(); + // when closing this connection, HOW MANY endpoints need to be closed? + closeLatch = new CountDownLatch(1); } } @@ -256,7 +273,7 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn @Override public boolean isLoopback() { - return isLoopback; + return channelWrapper.isLoopback(); } /** @@ -330,20 +347,20 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn /** * INTERNAL USE ONLY. Used to initiate a ping, and to return a ping. - * Sends a ping message attempted in the following order: UDP, TCP + * + * Sends a ping message attempted in the following order: UDP, TCP,LOCAL */ public final void ping0(PingMessage ping) { if (this.channelWrapper.udp() != null) { - UDP(ping); + UDP(ping).flush(); } else if (this.channelWrapper.tcp() != null) { - TCP(ping); + TCP(ping).flush(); } else { self(ping); } - flush(); } /** @@ -417,14 +434,43 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn return this; } + /** + * Safely sends objects to a destination (such as a custom object or a standard ping). This will automatically choose which protocol + * is available to use. If you want specify the protocol, use {@link #send()}, followed by the protocol you wish to use. + * + * By default, this will try in the following order: + * - TCP (if available) + * - UDP (if available) + * - LOCAL + */ + @Override + public final + ConnectionPoint send(final Object message) { + if (this.channelWrapper.tcp() != null) { + return TCP(message); + } + else if (this.channelWrapper.udp() != null) { + return UDP(message); + } + else { + self(message); + + // we have to return something, otherwise dependent code will throw a null pointer exception + return ChannelNull.get(); + } + } + /** * Sends the object to other listeners INSIDE this endpoint. It does not send it to a remote address. */ @Override public final - void self(Object message) { + ConnectionPoint self(Object message) { logger.trace("Sending LOCAL {}", message); this.sessionManager.onMessage(this, message); + + // THIS IS REALLY A LOCAL CONNECTION! + return this.channelWrapper.tcp(); } /** @@ -432,7 +478,7 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn */ @Override public final - ConnectionPoint TCP(Object message) { + ConnectionPoint TCP(final Object message) { if (!closeInProgress.get()) { logger.trace("Sending TCP {}", message); @@ -440,7 +486,7 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn try { tcp.write(message); } catch (Exception e) { - logger.error("Unable to write TCP object {}", message.getClass()); + logger.error("Unable to write TCP object {}", message.getClass(), e); } return tcp; } @@ -465,7 +511,7 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn try { udp.write(message); } catch (Exception e) { - logger.error("Unable to write TCP object {}", message.getClass()); + logger.error("Unable to write UDP object {}", message.getClass(), e); } return udp; } @@ -479,8 +525,7 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn /** * Flushes the contents of the TCP/UDP/etc pipes to the actual transport. */ - @Override - public final + final void flush() { this.channelWrapper.flush(); } @@ -532,7 +577,7 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn ReferenceCountUtil.release(message); } - public + private void channelRead(Object object) { // prevent close from occurring SMACK in the middle of a message in progress. // delay close until it's finished. @@ -573,8 +618,9 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn Channel channel = context.channel(); Class channelClass = channel.getClass(); - boolean isTCP = isTcp(channelClass); - boolean isLocal = isLocal(channelClass); + boolean isTCP = isTcpChannel(channelClass); + boolean isUDP = false; + boolean isLocal = isLocalChannel(channelClass); if (this.logger.isInfoEnabled()) { String type; @@ -582,55 +628,115 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn if (isTCP) { type = "TCP"; } - else if (isUdp(channelClass)) { - type = "UDP"; - } - else if (isLocal) { - type = "LOCAL"; - } else { - type = "UNKNOWN"; + isUDP = isUdpChannel(channelClass); + if (isUDP) { + type = "UDP"; + } + else if (isLocal) { + type = "LOCAL"; + } + else { + type = "UNKNOWN"; + } } - this.logger.info("Closed remote {} connection: {}", + this.logger.info("Closed remote {} connection [{}]", type, - channel.remoteAddress() - .toString()); + EndPoint.getHostDetails(channel.remoteAddress())); } + // TODO: tell the remote endpoint that it needs to close (via a message, which might get there...). + + if (this.endPoint instanceof EndPointClient) { ((EndPointClient) this.endPoint).abortRegistration(); } - // our master channels are TCP/LOCAL (which are mutually exclusive). Only key disconnect events based on the status of them. - if (isTCP || isLocal) { - // this is because channelInactive can ONLY happen when netty shuts down the channel. - // and connection.close() can be called by the user. - // will auto-flush if necessary - this.sessionManager.onDisconnected(this); - // close TCP/UDP together! - close(); + /* + * Only close if we are: + * - local (mutually exclusive to TCP/UDP) + * - TCP (and TCP+UDP) + * - UDP (and not part of TCP+UDP) + * + * DO NOT call close if we are: + * - UDP (part of TCP+UDP) + */ + if (isLocal || + isTCP || + (isUDP && this.channelWrapper.tcp() == null)) { + + // we can get to this point in two ways. We only want this to happen once + // - remote endpoint disconnects (and so closes us) + // - local endpoint calls close(), and netty will call this. + + + // this must happen first, because client.close() depends on it! + // onDisconnected() must happen last. + boolean doClose = channelIsClosed.compareAndSet(false, true); + + if (!closeInProgress.get()) { + if (endPoint instanceof EndPointClient) { + // client closes single connection + ((Client) endPoint).close(); + } else { + // server only closes this connection. + close(); + } + } + + if (doClose) { + // this is because channelInactive can ONLY happen when netty shuts down the channel. + // and connection.close() can be called by the user. + // will auto-flush if necessary + this.sessionManager.onDisconnected(this); + } } - synchronized (this.closeInProgressLock) { - this.alreadyClosed.set(true); - this.closeInProgressLock.notify(); - } + closeLatch.countDown(); + + // UDP connections ALWAYS have to shutdown their event loop (because of how session management works) + // if (isUDP || this.endPoint instanceof EndPointClient) { + // // also have to shutdown this eventloop, but ONLY for the client! + // channel.eventLoop() + // .shutdownGracefully(); + // } } /** - * Closes the connection + * Closes the connection, but does not remove any listeners */ @Override public final void close() { - close(false); + close(true); } + /** + * we can get to this point in two ways. We only want this to happen once + * - remote endpoint disconnects (and so netty calls us) + * - local endpoint calls close() directly + * + * NOTE: If we remove all listeners and we are the client, then we remove ALL logic from the client! + */ final void close(final boolean keepListeners) { + // if we are in the same thread as netty, run in a new thread to prevent deadlocks with messageInProgress + if (!this.closeInProgress.get() && this.messageInProgress.get() && Shutdownable.isNettyThread()) { + Shutdownable.runNewThread("Close connection Thread", new Runnable() { + @Override + public + void run() { + close(keepListeners); + } + }); + + return; + } + + // only close if we aren't already in the middle of closing. if (this.closeInProgress.compareAndSet(false, true)) { int idleTimeoutMs = this.endPoint.getIdleTimeout(); @@ -641,7 +747,8 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn // if we are in the middle of a message, hold off. synchronized (this.messageInProgressLock) { - if (this.messageInProgress.get()) { + // while loop is to prevent spurious wakeups! + while (this.messageInProgress.get()) { try { this.messageInProgressLock.wait(idleTimeoutMs); } catch (InterruptedException ignored) { @@ -652,8 +759,6 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn // flush any pending messages this.channelWrapper.flush(); - this.channelWrapper.close(this, this.sessionManager); - // close out the ping future PingFuture pingFuture2 = this.pingFuture; if (pingFuture2 != null) { @@ -661,18 +766,22 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn } this.pingFuture = null; - // want to wait for the "channelInactive" method to FINISH before allowing our current thread to continue! - synchronized (this.closeInProgressLock) { - if (!this.alreadyClosed.get()) { + + + synchronized (this.channelIsClosed) { + if (!this.channelIsClosed.get()) { + // this will have netty call "channelInactive()" + this.channelWrapper.close(this, this.sessionManager); + + // want to wait for the "channelInactive()" method to FINISH ALL TYPES before allowing our current thread to continue! try { - this.closeInProgressLock.wait(idleTimeoutMs); - } catch (Exception ignored) { + closeLatch.await(idleTimeoutMs, TimeUnit.MILLISECONDS); + } catch (InterruptedException ignored) { } } } - // remove all listeners, but ONLY if we are the server. If we remove all listeners and we are the client, then we remove - // ALL logic from the client! The server is OK because the server listeners per connection are dynamically added + // remove all listeners AFTER we close the channel. if (!keepListeners) { removeAll(); } @@ -688,7 +797,6 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn } } - /** * Marks the connection to be closed as soon as possible. This is evaluated when the current * thread execution returns to the network stack. @@ -958,8 +1066,7 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn // have to wait for the object to be created + ID to be assigned on the remote system BEFORE we can create the proxy instance here. // this means we are creating a NEW object on the server, bound access to only this connection - TCP(message); - flush(); + send(message).flush(); } @Override @@ -985,8 +1092,7 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn // have to wait for the object to be created + ID to be assigned on the remote system BEFORE we can create the proxy instance here. // this means we are creating a NEW object on the server, bound access to only this connection - TCP(message); - flush(); + send(message).flush(); } diff --git a/src/dorkbox/network/connection/ConnectionManager.java b/src/dorkbox/network/connection/ConnectionManager.java index e7a19cc5..36121cd3 100644 --- a/src/dorkbox/network/connection/ConnectionManager.java +++ b/src/dorkbox/network/connection/ConnectionManager.java @@ -15,6 +15,7 @@ */ package dorkbox.network.connection; +import java.util.LinkedList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; @@ -201,6 +202,12 @@ class ConnectionManager implements Listeners, ISessionMana throw new IllegalArgumentException("listener cannot be null."); } + if (logger.isTraceEnabled()) { + logger.trace("listener removed: {}", + listener.getClass() + .getName()); + } + boolean found = false; int remainingListeners = 0; @@ -237,12 +244,6 @@ class ConnectionManager implements Listeners, ISessionMana if (remainingListeners == 0) { hasAtLeastOneListener.set(false); } - - if (logger.isTraceEnabled()) { - logger.trace("listener removed: {}", - listener.getClass() - .getName()); - } } else { logger.error("No matching listener types. Unable to remove listener: {}", @@ -261,12 +262,12 @@ class ConnectionManager implements Listeners, ISessionMana @Override public final Listeners removeAll() { - onMessageReceivedManager.removeAll(); + onConnectedManager.clear(); + onDisconnectedManager.clear(); + onIdleManager.clear(); + onMessageReceivedManager.clear(); - Logger logger2 = this.logger; - if (logger2.isTraceEnabled()) { - logger2.trace("ALL listeners removed !!"); - } + logger.trace("ALL listeners removed !!"); return this; } @@ -311,6 +312,7 @@ class ConnectionManager implements Listeners, ISessionMana @Override public final void onMessage(final ConnectionImpl connection, final Object message) { + logger.trace("onMessage({}, {})", connection.id(), message.getClass()); notifyOnMessage0(connection, message, false); } @@ -320,6 +322,9 @@ class ConnectionManager implements Listeners, ISessionMana if (connection.manageRmi(message)) { // if we are an RMI message/registration, we have very specific, defined behavior. We do not use the "normal" listener callback pattern // because these methods are rare, and require special functionality + + // make sure we flush the message to the socket! + connection.flush(); return true; } @@ -372,13 +377,16 @@ class ConnectionManager implements Listeners, ISessionMana } } + /** * Invoked when a Channel is open, bound to a local address, and connected to a remote address. */ @Override public void onConnected(final ConnectionImpl connection) { - addConnection(connection); + logger.trace("onConnected({})", connection.id()); + + // we add the connection in a different step! boolean foundListener = onConnectedManager.notifyConnected((C) connection, shutdown); @@ -401,7 +409,9 @@ class ConnectionManager implements Listeners, ISessionMana @Override public void onDisconnected(final ConnectionImpl connection) { - boolean foundListener = onDisconnectedManager.notifyDisconnected((C) connection, shutdown); + logger.trace("onDisconnected({})", connection.id()); + + boolean foundListener = onDisconnectedManager.notifyDisconnected((C) connection); if (foundListener) { connection.flush(); @@ -422,6 +432,25 @@ class ConnectionManager implements Listeners, ISessionMana removeConnection(connection); } + /** + * Invoked when a Channel is open, bound to a local address, and connected to a remote address. + */ + @Override + public + void addConnection(ConnectionImpl connection) { + logger.trace("addConnection({})", connection.id()); + + addConnection0(connection); + + // now have to account for additional (local) listener managers. + // access a snapshot of the managers (single-writer-principle) + final IdentityMap localManagers = localManagersREF.get(this); + ConnectionManager localManager = localManagers.get(connection); + if (localManager != null) { + localManager.addConnection(connection); + } + } + /** * Adds a custom connection to the server. *

@@ -430,7 +459,7 @@ class ConnectionManager implements Listeners, ISessionMana * * @param connection the connection to add */ - void addConnection(final Connection connection) { + void addConnection0(final Connection connection) { // synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this // section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our // use-case 99% of the time) @@ -457,7 +486,6 @@ class ConnectionManager implements Listeners, ISessionMana * * @param connection the connection to remove */ - public void removeConnection(Connection connection) { // synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this // section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our @@ -599,6 +627,8 @@ class ConnectionManager implements Listeners, ISessionMana */ final void closeConnections(boolean keepListeners) { + LinkedList closeConnections = new LinkedList(); + // synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this // section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our // use-case 99% of the time) @@ -609,19 +639,19 @@ class ConnectionManager implements Listeners, ISessionMana // Close the connection. Make sure the close operation ends because // all I/O operations are asynchronous in Netty. // Also necessary otherwise workers won't close. - - - if (keepListeners && connection instanceof ConnectionImpl) { - ((ConnectionImpl) connection).close(true); - } - else { - connection.close(); + if (connection instanceof ConnectionImpl) { + closeConnections.add((ConnectionImpl) connection); } } this.connectionEntries.clear(); this.connectionsHead = null; } + + // must be outside of the synchronize, otherwise we can potentially deadlock + for (ConnectionImpl connection : closeConnections) { + connection.close(keepListeners); + } } /** @@ -704,7 +734,7 @@ class ConnectionManager implements Listeners, ISessionMana */ @Override public - void self(final Object message) { + ConnectionPoint self(final Object message) { ConcurrentEntry current = connectionsREF.get(this); ConnectionImpl c; while (current != null) { @@ -713,6 +743,7 @@ class ConnectionManager implements Listeners, ISessionMana onMessage(c, message); } + return this; } /** @@ -751,6 +782,44 @@ class ConnectionManager implements Listeners, ISessionMana return this; } + /** + * Safely sends objects to a destination (such as a custom object or a standard ping). This will automatically choose which protocol + * is available to use. If you want specify the protocol, use {@link ConnectionManager#TCP(Object)}, etc. + *

+ * By default, this will try in the following order: + * - TCP (if available) + * - UDP (if available) + * - LOCAL + */ + protected + ConnectionPoint send(final Object message) { + ConcurrentEntry current = connectionsREF.get(this); + Connection c; + while (current != null) { + c = current.getValue(); + current = current.next(); + + c.send(message); + } + return this; + } + + /** + * Flushes the contents of the TCP/UDP/etc pipes to the actual transport socket. + */ + @Override + public + void flush() { + ConcurrentEntry current = connectionsREF.get(this); + ConnectionImpl c; + while (current != null) { + c = current.getValue(); + current = current.next(); + + c.flush(); + } + } + @Override public boolean equals(final Object o) { diff --git a/src/dorkbox/network/connection/ConnectionPoint.java b/src/dorkbox/network/connection/ConnectionPoint.java index a4d81c3c..c328c7fc 100644 --- a/src/dorkbox/network/connection/ConnectionPoint.java +++ b/src/dorkbox/network/connection/ConnectionPoint.java @@ -30,6 +30,11 @@ interface ConnectionPoint { */ void write(Object object) throws Exception; + /** + * Flushes the contents of the TCP/UDP/etc pipes to the actual transport socket. + */ + void flush(); + /** * Creates a new promise associated with this connection type */ diff --git a/src/dorkbox/network/connection/ICryptoConnection.java b/src/dorkbox/network/connection/CryptoConnection.java similarity index 96% rename from src/dorkbox/network/connection/ICryptoConnection.java rename to src/dorkbox/network/connection/CryptoConnection.java index f84732c7..b8277007 100644 --- a/src/dorkbox/network/connection/ICryptoConnection.java +++ b/src/dorkbox/network/connection/CryptoConnection.java @@ -21,7 +21,7 @@ import org.bouncycastle.crypto.params.ParametersWithIV; * Supporting methods for encrypting data to a remote endpoint */ public -interface ICryptoConnection extends IRmiConnection { +interface CryptoConnection extends RmiConnection, Connection { /** * This is the per-message sequence number. diff --git a/src/dorkbox/network/connection/EndPoint.java b/src/dorkbox/network/connection/EndPoint.java index e9cad507..49ba8075 100644 --- a/src/dorkbox/network/connection/EndPoint.java +++ b/src/dorkbox/network/connection/EndPoint.java @@ -15,7 +15,9 @@ */ package dorkbox.network.connection; +import java.net.InetAddress; import java.net.InetSocketAddress; +import java.net.SocketAddress; import java.security.SecureRandom; import java.util.List; import java.util.concurrent.Executor; @@ -44,6 +46,7 @@ import dorkbox.util.Property; import dorkbox.util.crypto.CryptoECC; import dorkbox.util.entropy.Entropy; import dorkbox.util.exceptions.SecurityException; +import io.netty.channel.local.LocalAddress; import io.netty.util.NetUtil; /** @@ -63,11 +66,45 @@ class EndPoint extends Shutdownable { // TODO: maybe some sort of STUN-like connection keep-alive?? + public static + String getHostDetails(final SocketAddress socketAddress) { + StringBuilder builder = new StringBuilder(); + getHostDetails(builder, socketAddress); + return builder.toString(); + } + + public static + void getHostDetails(StringBuilder stringBuilder, final SocketAddress socketAddress) { + if (socketAddress instanceof InetSocketAddress) { + InetSocketAddress address = (InetSocketAddress) socketAddress; + + InetAddress address1 = address.getAddress(); + + String hostName = address1.getHostName(); + String hostAddress = address1.getHostAddress(); + + if (!hostName.equals(hostAddress)) { + stringBuilder.append(hostName) + .append('/') + .append(hostAddress); + } + else { + stringBuilder.append(hostAddress); + } + + stringBuilder.append(':') + .append(address.getPort()); + } + else if (socketAddress instanceof LocalAddress) { + stringBuilder.append(socketAddress.toString()); + } + } + /** * Defines if we are allowed to use the native OS-specific network interface (non-native to java) for boosted networking performance. */ @Property - public static boolean enableNativeLibrary = true; + public static boolean enableNativeLibrary = false; public static final String LOCAL_CHANNEL = "local_channel"; @@ -122,7 +159,8 @@ class EndPoint extends Shutdownable { */ private volatile int idleTimeoutMs = 0; - private AtomicBoolean isConnected = new AtomicBoolean(false); + // the connection status of this endpoint. Once a server has connected to ANY client, it will always return true until server.close() is called + protected final AtomicBoolean isConnected = new AtomicBoolean(false); /** @@ -244,7 +282,7 @@ class EndPoint extends Shutdownable { */ public void disableRemoteKeyValidation() { - if (isConnected()) { + if (isConnected.get()) { logger.error("Cannot disable the remote key validation after this endpoint is connected!"); } else { @@ -301,16 +339,6 @@ class EndPoint extends Shutdownable { this.idleTimeoutMs = idleTimeoutMs; } - /** - * Return the connection status of this endpoint. - *

- * Once a server has connected to ANY client, it will always return true until server.close() is called - */ - public final - boolean isConnected() { - return isConnected.get(); - } - /** * Returns the serialization wrapper if there is an object type that needs to be added outside of the basics. */ @@ -342,7 +370,7 @@ class EndPoint extends Shutdownable { * @param remoteAddress be NULL (when getting the baseClass or when creating a local channel) */ protected final - Connection connection0(final MetaChannel metaChannel, final InetSocketAddress remoteAddress) { + ConnectionImpl connection0(final MetaChannel metaChannel, final InetSocketAddress remoteAddress) { ConnectionImpl connection; RmiBridge rmiBridge = null; @@ -357,7 +385,6 @@ class EndPoint extends Shutdownable { ChannelWrapper wrapper; connection = newConnection(logger, this, rmiBridge); - metaChannel.connection = connection; if (metaChannel.localChannel != null) { if (rmiEnabled) { @@ -378,6 +405,9 @@ class EndPoint extends Shutdownable { // now initialize the connection channels with whatever extra info they might need. connection.init(wrapper, connectionManager); + + isConnected.set(true); + connectionManager.addConnection(connection); } else { // getting the connection baseClass @@ -393,14 +423,9 @@ class EndPoint extends Shutdownable { * Internal call by the pipeline to notify the "Connection" object that it has "connected", meaning that modifications * to the pipeline are finished. *

- * Only the CLIENT injects in front of this) + * Only the CLIENT injects in front of this */ void connectionConnected0(ConnectionImpl connection) { - isConnected.set(true); - - // prep the channel wrapper - connection.prep(); - connectionManager.onConnected(connection); } @@ -426,6 +451,13 @@ class EndPoint extends Shutdownable { public abstract ConnectionBridgeBase send(); + /** + * Safely sends objects to a destination (such as a custom object or a standard ping). This will automatically choose which protocol + * is available to use. If you want specify the protocol, use {@link #send()}, followed by the protocol you wish to use. + */ + public abstract + ConnectionPoint send(final Object message); + /** * Closes all connections ONLY (keeps the server/client running). To STOP the client/server, use stop(). *

@@ -434,17 +466,7 @@ class EndPoint extends Shutdownable { * The server should ALWAYS use STOP. */ void closeConnections(boolean shouldKeepListeners) { - // give a chance to other threads. - Thread.yield(); - // stop does the same as this + more. Only keep the listeners for connections IF we are the client. If we remove listeners as a client, - // ALL of the client logic will be lost. The server is reactive, so listeners are added to connections as needed (instead of before startup) - connectionManager.closeConnections(shouldKeepListeners); - - // Sometimes there might be "lingering" connections (ie, halfway though registration) that need to be closed. - registrationWrapper.clearSessions(); - - isConnected.set(false); } /** @@ -462,8 +484,6 @@ class EndPoint extends Shutdownable { @Override protected void shutdownChannelsPre() { - closeConnections(false); - // this does a closeConnections + clear_listeners connectionManager.stop(); } diff --git a/src/dorkbox/network/connection/EndPointClient.java b/src/dorkbox/network/connection/EndPointClient.java index a55f3ca1..65614cbe 100644 --- a/src/dorkbox/network/connection/EndPointClient.java +++ b/src/dorkbox/network/connection/EndPointClient.java @@ -55,21 +55,23 @@ class EndPointClient extends EndPoint { super(Client.class, config); } + /** + * Internal call by the pipeline to start the client registering the different session protocols. + */ protected void startRegistration() throws IOException { synchronized (bootstrapLock) { // always reset everything. registration = new CountDownLatch(1); - bootstrapIterator = bootstraps.iterator(); + + doRegistration(); } - doRegistration(); - - // have to BLOCK - // don't want the client to run before registration is complete + // have to BLOCK (must be outside of the synchronize call), we don't want the client to run before registration is complete try { if (!registration.await(connectionTimeout, TimeUnit.MILLISECONDS)) { + closeConnection(); throw new IOException("Unable to complete registration within '" + connectionTimeout + "' milliseconds"); } } catch (InterruptedException e) { @@ -77,63 +79,6 @@ class EndPointClient extends EndPoint { } } - // this is called by 2 threads. The startup thread, and the registration-in-progress thread - private void doRegistration() { - synchronized (bootstrapLock) { - BootstrapWrapper bootstrapWrapper = bootstrapIterator.next(); - - ChannelFuture future; - - if (connectionTimeout != 0) { - // must be before connect - bootstrapWrapper.bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectionTimeout); - } - - try { - // UDP : When this is CONNECT, a udp socket will ONLY accept UDP traffic from the remote address (ip/port combo). - // If the reply isn't from the correct port, then the other end will receive a "Port Unreachable" exception. - - future = bootstrapWrapper.bootstrap.connect(); - future.await(connectionTimeout); - } catch (Exception e) { - String errorMessage = "Could not connect to the " + bootstrapWrapper.type + " server at " + bootstrapWrapper.address + " on port: " + bootstrapWrapper.port; - if (logger.isDebugEnabled()) { - // extra info if debug is enabled - logger.error(errorMessage, e); - } - else { - logger.error(errorMessage); - } - - return; - } - - if (!future.isSuccess()) { - Throwable cause = future.cause(); - // extra space here is so it aligns with "Connecting to server:" - String errorMessage = "Connection refused :" + bootstrapWrapper.address + " at " + bootstrapWrapper.type + " port: " + bootstrapWrapper.port; - - if (cause instanceof java.net.ConnectException) { - if (cause.getMessage() - .contains("refused")) { - logger.error(errorMessage); - } - - } else { - logger.error(errorMessage, cause); - } - - return; - } - - logger.trace("Waiting for registration from server."); - - manageForShutdown(future); - } - } - - - /** * Internal call by the pipeline to notify the client to continue registering the different session protocols. * @@ -164,6 +109,69 @@ class EndPointClient extends EndPoint { } } + /** + * this is called by 2 threads. The startup thread, and the registration-in-progress thread + * + * NOTE: must be inside synchronize(bootstrapLock)! + */ + private + void doRegistration() { + BootstrapWrapper bootstrapWrapper = bootstrapIterator.next(); + + ChannelFuture future; + + if (connectionTimeout != 0) { + // must be before connect + bootstrapWrapper.bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectionTimeout); + } + + try { + // UDP : When this is CONNECT, a udp socket will ONLY accept UDP traffic from the remote address (ip/port combo). + // If the reply isn't from the correct port, then the other end will receive a "Port Unreachable" exception. + + future = bootstrapWrapper.bootstrap.connect(); + future.await(connectionTimeout); + } catch (Exception e) { + String errorMessage = + "Could not connect to the " + bootstrapWrapper.type + " server at " + bootstrapWrapper.address + " on port: " + + bootstrapWrapper.port; + if (logger.isDebugEnabled()) { + // extra info if debug is enabled + logger.error(errorMessage, e); + } + else { + logger.error(errorMessage); + } + + return; + } + + if (!future.isSuccess()) { + Throwable cause = future.cause(); + + // extra space here is so it aligns with "Connecting to server:" + String errorMessage = "Connection refused :" + bootstrapWrapper.address + " at " + bootstrapWrapper.type + " port: " + + bootstrapWrapper.port; + + if (cause instanceof java.net.ConnectException) { + if (cause.getMessage() + .contains("refused")) { + logger.error(errorMessage); + } + + } + else { + logger.error(errorMessage, cause); + } + + return; + } + + logger.trace("Waiting for registration from server."); + + manageForShutdown(future); + } + /** * Internal (to the networking stack) to notify the client that registration has COMPLETED. This is necessary because the client * will BLOCK until it has successfully registered it's connections. @@ -174,16 +182,18 @@ class EndPointClient extends EndPoint { connectionBridgeFlushAlways = new ConnectionBridge() { @Override public - void self(Object message) { - connection.self(message); - flush(); + ConnectionPoint self(Object message) { + ConnectionPoint self = connection.self(message); + connection.flush(); + + return self; } @Override public ConnectionPoint TCP(Object message) { ConnectionPoint tcp = connection.TCP(message); - flush(); + connection.flush(); // needed to place back-pressure when writing too much data to the connection. Will create deadlocks if called from // INSIDE the event loop @@ -196,7 +206,7 @@ class EndPointClient extends EndPoint { public ConnectionPoint UDP(Object message) { ConnectionPoint udp = connection.UDP(message); - flush(); + connection.flush(); // needed to place back-pressure when writing too much data to the connection. Will create deadlocks if called from // INSIDE the event loop @@ -208,25 +218,15 @@ class EndPointClient extends EndPoint { public Ping ping() { Ping ping = connection.ping(); - flush(); - return ping; - } - - @Override - public - void flush() { connection.flush(); + return ping; } }; //noinspection unchecked this.connection = connection; - synchronized (bootstrapLock) { - // we're done with registration, so no need to keep this around - bootstrapIterator = null; - registration.countDown(); - } + stopRegistration(); // invokes the listener.connection() method, and initialize the connection channels with whatever extra info they might need. // This will also start the RMI (if necessary) initialization/creation of objects @@ -240,10 +240,23 @@ class EndPointClient extends EndPoint { synchronized (bootstrapLock) { // we're done with registration, so no need to keep this around bootstrapIterator = null; - registration.countDown(); + while (registration.getCount() > 0) { + registration.countDown(); + } } } + /** + * AFTER registration is complete, if we are UDP only -- setup a heartbeat (must be the larger of 2x the idle timeout OR 10 seconds) + * + * If the server disconnects because of a heartbeat failure, the client has to be made aware of this when it tries to send data again + * (and it must go through it's entire reconnect protocol) + */ + protected + void startUdpHeartbeat() { + + } + /** * Expose methods to send objects to a destination. *

@@ -256,23 +269,48 @@ class EndPointClient extends EndPoint { return connectionBridgeFlushAlways; } + @Override + public + ConnectionPoint send(final Object message) { + ConnectionPoint send = connection.send(message); + send.flush(); + + // needed to place back-pressure when writing too much data to the connection. Will create deadlocks if called from + // INSIDE the event loop + ((ConnectionImpl)connection).controlBackPressure(send); + return send; + } + /** * Closes all connections ONLY (keeps the client running). To STOP the client, use stop(). *

* This is used, for example, when reconnecting to a server. */ - public - void closeConnections() { - // Only keep the listeners for connections IF we are the client. If we remove listeners as a client, - // ALL of the client logic will be lost. The server is reactive, so listeners are added to connections as needed (instead of before startup) - closeConnections(true); - // for the CLIENT only, we clear these connections! (the server only clears them on shutdown) - shutdownChannels(); + protected + void closeConnection() { + if (isConnected.get()) { + // make sure we're not waiting on registration + stopRegistration(); - connection = null; + // for the CLIENT only, we clear these connections! (the server only clears them on shutdown) - // make sure we're not waiting on registration - stopRegistration(); + // stop does the same as this + more. Only keep the listeners for connections IF we are the client. If we remove listeners as a client, + // ALL of the client logic will be lost. The server is reactive, so listeners are added to connections as needed (instead of before startup) + connectionManager.closeConnections(true); + + // Sometimes there might be "lingering" connections (ie, halfway though registration) that need to be closed. + registrationWrapper.clearSessions(); + + + closeConnections(true); + shutdownAllChannels(); + // shutdownEventLoops(); + + + + connection = null; + isConnected.set(false); + } } /** diff --git a/src/dorkbox/network/connection/EndPointServer.java b/src/dorkbox/network/connection/EndPointServer.java index fd0b3b7a..369514fe 100644 --- a/src/dorkbox/network/connection/EndPointServer.java +++ b/src/dorkbox/network/connection/EndPointServer.java @@ -40,6 +40,17 @@ class EndPointServer extends EndPoint { return this.connectionManager; } + /** + * Safely sends objects to a destination (such as a custom object or a standard ping). This will automatically choose which protocol + * is available to use. If you want specify the protocol, use {@link #send()}, followed by the protocol you wish to use. + */ + @Override + public + ConnectionPoint send(final Object message) { + return this.connectionManager.send(message); + } + + /** * When called by a server, NORMALLY listeners are added at the GLOBAL level (meaning, I add one listener, * and ALL connections are notified of that listener. @@ -78,7 +89,7 @@ class EndPointServer extends EndPoint { */ public void add(Connection connection) { - connectionManager.addConnection(connection); + connectionManager.addConnection0(connection); } /** diff --git a/src/dorkbox/network/connection/ISessionManager.java b/src/dorkbox/network/connection/ISessionManager.java index a36ebb30..1a7f5514 100644 --- a/src/dorkbox/network/connection/ISessionManager.java +++ b/src/dorkbox/network/connection/ISessionManager.java @@ -33,6 +33,11 @@ interface ISessionManager { */ void onIdle(ConnectionImpl connection); + /** + * Invoked when a Channel is open, bound to a local address, and connected to a remote address. + */ + void addConnection(ConnectionImpl connection); + /** * Invoked when a Channel is open, bound to a local address, and connected to a remote address. *

diff --git a/src/dorkbox/network/connection/KryoExtra.java b/src/dorkbox/network/connection/KryoExtra.java index 9217e3c0..aeb7dc3a 100644 --- a/src/dorkbox/network/connection/KryoExtra.java +++ b/src/dorkbox/network/connection/KryoExtra.java @@ -25,7 +25,6 @@ import com.esotericsoftware.kryo.Kryo; import dorkbox.network.pipeline.ByteBufInput; import dorkbox.network.pipeline.ByteBufOutput; -import dorkbox.network.pipeline.MagicBytes; import dorkbox.network.serialization.CryptoSerializationManager; import dorkbox.util.bytes.BigEndian; import dorkbox.util.bytes.OptimizeUtilsByteArray; @@ -40,7 +39,7 @@ import net.jpountz.lz4.LZ4FastDecompressor; * Nothing in this class is thread safe */ public -class KryoExtra extends Kryo { +class KryoExtra extends Kryo { // snappycomp : 7.534 micros/op; 518.5 MB/s (output: 55.1%) // snappyuncomp : 1.391 micros/op; 2808.1 MB/s // lz4comp : 6.210 micros/op; 629.0 MB/s (output: 55.4%) @@ -52,7 +51,7 @@ class KryoExtra extends Kryo { private final ByteBufOutput writer = new ByteBufOutput(); // volatile to provide object visibility for entire class - public volatile IRmiConnection connection; + public volatile RmiConnection connection; private final GCMBlockCipher aesEngine = new GCMBlockCipher(new AESFastEngine()); @@ -94,9 +93,6 @@ class KryoExtra extends Kryo { // connection will always be NULL during connection initialization this.connection = null; - // during INIT and handshake, we don't use connection encryption/compression - buffer.writeByte(0); - // write the object to the NORMAL output buffer! writer.setBuffer(buffer); @@ -114,15 +110,15 @@ class KryoExtra extends Kryo { ByteBuf inputBuf = buffer; - // read off the magic byte - final byte magicByte = buffer.readByte(); - // read the object from the buffer. reader.setBuffer(inputBuf); return readClassAndObject(reader); // this properly sets the readerIndex, but only if it's the correct buffer } + /** + * This is NOT ENCRYPTED (and is only done on the loopback connection!) + */ public synchronized void writeCompressed(final C connection, final ByteBuf buffer, final Object message) throws IOException { // required by RMI and some serializers to determine which connection wrote (or has info about) this object @@ -180,10 +176,10 @@ class KryoExtra extends Kryo { byte[] compressOutput = this.compressOutput; - int maxLengthLengthOffset = 5; + int maxLengthLengthOffset = 4; // length is never negative, so 4 is OK (5 means it's negative) int maxCompressedLength = compressor.maxCompressedLength(length); - // add 5 so there is room to write the compressed size to the buffer + // add 4 so there is room to write the compressed size to the buffer int maxCompressedLengthWithOffset = maxCompressedLength + maxLengthLengthOffset; // lazy initialize the compression output buffer @@ -194,7 +190,7 @@ class KryoExtra extends Kryo { } - // LZ4 compress. output offset max 5 bytes to leave room for length of tempOutput data + // LZ4 compress. output offset max 4 bytes to leave room for length of tempOutput data int compressedLength = compressor.compress(inputArray, inputOffset, length, compressOutput, maxLengthLengthOffset, maxCompressedLength); // bytes can now be written to, because our compressed data is stored in a temp array. @@ -209,13 +205,13 @@ class KryoExtra extends Kryo { // now write the ORIGINAL (uncompressed) length to the front of the byte array (this is NOT THE BUFFER!). This is so we can use the FAST decompress version OptimizeUtilsByteArray.writeInt(inputArray, length, true, inputOffset); - // write out the "magic" byte. - buffer.writeByte(MagicBytes.crypto); - // have to copy over the orig data, because we used the temp buffer. Also have to account for the length of the uncompressed size buffer.writeBytes(inputArray, inputOffset, compressedLength + lengthLength); } + /** + * This is NOT ENCRYPTED (and is only done on the loopback connection!) + */ public Object readCompressed(final C connection, final ByteBuf buffer, int length) throws IOException { // required by RMI and some serializers to determine which connection wrote (or has info about) this object @@ -228,15 +224,12 @@ class KryoExtra extends Kryo { ByteBuf inputBuf = buffer; - // read off the magic byte - final byte magicByte = buffer.readByte(); - // get the decompressed length (at the beginning of the array) final int uncompressedLength = OptimizeUtilsByteBuf.readInt(buffer, true); final int lengthLength = OptimizeUtilsByteArray.intLength(uncompressedLength, true); // because 1-5 bytes for the decompressed size - // have to adjust for the magic byte and uncompressed length - length = length - 1 - lengthLength; + // have to adjust for uncompressed length + length = length - lengthLength; ///////// decompress data -- as it's ALWAYS compressed @@ -363,10 +356,10 @@ class KryoExtra extends Kryo { byte[] compressOutput = this.compressOutput; - int maxLengthLengthOffset = 5; + int maxLengthLengthOffset = 4; // length is never negative, so 4 is OK (5 means it's negative) int maxCompressedLength = compressor.maxCompressedLength(length); - // add 5 so there is room to write the compressed size to the buffer + // add 4 so there is room to write the compressed size to the buffer int maxCompressedLengthWithOffset = maxCompressedLength + maxLengthLengthOffset; // lazy initialize the compression output buffer @@ -378,7 +371,7 @@ class KryoExtra extends Kryo { - // LZ4 compress. output offset max 5 bytes to leave room for length of tempOutput data + // LZ4 compress. output offset max 4 bytes to leave room for length of tempOutput data int compressedLength = compressor.compress(inputArray, inputOffset, length, compressOutput, maxLengthLengthOffset, maxCompressedLength); // bytes can now be written to, because our compressed data is stored in a temp array. @@ -394,7 +387,7 @@ class KryoExtra extends Kryo { OptimizeUtilsByteArray.writeInt(inputArray, length, true, inputOffset); // correct length for encryption - length = compressedLength + lengthLength; // +1 to +5 for the uncompressed size bytes + length = compressedLength + lengthLength; // +1 to +4 for the uncompressed size bytes @@ -432,9 +425,6 @@ class KryoExtra extends Kryo { throw new IOException("Unable to AES encrypt the data", e); } - // write out the "magic" byte. - buffer.writeByte(MagicBytes.crypto); - // write out our GCM counter OptimizeUtilsByteBuf.writeLong(buffer, nextGcmSequence, true); @@ -454,15 +444,11 @@ class KryoExtra extends Kryo { ByteBuf inputBuf = buffer; - // read off the magic byte - final byte magicByte = buffer.readByte(); - final long gcmIVCounter = OptimizeUtilsByteBuf.readLong(buffer, true); - // compression can ONLY happen if it's ALSO crypto'd - // have to adjust for the magic byte and the gcmIVCounter - length = length - 1 - OptimizeUtilsByteArray.longLength(gcmIVCounter, true); + // have to adjust for the gcmIVCounter + length = length - OptimizeUtilsByteArray.longLength(gcmIVCounter, true); /////////// decrypting data @@ -543,7 +529,7 @@ class KryoExtra extends Kryo { // get the decompressed length (at the beginning of the array) inputArray = decryptOutputArray; final int uncompressedLength = OptimizeUtilsByteArray.readInt(inputArray, true); - inputOffset = OptimizeUtilsByteArray.intLength(uncompressedLength, true); // because 1-5 bytes for the decompressed size + inputOffset = OptimizeUtilsByteArray.intLength(uncompressedLength, true); // because 1-4 bytes for the decompressed size byte[] decompressOutputArray = this.decompressOutput; diff --git a/src/dorkbox/network/connection/RegistrationWrapper.java b/src/dorkbox/network/connection/RegistrationWrapper.java index a6e66ceb..4569e580 100644 --- a/src/dorkbox/network/connection/RegistrationWrapper.java +++ b/src/dorkbox/network/connection/RegistrationWrapper.java @@ -112,12 +112,11 @@ class RegistrationWrapper { } /** - * Internal call by the pipeline to notify the "Connection" object that it has "connected", meaning that modifications to the pipeline - * are finished. + * Internal call by the pipeline to notify the "Connection" object that it has "connected". */ public - void connectionConnected0(ConnectionImpl networkConnection) { - this.endPoint.connectionConnected0(networkConnection); + void connectionConnected0(ConnectionImpl connection) { + this.endPoint.connectionConnected0(connection); } /** @@ -126,7 +125,7 @@ class RegistrationWrapper { * @param metaChannel can be NULL (when getting the baseClass) */ public - Connection connection0(MetaChannel metaChannel, final InetSocketAddress remoteAddress) { + ConnectionImpl connection0(MetaChannel metaChannel, final InetSocketAddress remoteAddress) { return this.endPoint.connection0(metaChannel, remoteAddress); } diff --git a/src/dorkbox/network/connection/IRmiConnection.java b/src/dorkbox/network/connection/RmiConnection.java similarity index 98% rename from src/dorkbox/network/connection/IRmiConnection.java rename to src/dorkbox/network/connection/RmiConnection.java index 5496dabf..893a10a9 100644 --- a/src/dorkbox/network/connection/IRmiConnection.java +++ b/src/dorkbox/network/connection/RmiConnection.java @@ -21,7 +21,7 @@ import dorkbox.network.rmi.RemoteObject; * Supporting methods for RMI connections */ public -interface IRmiConnection { +interface RmiConnection { /** * Used by RMI for the LOCAL side, to get the proxy object as an interface diff --git a/src/dorkbox/network/connection/Shutdownable.java b/src/dorkbox/network/connection/Shutdownable.java index a309f6cb..a436145d 100644 --- a/src/dorkbox/network/connection/Shutdownable.java +++ b/src/dorkbox/network/connection/Shutdownable.java @@ -1,5 +1,10 @@ package dorkbox.network.connection; +import static dorkbox.network.pipeline.ConnectionType.EPOLL; +import static dorkbox.network.pipeline.ConnectionType.KQUEUE; +import static dorkbox.network.pipeline.ConnectionType.NIO; +import static dorkbox.network.pipeline.ConnectionType.OIO; + import java.security.AccessControlException; import java.util.ArrayList; import java.util.LinkedList; @@ -10,11 +15,19 @@ import java.util.concurrent.atomic.AtomicBoolean; import org.slf4j.Logger; +import dorkbox.network.NativeLibrary; +import dorkbox.network.pipeline.ConnectionType; +import dorkbox.util.NamedThreadFactory; import dorkbox.util.OS; import dorkbox.util.Property; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; +import io.netty.channel.DefaultEventLoopGroup; import io.netty.channel.EventLoopGroup; +import io.netty.channel.epoll.EpollEventLoopGroup; +import io.netty.channel.kqueue.KQueueEventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.oio.OioEventLoopGroup; import io.netty.util.concurrent.EventExecutor; import io.netty.util.concurrent.Future; import io.netty.util.internal.PlatformDependent; @@ -71,6 +84,31 @@ class Shutdownable { @Property public static long maxShutdownWaitTimeInMilliSeconds = 2000L; // in milliseconds + /** + * Checks to see if we are running in the netty thread. This is (usually) to prevent potential deadlocks in code that CANNOT be run from + * inside a netty worker. + */ + public static + boolean isNettyThread() { + return Thread.currentThread() + .getThreadGroup() + .getName() + .contains(THREADGROUP_NAME); + } + + /** + * Runs a runnable inside a NEW thread that is NOT in the same thread group as Netty + */ + public static + void runNewThread(final String threadName, final Runnable runnable) { + Thread thread = new Thread(Thread.currentThread() + .getThreadGroup() + .getParent(), + runnable); + thread.setDaemon(true); + thread.setName(threadName); + thread.start(); + } protected final org.slf4j.Logger logger; @@ -145,8 +183,18 @@ class Shutdownable { } } + /** + * Remove an eventloop group to be tracked & managed for shutdown + */ + protected final + void removeFromShutdown(EventLoopGroup loopGroup) { + synchronized (eventLoopGroups) { + eventLoopGroups.remove(loopGroup); + } + } + // server only does this on stop. Client does this on closeConnections - void shutdownChannels() { + void shutdownAllChannels() { synchronized (shutdownChannelList) { // now we stop all of our channels. For the server, this will close the server manager for UDP sessions for (ChannelFuture f : shutdownChannelList) { @@ -163,6 +211,31 @@ class Shutdownable { } } + // shutdown all event loops associated + void shutdownEventLoops() { + // we want to WAIT until after the event executors have completed shutting down. + List> shutdownThreadList = new LinkedList>(); + + List loopGroups; + synchronized (eventLoopGroups) { + loopGroups = new ArrayList(eventLoopGroups.size()); + loopGroups.addAll(eventLoopGroups); + } + + for (EventLoopGroup loopGroup : loopGroups) { + shutdownThreadList.add(loopGroup.shutdownGracefully(maxShutdownWaitTimeInMilliSeconds, + maxShutdownWaitTimeInMilliSeconds * 10, + TimeUnit.MILLISECONDS)); + Thread.yield(); + } + + // now wait for them to finish! + // It can take a few seconds to shut down the executor. This will affect unit testing, where connections are quickly created/stopped + for (Future f : shutdownThreadList) { + f.syncUninterruptibly(); + Thread.yield(); + } + } protected final String stopWithErrorMessage(Logger logger, String errorMessage, Throwable throwable) { @@ -189,6 +262,72 @@ class Shutdownable { } + /** + * Creates a new event loop based on the OS type and specified configuration + * + * @param threadCount number of threads for the event loop + * + * @return a new event loop group based on the specified parameters + */ + protected + EventLoopGroup newEventLoop(final int threadCount, final String threadName) { + if (OS.isAndroid()) { + // android ONLY supports OIO + return newEventLoop(OIO, threadCount, threadName); + } + else if (OS.isLinux() && NativeLibrary.isAvailable()) { + // epoll network stack is MUCH faster (but only on linux) + return newEventLoop(EPOLL, threadCount, threadName); + } + else if (OS.isMacOsX() && NativeLibrary.isAvailable()) { + // KQueue network stack is MUCH faster (but only on macosx) + return newEventLoop(KQUEUE, threadCount, threadName); + } + else { + return newEventLoop(NIO, threadCount, threadName); + } + } + + /** + * Creates a new event loop based on the specified configuration + * + * @param connectionType LOCAL, NIO, EPOLL, etc... + * @param threadCount number of threads for the event loop + * + * @return a new event loop group based on the specified parameters + */ + protected + EventLoopGroup newEventLoop(final ConnectionType connectionType, final int threadCount, final String threadName) { + NamedThreadFactory threadFactory = new NamedThreadFactory(threadName, threadGroup); + + EventLoopGroup group; + + switch (connectionType) { + case LOCAL: + group = new DefaultEventLoopGroup(threadCount, threadFactory); + break; + case OIO: + group = new OioEventLoopGroup(threadCount, threadFactory); + break; + case NIO: + group = new NioEventLoopGroup(threadCount, threadFactory); + break; + case EPOLL: + group = new EpollEventLoopGroup(threadCount, threadFactory); + break; + case KQUEUE: + group = new KQueueEventLoopGroup(threadCount, threadFactory); + break; + + default: + group = new DefaultEventLoopGroup(threadCount, threadFactory); + break; + } + + manageForShutdown(group); + return group; + } + /** * Check to see if the current thread is running from it's OWN thread, or from Netty... This is used to prevent deadlocks. * @@ -276,32 +415,9 @@ class Shutdownable { // This will wait until we have finished starting up/shutting down. synchronized (shutdownInProgress) { shutdownChannelsPre(); - shutdownChannels(); - - // we want to WAIT until after the event executors have completed shutting down. - List> shutdownThreadList = new LinkedList>(); - - List loopGroups; - synchronized (eventLoopGroups) { - loopGroups = new ArrayList(eventLoopGroups.size()); - loopGroups.addAll(eventLoopGroups); - } - - for (EventLoopGroup loopGroup : loopGroups) { - shutdownThreadList.add(loopGroup.shutdownGracefully(maxShutdownWaitTimeInMilliSeconds, - maxShutdownWaitTimeInMilliSeconds * 4, - TimeUnit.MILLISECONDS)); - Thread.yield(); - } - - // now wait for them to finish! - // It can take a few seconds to shut down the executor. This will affect unit testing, where connections are quickly created/stopped - for (Future f : shutdownThreadList) { - f.syncUninterruptibly(); - Thread.yield(); - } - + shutdownAllChannels(); + shutdownEventLoops(); logger.info("Stopping endpoint."); diff --git a/src/dorkbox/network/connection/bridge/ConnectionBridge.java b/src/dorkbox/network/connection/bridge/ConnectionBridge.java index 4f86384b..ac4c0dd2 100644 --- a/src/dorkbox/network/connection/bridge/ConnectionBridge.java +++ b/src/dorkbox/network/connection/bridge/ConnectionBridge.java @@ -25,9 +25,4 @@ interface ConnectionBridge extends ConnectionBridgeBase { * @return Ping can have a listener attached, which will get called when the ping returns. */ Ping ping(); - - /** - * Flushes the contents of the TCP/UDP/etc pipes to the actual transport. - */ - void flush(); } diff --git a/src/dorkbox/network/connection/bridge/ConnectionBridgeBase.java b/src/dorkbox/network/connection/bridge/ConnectionBridgeBase.java index 45b6567a..1301f9dd 100644 --- a/src/dorkbox/network/connection/bridge/ConnectionBridgeBase.java +++ b/src/dorkbox/network/connection/bridge/ConnectionBridgeBase.java @@ -22,7 +22,7 @@ interface ConnectionBridgeBase { /** * Sends the message to other listeners INSIDE this endpoint. It does not send it to a remote address. */ - void self(Object message); + ConnectionPoint self(Object message); /** * Sends the message over the network using TCP. (or via LOCAL when it's a local channel). diff --git a/src/dorkbox/network/connection/registration/ConnectionWrapper.java b/src/dorkbox/network/connection/registration/ConnectionWrapper.java new file mode 100644 index 00000000..b9e6ce46 --- /dev/null +++ b/src/dorkbox/network/connection/registration/ConnectionWrapper.java @@ -0,0 +1,188 @@ +/* + * Copyright 2018 dorkbox, llc. + * + * 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 dorkbox.network.connection.registration; + +import org.bouncycastle.crypto.params.ParametersWithIV; + +import dorkbox.network.connection.ConnectionImpl; +import dorkbox.network.connection.ConnectionPoint; +import dorkbox.network.connection.CryptoConnection; +import dorkbox.network.connection.EndPoint; +import dorkbox.network.connection.Listeners; +import dorkbox.network.connection.bridge.ConnectionBridge; +import dorkbox.network.connection.idle.IdleBridge; +import dorkbox.network.connection.idle.IdleSender; +import dorkbox.network.rmi.RemoteObject; +import dorkbox.network.rmi.RemoteObjectCallback; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; + +/** + * A wrapper for the period of time between registration and connect for a "connection" session. + * + * This is to prevent race conditions where onMessage() can happen BEFORE a "connection" is "connected" + */ +public +class ConnectionWrapper implements CryptoConnection, ChannelHandler { + public final ConnectionImpl connection; + + public + ConnectionWrapper(final ConnectionImpl connection) { + this.connection = connection; + } + + @Override + public + void handlerAdded(final ChannelHandlerContext ctx) throws Exception { + } + + @Override + public + void handlerRemoved(final ChannelHandlerContext ctx) throws Exception { + } + + @Override + public + void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cause) throws Exception { + } + + + @Override + public + long getNextGcmSequence() { + return connection.getNextGcmSequence(); + } + + @Override + public + ParametersWithIV getCryptoParameters() { + return connection.getCryptoParameters(); + } + + @Override + public + RemoteObject getProxyObject(final int objectID, final Class iFace) { + throw new IllegalArgumentException("not implemented"); + } + + @Override + public + Object getImplementationObject(final int objectID) { + throw new IllegalArgumentException("not implemented"); + } + + @Override + public + int getRegisteredId(final T object) { + return 0; + } + + @Override + public + boolean hasRemoteKeyChanged() { + throw new IllegalArgumentException("not implemented"); + } + + @Override + public + String getRemoteHost() { + throw new IllegalArgumentException("not implemented"); + } + + @Override + public + boolean isLoopback() { + return connection.isLoopback(); + } + + @Override + public + EndPoint getEndPoint() { + throw new IllegalArgumentException("not implemented"); + } + + @Override + public + int id() { + throw new IllegalArgumentException("not implemented"); + } + + @Override + public + String idAsHex() { + throw new IllegalArgumentException("not implemented"); + } + + @Override + public + boolean hasUDP() { + throw new IllegalArgumentException("not implemented"); + } + + @Override + public + ConnectionBridge send() { + throw new IllegalArgumentException("not implemented"); + } + + @Override + public + ConnectionPoint send(final Object message) { + throw new IllegalArgumentException("not implemented"); + } + + @Override + public + IdleBridge sendOnIdle(final IdleSender sender) { + throw new IllegalArgumentException("not implemented"); + } + + @Override + public + IdleBridge sendOnIdle(final Object message) { + throw new IllegalArgumentException("not implemented"); + } + + @Override + public + Listeners listeners() { + throw new IllegalArgumentException("not implemented"); + } + + @Override + public + void close() { + throw new IllegalArgumentException("not implemented"); + } + + @Override + public + void closeAsap() { + throw new IllegalArgumentException("not implemented"); + } + + @Override + public + void createRemoteObject(final Class interfaceClass, final RemoteObjectCallback callback) { + throw new IllegalArgumentException("not implemented"); + } + + @Override + public + void getRemoteObject(final int objectId, final RemoteObjectCallback callback) { + throw new IllegalArgumentException("not implemented"); + } +} diff --git a/src/dorkbox/network/connection/registration/MetaChannel.java b/src/dorkbox/network/connection/registration/MetaChannel.java index 80cdd7f4..9adc1844 100644 --- a/src/dorkbox/network/connection/registration/MetaChannel.java +++ b/src/dorkbox/network/connection/registration/MetaChannel.java @@ -20,12 +20,11 @@ import java.util.concurrent.atomic.AtomicLong; import org.bouncycastle.crypto.AsymmetricCipherKeyPair; import org.bouncycastle.crypto.params.ECPublicKeyParameters; -import dorkbox.network.connection.ConnectionImpl; import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; public class MetaChannel { - // @formatter:off // how long between receiving data over TCP. This is used to determine how long to wait before notifying the APP, // so the registration message has time to arrive to the other endpoint. @@ -39,7 +38,7 @@ class MetaChannel { public Channel tcpChannel = null; public Channel udpChannel = null; - public ConnectionImpl connection; // only needed until the connection has been notified. + public ChannelHandler connection; // only needed until the connection has been notified. public ECPublicKeyParameters publicKey; // used for ECC crypto + handshake on NETWORK (remote) connections. This is the remote public key. public AsymmetricCipherKeyPair ecdhKey; // used for ECC Diffie-Hellman-Merkle key exchanges: see http://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange @@ -53,60 +52,11 @@ class MetaChannel { // If the server detects this, it has the option for additional security (two-factor auth, perhaps?) public boolean changedRemoteKey = false; - // @formatter:on - public MetaChannel(final int sessionId) { this.sessionId = sessionId; } - public - void close() { - if (this.localChannel != null) { - this.localChannel.close(); - } - - if (this.tcpChannel != null) { - this.tcpChannel.close(); - } - - if (this.udpChannel != null) { - // if (this.udpRemoteAddress == null) { - // FIXME: ?? only the CLIENT will have this. - this.udpChannel.close(); - // } - // else if (this.handlerServerUDP != null) { - // only the SERVER will have this - // we DO NOT want to close the UDP channel, otherwise no other UDP clients can connect - // this.handlerServerUDP.unRegisterServerUDP(this.udpRemoteAddress); - // } - } - } - - public - void close(final long maxShutdownWaitTimeInMilliSeconds) { - if (this.localChannel != null && this.localChannel.isOpen()) { - this.localChannel.close(); - } - - if (this.tcpChannel != null && this.tcpChannel.isOpen()) { - this.tcpChannel.close() - .awaitUninterruptibly(maxShutdownWaitTimeInMilliSeconds); - } - - if (this.udpChannel != null && this.udpChannel.isOpen()) { - // if (this.udpRemoteAddress == null) { - // FIXME: ?? only the CLIENT will have this. - this.udpChannel.close() - .awaitUninterruptibly(maxShutdownWaitTimeInMilliSeconds); - // } - // else { - // only the SERVER will have this - // we DO NOT want to close the UDP channel, otherwise no other UDP clients can connect - // this.handlerServerUDP.unRegisterServerUDP(this.udpRemoteAddress); - // } - } - } /** * Update the network round trip time. diff --git a/src/dorkbox/network/connection/registration/Registration.java b/src/dorkbox/network/connection/registration/Registration.java index 68babda8..600c494f 100644 --- a/src/dorkbox/network/connection/registration/Registration.java +++ b/src/dorkbox/network/connection/registration/Registration.java @@ -35,6 +35,12 @@ class Registration { // true if we have more registrations to process, false if we are done public boolean hasMore; + // true when we are ready to setup the connection (hasMore will always be false if this is true). False when we are ready to connect + public boolean upgrade; + + // true when we are fully upgraded + public boolean upgraded; + private Registration() { // for serialization diff --git a/src/dorkbox/network/connection/registration/RegistrationHandler.java b/src/dorkbox/network/connection/registration/RegistrationHandler.java index 238cee71..6ca500aa 100644 --- a/src/dorkbox/network/connection/registration/RegistrationHandler.java +++ b/src/dorkbox/network/connection/registration/RegistrationHandler.java @@ -20,19 +20,22 @@ import io.netty.channel.Channel; import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.EventLoopGroup; @Sharable public abstract class RegistrationHandler extends ChannelInboundHandlerAdapter { - protected static final String CONNECTION_HANDLER = "connectionHandler"; + protected static final String CONNECTION_HANDLER = "connection"; protected final RegistrationWrapper registrationWrapper; protected final org.slf4j.Logger logger; protected final String name; + protected final EventLoopGroup workerEventLoop; public - RegistrationHandler(final String name, RegistrationWrapper registrationWrapper) { + RegistrationHandler(final String name, RegistrationWrapper registrationWrapper, final EventLoopGroup workerEventLoop) { this.name = name; + this.workerEventLoop = workerEventLoop; this.logger = org.slf4j.LoggerFactory.getLogger(this.name); this.registrationWrapper = registrationWrapper; } @@ -74,7 +77,6 @@ class RegistrationHandler extends ChannelInboundHandlerAdapter { @Override public void channelReadComplete(final ChannelHandlerContext context) throws Exception { - context.flush(); } @Override diff --git a/src/dorkbox/network/connection/registration/local/RegistrationLocalHandler.java b/src/dorkbox/network/connection/registration/local/RegistrationLocalHandler.java index c4dad9af..c4059eed 100644 --- a/src/dorkbox/network/connection/registration/local/RegistrationLocalHandler.java +++ b/src/dorkbox/network/connection/registration/local/RegistrationLocalHandler.java @@ -20,14 +20,15 @@ import dorkbox.network.connection.registration.MetaChannel; import dorkbox.network.connection.registration.RegistrationHandler; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.EventLoopGroup; import io.netty.util.AttributeKey; public abstract class RegistrationLocalHandler extends RegistrationHandler { public static final AttributeKey META_CHANNEL = AttributeKey.valueOf(RegistrationLocalHandler.class, "MetaChannel.local"); - RegistrationLocalHandler(String name, RegistrationWrapper registrationWrapper) { - super(name, registrationWrapper); + RegistrationLocalHandler(String name, RegistrationWrapper registrationWrapper, final EventLoopGroup workerEventLoop) { + super(name, registrationWrapper, workerEventLoop); } /** @@ -45,6 +46,12 @@ class RegistrationLocalHandler extends RegistrationHandler { logger.trace("New LOCAL connection."); } + @Override + public + void channelActive(final ChannelHandlerContext context) throws Exception { + // to suppress warnings in the super class + } + @Override public void exceptionCaught(ChannelHandlerContext context, Throwable cause) throws Exception { @@ -59,12 +66,6 @@ class RegistrationLocalHandler extends RegistrationHandler { } } - @Override - public - void channelActive(ChannelHandlerContext context) throws Exception { - // not used (so we prevent the warnings from the super class) - } - // this SHOULDN'T ever happen, but we might shutdown in the middle of registration @Override public final diff --git a/src/dorkbox/network/connection/registration/local/RegistrationLocalHandlerClient.java b/src/dorkbox/network/connection/registration/local/RegistrationLocalHandlerClient.java index ee29c9f8..a77b140c 100644 --- a/src/dorkbox/network/connection/registration/local/RegistrationLocalHandlerClient.java +++ b/src/dorkbox/network/connection/registration/local/RegistrationLocalHandlerClient.java @@ -22,14 +22,15 @@ import dorkbox.network.connection.registration.Registration; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; import io.netty.util.ReferenceCountUtil; public class RegistrationLocalHandlerClient extends RegistrationLocalHandler { public - RegistrationLocalHandlerClient(String name, RegistrationWrapper registrationWrapper) { - super(name, registrationWrapper); + RegistrationLocalHandlerClient(String name, RegistrationWrapper registrationWrapper, final EventLoopGroup workerEventLoop) { + super(name, registrationWrapper, workerEventLoop); } /** @@ -67,13 +68,10 @@ class RegistrationLocalHandlerClient extends RegistrationLocalHandler { // Event though a local channel is XOR with everything else, we still have to make the client clean up it's state. registrationWrapper.startNextProtocolRegistration(); - registrationWrapper.connection0(metaChannel, null); - ConnectionImpl connection = metaChannel.connection; + ConnectionImpl connection = registrationWrapper.connection0(metaChannel, null); // have to setup connection handler pipeline.addLast(CONNECTION_HANDLER, connection); - - registrationWrapper.connectionConnected0(connection); } else { diff --git a/src/dorkbox/network/connection/registration/local/RegistrationLocalHandlerServer.java b/src/dorkbox/network/connection/registration/local/RegistrationLocalHandlerServer.java index 7cd75131..866bc013 100644 --- a/src/dorkbox/network/connection/registration/local/RegistrationLocalHandlerServer.java +++ b/src/dorkbox/network/connection/registration/local/RegistrationLocalHandlerServer.java @@ -21,14 +21,15 @@ import dorkbox.network.connection.registration.MetaChannel; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; import io.netty.util.ReferenceCountUtil; public class RegistrationLocalHandlerServer extends RegistrationLocalHandler { public - RegistrationLocalHandlerServer(String name, RegistrationWrapper registrationWrapper) { - super(name, registrationWrapper); + RegistrationLocalHandlerServer(String name, RegistrationWrapper registrationWrapper, final EventLoopGroup workerEventLoop) { + super(name, registrationWrapper, workerEventLoop); } /** @@ -65,13 +66,11 @@ class RegistrationLocalHandlerServer extends RegistrationLocalHandler { MetaChannel metaChannel = channel.attr(META_CHANNEL) .getAndSet(null); if (metaChannel != null) { - registrationWrapper.connection0(metaChannel, null); - ConnectionImpl connection = metaChannel.connection; + ConnectionImpl connection = registrationWrapper.connection0(metaChannel, null); if (connection != null) { // have to setup connection handler pipeline.addLast(CONNECTION_HANDLER, connection); - registrationWrapper.connectionConnected0(connection); } } diff --git a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandler.java b/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandler.java index 681f1ae4..fff65660 100644 --- a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandler.java +++ b/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandler.java @@ -22,7 +22,9 @@ import org.bouncycastle.jce.ECNamedCurveTable; import org.bouncycastle.jce.spec.ECParameterSpec; import dorkbox.network.connection.ConnectionImpl; +import dorkbox.network.connection.EndPoint; import dorkbox.network.connection.RegistrationWrapper; +import dorkbox.network.connection.registration.ConnectionWrapper; import dorkbox.network.connection.registration.MetaChannel; import dorkbox.network.connection.registration.Registration; import dorkbox.network.connection.registration.RegistrationHandler; @@ -31,11 +33,15 @@ import dorkbox.network.pipeline.tcp.KryoDecoderCrypto; import dorkbox.network.serialization.CryptoSerializationManager; import dorkbox.util.crypto.CryptoECC; import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; import io.netty.handler.timeout.IdleState; import io.netty.handler.timeout.IdleStateEvent; import io.netty.handler.timeout.IdleStateHandler; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; public abstract class RegistrationRemoteHandler extends RegistrationHandler { @@ -59,8 +65,8 @@ class RegistrationRemoteHandler extends RegistrationHandler { protected final CryptoSerializationManager serializationManager; - RegistrationRemoteHandler(final String name, final RegistrationWrapper registrationWrapper) { - super(name, registrationWrapper); + RegistrationRemoteHandler(final String name, final RegistrationWrapper registrationWrapper, final EventLoopGroup workerEventLoop) { + super(name, registrationWrapper, workerEventLoop); this.serializationManager = registrationWrapper.getSerializtion(); } @@ -76,8 +82,8 @@ class RegistrationRemoteHandler extends RegistrationHandler { Class channelClass = channel.getClass(); // because of the way TCP works, we have to have special readers/writers. For UDP, all data must be in a single packet. - boolean isTcpChannel = ConnectionImpl.isTcp(channelClass); - boolean isUdpChannel = !isTcpChannel && ConnectionImpl.isUdp(channelClass); + boolean isTcpChannel = ConnectionImpl.isTcpChannel(channelClass); + boolean isUdpChannel = !isTcpChannel && ConnectionImpl.isUdpChannel(channelClass); if (isTcpChannel) { /////////////////////// @@ -91,14 +97,10 @@ class RegistrationRemoteHandler extends RegistrationHandler { pipeline.addFirst(KRYO_DECODER, this.registrationWrapper.kryoUdpDecoder); } - - int idleTimeout = this.registrationWrapper.getIdleTimeout(); - if (idleTimeout > 0) { - // this makes the proper event get raised in the registrationHandler to kill NEW idle connections. Once "connected" they last a lot longer. - // we ALWAYS have this initial IDLE handler, so we don't have to worry about a SLOW-LORIS ATTACK against the server. - // in Seconds -- not shared, because it is per-connection - pipeline.addFirst(IDLE_HANDLER, new IdleStateHandler(2, 0, 0)); - } + // this makes the proper event get raised in the registrationHandler to kill NEW idle connections. Once "connected" they last a lot longer. + // we ALWAYS have this initial IDLE handler, so we don't have to worry about a SLOW-LORIS ATTACK against the server. + // in Seconds -- not shared, because it is per-connection + pipeline.addFirst(IDLE_HANDLER, new IdleStateHandler(2, 0, 0)); if (isTcpChannel) { ///////////////////////// @@ -120,49 +122,35 @@ class RegistrationRemoteHandler extends RegistrationHandler { // add the channel so we can access it later. // do NOT want to add UDP channels, since they are tracked differently. - if (this.logger.isInfoEnabled()) { + if (this.logger.isDebugEnabled()) { Channel channel = context.channel(); Class channelClass = channel.getClass(); - boolean isUdp = ConnectionImpl.isUdp(channelClass); + boolean isUdp = ConnectionImpl.isUdpChannel(channelClass); StringBuilder stringBuilder = new StringBuilder(96); stringBuilder.append("Connected to remote "); - if (ConnectionImpl.isTcp(channelClass)) { + if (ConnectionImpl.isTcpChannel(channelClass)) { stringBuilder.append("TCP"); } else if (isUdp) { stringBuilder.append("UDP"); } - else if (ConnectionImpl.isLocal(channelClass)) { + else if (ConnectionImpl.isLocalChannel(channelClass)) { stringBuilder.append("LOCAL"); } else { stringBuilder.append("UNKNOWN"); } - stringBuilder.append(" connection. ["); - stringBuilder.append(channel.localAddress()); + stringBuilder.append(" connection ["); + EndPoint.getHostDetails(stringBuilder, channel.localAddress()); - // this means we are "Sessionless" - if (isUdp) { - if (channel.remoteAddress() != null) { - stringBuilder.append(" ==> "); - stringBuilder.append(channel.remoteAddress()); - } - else { - // this means we are LISTENING. - stringBuilder.append(" <== "); - stringBuilder.append("?????"); - } - } - else { - stringBuilder.append(getConnectionDirection()); - stringBuilder.append(channel.remoteAddress()); - } + stringBuilder.append(getConnectionDirection()); + EndPoint.getHostDetails(stringBuilder, channel.remoteAddress()); stringBuilder.append("]"); - this.logger.info(stringBuilder.toString()); + this.logger.debug(stringBuilder.toString()); } } @@ -221,119 +209,228 @@ class RegistrationRemoteHandler extends RegistrationHandler { return false; } - // have to setup AFTER establish connection, data, as we don't want to enable AES until we're ready. + + /** + * upgrades a channel ONE channel at a time + */ final - void setupConnectionCrypto(final MetaChannel metaChannel, final InetSocketAddress remoteAddress) { + void upgradeDecoders(final Channel channel, final MetaChannel metaChannel) { + ChannelPipeline pipeline = channel.pipeline(); - if (this.logger.isDebugEnabled()) { - String type = ""; + try { + if (metaChannel.tcpChannel == channel) { + // add the new handlers (FORCE encryption and longer IDLE handler) + pipeline.replace(FRAME_AND_KRYO_DECODER, + FRAME_AND_KRYO_CRYPTO_DECODER, + new KryoDecoderCrypto(this.serializationManager)); // cannot be shared because of possible fragmentation. + } - if (metaChannel.tcpChannel != null) { - type = "TCP"; + if (metaChannel.udpChannel == channel) { + if (metaChannel.tcpChannel == null) { + // TODO: UDP (and TCP??) idle timeout (also, to close UDP session when TCP shuts down) + // this means that we are ONLY UDP, and we should have an idle timeout that CLOSES the session after a while. + // Naturally, one would want the "normal" idle to trigger first, but there always be a heartbeat if the idle trigger DOES NOT + // send data on the network, to make sure that the UDP-only session stays alive or disconnects. - if (metaChannel.udpChannel != null) { - type += "/"; + // If the server disconnects, the client has to be made aware of this when it tries to send data again (it must go through + // it's entire reconnect protocol) } + + // these encoders are shared + pipeline.replace(KRYO_DECODER, KRYO_CRYPTO_DECODER, this.registrationWrapper.kryoUdpDecoderCrypto); } - - if (metaChannel.udpChannel != null) { - type += "UDP"; - } - - this.logger.debug("Encrypting {} session with {}", type, remoteAddress); - } - - if (metaChannel.tcpChannel != null) { - ChannelPipeline pipeline = metaChannel.tcpChannel.pipeline(); - - // add the new handlers (FORCE encryption and longer IDLE handler) - pipeline.replace(FRAME_AND_KRYO_DECODER, - FRAME_AND_KRYO_CRYPTO_DECODER, - new KryoDecoderCrypto(this.serializationManager)); // cannot be shared because of possible fragmentation. - - int idleTimeout = this.registrationWrapper.getIdleTimeout(); - if (idleTimeout > 0) { - pipeline.replace(IDLE_HANDLER, IDLE_HANDLER_FULL, new IdleStateHandler(0, 0, idleTimeout, TimeUnit.MILLISECONDS)); - } - - pipeline.replace(FRAME_AND_KRYO_ENCODER, - FRAME_AND_KRYO_CRYPTO_ENCODER, - this.registrationWrapper.kryoTcpEncoderCrypto); // this is shared - } - - if (metaChannel.udpChannel != null) { - ChannelPipeline pipeline = metaChannel.udpChannel.pipeline(); - - int idleTimeout = this.registrationWrapper.getIdleTimeout(); - if (idleTimeout > 0) { - pipeline.replace(IDLE_HANDLER, IDLE_HANDLER_FULL, new IdleStateHandler(0, 0, idleTimeout, TimeUnit.MILLISECONDS)); - } - - pipeline.replace(KRYO_DECODER, KRYO_CRYPTO_DECODER, this.registrationWrapper.kryoUdpDecoderCrypto); - pipeline.replace(KRYO_ENCODER, KRYO_CRYPTO_ENCODER, this.registrationWrapper.kryoUdpEncoderCrypto); - } - } - - // have to setup AFTER establish connection, data, as we don't want to enable AES until we're ready. - @SuppressWarnings("AutoUnboxing") - final - void setupConnection(final MetaChannel metaChannel, final Channel channel) { - // Now setup our meta-channel to migrate to the correct connection handler for all regular data. - - // add the "connected"/"normal" handler now that we have established a "new" connection. - // This will have state, etc. for this connection. - InetSocketAddress remoteAddress = (InetSocketAddress) channel.remoteAddress(); - ConnectionImpl connection = (ConnectionImpl) this.registrationWrapper.connection0(metaChannel, remoteAddress); - - if (metaChannel.tcpChannel != null) { - ChannelPipeline pipeline = metaChannel.tcpChannel.pipeline(); - if (registrationWrapper.isClient()) { - pipeline.remove(RegistrationRemoteHandlerClientTCP.class); - } - else { - pipeline.remove(RegistrationRemoteHandlerServerTCP.class); - } - pipeline.addLast(CONNECTION_HANDLER, connection); - } - - if (metaChannel.udpChannel != null) { - ChannelPipeline pipeline = metaChannel.udpChannel.pipeline(); - if (registrationWrapper.isClient()) { - pipeline.remove(RegistrationRemoteHandlerClientUDP.class); - } - else { - pipeline.remove(RegistrationRemoteHandlerServerUDP.class); - } - pipeline.addLast(CONNECTION_HANDLER, connection); - } - - if (this.logger.isInfoEnabled()) { - String type = ""; - - if (metaChannel.tcpChannel != null) { - type = "TCP"; - - if (metaChannel.udpChannel != null) { - type += "/"; - } - } - - if (metaChannel.udpChannel != null) { - type += "UDP"; - } - - this.logger.info("Created a {} connection with {}", type, remoteAddress.getAddress()); + } catch (Exception e) { + logger.error("Error during connection pipeline upgrade", e); } } /** - * Internal call by the pipeline to notify the "Connection" object that it has "connected", meaning that modifications to the pipeline - * are finished. + * upgrades a channel ONE channel at a time */ final - void notifyConnection(MetaChannel metaChannel) { - this.registrationWrapper.connectionConnected0(metaChannel.connection); - this.registrationWrapper.removeSession(metaChannel); + void upgradeEncoders(final Channel channel, final MetaChannel metaChannel, final InetSocketAddress remoteAddress) { + ChannelPipeline pipeline = channel.pipeline(); + + try { + if (metaChannel.tcpChannel == channel) { + // add the new handlers (FORCE encryption and longer IDLE handler) + pipeline.replace(FRAME_AND_KRYO_ENCODER, + FRAME_AND_KRYO_CRYPTO_ENCODER, + registrationWrapper.kryoTcpEncoderCrypto); // this is shared + } + + if (metaChannel.udpChannel == channel) { + // these encoders are shared + pipeline.replace(KRYO_ENCODER, KRYO_CRYPTO_ENCODER, registrationWrapper.kryoUdpEncoderCrypto); + } + } catch (Exception e) { + logger.error("Error during connection pipeline upgrade", e); + } + } + + /** + * upgrades a channel ONE channel at a time + */ + final + void upgradePipeline(final MetaChannel metaChannel, final InetSocketAddress remoteAddress) { + try { + if (metaChannel.udpChannel != null) { + if (metaChannel.tcpChannel == null) { + // TODO: UDP (and TCP??) idle timeout (also, to close UDP session when TCP shuts down) + // this means that we are ONLY UDP, and we should have an idle timeout that CLOSES the session after a while. + // Naturally, one would want the "normal" idle to trigger first, but there always be a heartbeat if the idle trigger DOES NOT + // send data on the network, to make sure that the UDP-only session stays alive or disconnects. + + // If the server disconnects, the client has to be made aware of this when it tries to send data again (it must go through + // it's entire reconnect protocol) + } + } + + // add the "connected"/"normal" handler now that we have established a "new" connection. + // This will have state, etc. for this connection. THIS MUST BE 100% TCP/UDP created, otherwise it will break connections! + ConnectionImpl connection = this.registrationWrapper.connection0(metaChannel, remoteAddress); + metaChannel.connection = new ConnectionWrapper(connection); + + // Now setup our meta-channel to migrate to the correct connection handler for all regular data. + + if (metaChannel.tcpChannel != null) { + final ChannelPipeline pipeline = metaChannel.tcpChannel.pipeline(); + pipeline.addLast(CONNECTION_HANDLER, metaChannel.connection); + } + + if (metaChannel.udpChannel != null) { + final ChannelPipeline pipeline = metaChannel.udpChannel.pipeline(); + pipeline.addLast(CONNECTION_HANDLER, metaChannel.connection); + } + + + if (this.logger.isInfoEnabled()) { + String type = ""; + + if (metaChannel.tcpChannel != null) { + type = "TCP"; + + if (metaChannel.udpChannel != null) { + type += "/"; + } + } + + if (metaChannel.udpChannel != null) { + type += "UDP"; + } + + + StringBuilder stringBuilder = new StringBuilder(96); + + stringBuilder.append("Encrypted "); + if (metaChannel.tcpChannel != null) { + stringBuilder.append(type) + .append(" connection ["); + EndPoint.getHostDetails(stringBuilder, metaChannel.tcpChannel.localAddress()); + + stringBuilder.append(getConnectionDirection()); + EndPoint.getHostDetails(stringBuilder, metaChannel.tcpChannel.remoteAddress()); + stringBuilder.append("]"); + } + else if (metaChannel.udpChannel != null) { + stringBuilder.append(type) + .append(" connection ["); + EndPoint.getHostDetails(stringBuilder, metaChannel.udpChannel.localAddress()); + + stringBuilder.append(getConnectionDirection()); + EndPoint.getHostDetails(stringBuilder, metaChannel.udpChannel.remoteAddress()); + stringBuilder.append("]"); + } + + this.logger.info(stringBuilder.toString()); + } + } catch (Exception e) { + logger.error("Error during connection pipeline upgrade", e); + } + } + + final void cleanupPipeline(final MetaChannel metaChannel) { + final int idleTimeout = this.registrationWrapper.getIdleTimeout(); + + try { + // REMOVE our channel wrapper (only used for encryption) with the actual connection + metaChannel.connection = ((ConnectionWrapper) metaChannel.connection).connection; + + + if (metaChannel.tcpChannel != null) { + final ChannelPipeline pipeline = metaChannel.tcpChannel.pipeline(); + if (registrationWrapper.isClient()) { + pipeline.remove(RegistrationRemoteHandlerClientTCP.class); + } + else { + pipeline.remove(RegistrationRemoteHandlerServerTCP.class); + } + pipeline.remove(ConnectionWrapper.class); + + if (idleTimeout > 0) { + pipeline.replace(IDLE_HANDLER, IDLE_HANDLER_FULL, new IdleStateHandler(0, 0, idleTimeout, TimeUnit.MILLISECONDS)); + } else { + pipeline.remove(IDLE_HANDLER); + } + + pipeline.addLast(CONNECTION_HANDLER, metaChannel.connection); + + // we also DEREGISTER and run on a different event loop! + ChannelFuture future = metaChannel.tcpChannel.deregister(); + future.addListener(new GenericFutureListener>() { + @Override + public + void operationComplete(final Future f) throws Exception { + if (f.isSuccess()) { + workerEventLoop.register(metaChannel.tcpChannel); + } + } + }); + } + + if (metaChannel.udpChannel != null) { + final ChannelPipeline pipeline = metaChannel.udpChannel.pipeline(); + if (registrationWrapper.isClient()) { + pipeline.remove(RegistrationRemoteHandlerClientUDP.class); + } + else { + pipeline.remove(RegistrationRemoteHandlerServerUDP.class); + } + pipeline.remove(ConnectionWrapper.class); + + if (idleTimeout > 0) { + pipeline.replace(IDLE_HANDLER, IDLE_HANDLER_FULL, new IdleStateHandler(0, 0, idleTimeout, TimeUnit.MILLISECONDS)); + } + else { + pipeline.remove(IDLE_HANDLER); + } + + pipeline.addLast(CONNECTION_HANDLER, metaChannel.connection); + + // we also DEREGISTER and run on a different event loop! + // ONLY necessary for UDP-CLIENT, because for UDP-SERVER, the SessionManager takes care of this! + // if (registrationWrapper.isClient()) { + // ChannelFuture future = metaChannel.udpChannel.deregister(); + // future.addListener(new GenericFutureListener>() { + // @Override + // public + // void operationComplete(final Future f) throws Exception { + // if (f.isSuccess()) { + // workerEventLoop.register(metaChannel.udpChannel); + // } + // } + // }); + // } + } + } catch (Exception e) { + logger.error("Error during pipeline replace", e); + } + } + + final + void doConnect(final MetaChannel metaChannel) { + // safe cast, because it's always this way... + this.registrationWrapper.connectionConnected0((ConnectionImpl) metaChannel.connection); } // whoa! Didn't send valid public key info! diff --git a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerClient.java b/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerClient.java index 396a62dc..3faeecfc 100644 --- a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerClient.java +++ b/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerClient.java @@ -36,12 +36,13 @@ import dorkbox.util.crypto.CryptoECC; import dorkbox.util.exceptions.SecurityException; import dorkbox.util.serialization.EccPublicKeySerializer; import io.netty.channel.Channel; +import io.netty.channel.EventLoopGroup; public class RegistrationRemoteHandlerClient extends RegistrationRemoteHandler { - RegistrationRemoteHandlerClient(final String name, final RegistrationWrapper registrationWrapper) { - super(name, registrationWrapper); + RegistrationRemoteHandlerClient(final String name, final RegistrationWrapper registrationWrapper, final EventLoopGroup workerEventLoop) { + super(name, registrationWrapper, workerEventLoop); // check to see if we need to delete an IP address as commanded from the user prompt String ipAsString = System.getProperty(DELETE_IP); @@ -92,7 +93,7 @@ class RegistrationRemoteHandlerClient extends RegistrationRemoteHandler { @SuppressWarnings("Duplicates") void readClient(final Channel channel, final Registration registration, final String type, final MetaChannel metaChannel) { - InetSocketAddress remoteAddress = (InetSocketAddress) channel.remoteAddress(); + final InetSocketAddress remoteAddress = (InetSocketAddress) channel.remoteAddress(); // IN: session ID + public key + ecc parameters (which are a nonce. the SERVER defines what these are) // OUT: remote ECDH shared payload @@ -124,7 +125,6 @@ class RegistrationRemoteHandlerClient extends RegistrationRemoteHandler { EccPublicKeySerializer.write(output, (ECPublicKeyParameters) metaChannel.ecdhKey.getPublic()); outboundRegister.payload = output.toBytes(); - metaChannel.updateRoundTripOnWrite(); channel.writeAndFlush(outboundRegister); return; } @@ -171,46 +171,59 @@ class RegistrationRemoteHandlerClient extends RegistrationRemoteHandler { Registration outboundRegister = new Registration(metaChannel.sessionId); // do we have any more registrations? - boolean hasMoreRegistrations = registrationWrapper.hasMoreRegistrations(); - outboundRegister.hasMore = hasMoreRegistrations; - - metaChannel.updateRoundTripOnWrite(); + outboundRegister.hasMore = registrationWrapper.hasMoreRegistrations(); channel.writeAndFlush(outboundRegister); - - if (hasMoreRegistrations) { - // start the process for the next protocol. - registrationWrapper.startNextProtocolRegistration(); - } - - // always return! + // wait for ack from the server before registering the next protocol return; } - // // when we have a "continuing registration" for another protocol, we have to have another roundtrip. - // if (registration.payload != null) { - // metaChannel.updateRoundTripTime(); - // channel.writeAndFlush(new Registration(metaChannel.sessionId)); - // return; - // } + // IN: upgrade=true if we must upgrade this connection + if (registration.upgrade) { + // this pipeline can now be marked to be upgraded + + // upgrade the connection to an encrypted connection + // this pipeline encoder/decoder can now be upgraded, and the "connection" added + upgradeEncoders(channel, metaChannel, remoteAddress); + upgradeDecoders(channel, metaChannel); + } + + // IN: hasMore=true if we have more registrations to do, false otherwise + if (registration.hasMore) { + registrationWrapper.startNextProtocolRegistration(); + return; + } - // We ONLY get here after the server acks our registration status. - // The server will only ack if we DO NOT have more registrations. If we have more registrations, the server waits. - setupConnectionCrypto(metaChannel, remoteAddress); - setupConnection(metaChannel, channel); + // + // + // we only get this when we are 100% done with the registration of all connection types. + // + // - // wait for a "round trip" amount of time, then notify the APP! - final long delay = TimeUnit.NANOSECONDS.toMillis(metaChannel.getRoundTripTime() * 2); - channel.eventLoop() - .schedule(new Runnable() { - @Override - public - void run() { - logger.trace("Notify Connection"); - notifyConnection(metaChannel); - } - }, delay, TimeUnit.MILLISECONDS); + + if (!registration.upgraded) { + // setup the pipeline with the real connection + upgradePipeline(metaChannel, remoteAddress); + + // tell the server we are upgraded (it will bounce back telling us to connect) + registration.upgraded = true; + channel.writeAndFlush(registration); + return; + } + + + // remove the ConnectionWrapper (that was used to upgrade the connection) + cleanupPipeline(metaChannel); + + workerEventLoop.schedule(new Runnable() { + @Override + public + void run() { + logger.trace("Notify Connection"); + doConnect(metaChannel); + } + }, 20, TimeUnit.MILLISECONDS); } } diff --git a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerClientTCP.java b/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerClientTCP.java index 72330771..bedc4e46 100644 --- a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerClientTCP.java +++ b/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerClientTCP.java @@ -15,17 +15,21 @@ */ package dorkbox.network.connection.registration.remote; +import dorkbox.network.connection.ConnectionImpl; import dorkbox.network.connection.RegistrationWrapper; import dorkbox.network.connection.registration.MetaChannel; import dorkbox.network.connection.registration.Registration; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.EventLoopGroup; public class RegistrationRemoteHandlerClientTCP extends RegistrationRemoteHandlerClient { public - RegistrationRemoteHandlerClientTCP(final String name, final RegistrationWrapper registrationWrapper) { - super(name, registrationWrapper); + RegistrationRemoteHandlerClientTCP(final String name, + final RegistrationWrapper registrationWrapper, + final EventLoopGroup workerEventLoop) { + super(name, registrationWrapper, workerEventLoop); } /** @@ -76,7 +80,14 @@ class RegistrationRemoteHandlerClientTCP extends RegistrationRemoteHandlerClient } else { logger.error("Error registering TCP with remote server!"); - shutdown(channel, 0); + + // this is what happens when the registration happens too quickly... + Object connection = context.pipeline().last(); + if (connection instanceof ConnectionImpl) { + ((ConnectionImpl) connection).channelRead(context, message); + } else { + shutdown(channel, 0); + } } } } diff --git a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerClientUDP.java b/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerClientUDP.java index d8f30b73..9b096a7e 100644 --- a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerClientUDP.java +++ b/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerClientUDP.java @@ -18,18 +18,22 @@ package dorkbox.network.connection.registration.remote; import java.io.IOException; import java.net.InetSocketAddress; +import dorkbox.network.connection.ConnectionImpl; import dorkbox.network.connection.RegistrationWrapper; import dorkbox.network.connection.registration.MetaChannel; import dorkbox.network.connection.registration.Registration; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.EventLoopGroup; @SuppressWarnings("Duplicates") public class RegistrationRemoteHandlerClientUDP extends RegistrationRemoteHandlerClient { public - RegistrationRemoteHandlerClientUDP(final String name, final RegistrationWrapper registrationWrapper) { - super(name, registrationWrapper); + RegistrationRemoteHandlerClientUDP(final String name, + final RegistrationWrapper registrationWrapper, + final EventLoopGroup workerEventLoop) { + super(name, registrationWrapper, workerEventLoop); } /** @@ -53,12 +57,6 @@ class RegistrationRemoteHandlerClientUDP extends RegistrationRemoteHandlerClient if (firstSession != null) { outboundRegister.sessionID = firstSession.sessionId; outboundRegister.hasMore = registrationWrapper.hasMoreRegistrations(); - - - // when we have a "continuing registration" for another protocol, we have to have another roundtrip. - // outboundRegister.payload = new byte[0]; - - firstSession.updateRoundTripOnWrite(); } // no size info, since this is UDP, it is not segmented @@ -102,7 +100,15 @@ class RegistrationRemoteHandlerClientUDP extends RegistrationRemoteHandlerClient } else { logger.error("Error registering UDP with remote server!"); - shutdown(channel, 0); + // this is what happens when the registration happens too quickly... + Object connection = context.pipeline() + .last(); + if (connection instanceof ConnectionImpl) { + ((ConnectionImpl) connection).channelRead(context, message); + } + else { + shutdown(channel, 0); + } } } } diff --git a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerServer.java b/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerServer.java index 13fec36b..a3d60b6a 100644 --- a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerServer.java +++ b/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerServer.java @@ -39,6 +39,8 @@ import dorkbox.network.connection.registration.Registration; import dorkbox.util.crypto.CryptoECC; import dorkbox.util.serialization.EccPublicKeySerializer; import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.EventLoopGroup; public class RegistrationRemoteHandlerServer extends RegistrationRemoteHandler { @@ -50,8 +52,8 @@ class RegistrationRemoteHandlerServer extends RegistrationRemoteHandler { private volatile long ecdhTimeout = System.nanoTime(); - RegistrationRemoteHandlerServer(final String name, final RegistrationWrapper registrationWrapper) { - super(name, registrationWrapper); + RegistrationRemoteHandlerServer(final String name, final RegistrationWrapper registrationWrapper, final EventLoopGroup workerEventLoop) { + super(name, registrationWrapper, workerEventLoop); } /** @@ -83,12 +85,10 @@ class RegistrationRemoteHandlerServer extends RegistrationRemoteHandler { * UDP has a VERY limited size, so we have to break up registration steps into the following * 1) session ID == 0 -> exchange session ID and public keys (session ID != 0 now) * 2) session ID != 0 -> establish ECDH shared secret as AES key/iv - * 3) - * */ @SuppressWarnings("Duplicates") - void readServer(final Channel channel, final Registration registration, final String type, final MetaChannel metaChannel) { - InetSocketAddress remoteAddress = (InetSocketAddress) channel.remoteAddress(); + void readServer(final ChannelHandlerContext context, final Channel channel, final Registration registration, final String type, final MetaChannel metaChannel) { + final InetSocketAddress remoteAddress = (InetSocketAddress) channel.remoteAddress(); // IN: session ID == 0 (start of new connection) // OUT: session ID + public key + ecc parameters (which are a nonce. the SERVER defines what these are) @@ -115,8 +115,8 @@ class RegistrationRemoteHandlerServer extends RegistrationRemoteHandler { outboundRegister.publicKey = registrationWrapper.getPublicKey(); outboundRegister.eccParameters = CryptoECC.generateSharedParameters(registrationWrapper.getSecureRandom()); - metaChannel.updateRoundTripOnWrite(); channel.writeAndFlush(outboundRegister); + metaChannel.updateRoundTripOnWrite(); return; } @@ -172,48 +172,71 @@ class RegistrationRemoteHandlerServer extends RegistrationRemoteHandler { EccPublicKeySerializer.write(output, (ECPublicKeyParameters) metaChannel.ecdhKey.getPublic()); outboundRegister.payload = output.toBytes(); - metaChannel.updateRoundTripOnWrite(); channel.writeAndFlush(outboundRegister); + metaChannel.updateRoundTripOnWrite(); + return; + } + + // ALWAYS upgrade the connection at this point. + // IN: upgraded=false if we haven't upgraded to encryption yet (this will always be the case right after encryption is setup) + + // NOTE: if we have more registrations, we will "bounce back" that status so the client knows what to do. + // IN: hasMore=true if we have more registrations to do, false otherwise + + if (!registration.upgraded) { + // upgrade the connection to an encrypted connection + registration.upgrade = true; + upgradeDecoders(channel, metaChannel); + + // bounce back to the client so it knows we received it + channel.write(registration); + + // this pipeline encoder/decoder can now be upgraded, and the "connection" added + upgradeEncoders(channel, metaChannel, remoteAddress); + + if (!registration.hasMore) { + // we only get this when we are 100% done with the registration of all connection types. + + // setup the pipeline with the real connection + upgradePipeline(metaChannel, remoteAddress); + } + + channel.flush(); + metaChannel.updateRoundTripOnWrite(); return; } - // do we have any more registrations? + // + // + // we only get this when we are 100% done with the registration of all connection types. + // the context is the LAST protocol to be registered + // + // - // IN: hasMore=true if we have more registrations to do, false otherwise - if (!registration.hasMore) { - // // when we have a "continuing registration" for another protocol, we have to have another roundtrip. - // if (registration.payload != null) { - // metaChannel.updateRoundTripTime(); - // channel.writeAndFlush(registration); - // return; - // } + // make sure we don't try to upgrade the client again. + registration.upgrade = false; + + // have to get the delay before we update the round-trip time. We cannot have a delay that is BIGGER than our idle timeout! + final int idleTimeout = Math.max(2, this.registrationWrapper.getIdleTimeout()-2); // 2 because it is a reasonable amount for the "standard" delay amount + final long delay = Math.max(idleTimeout, TimeUnit.NANOSECONDS.toMillis(metaChannel.getRoundTripTime())); + logger.trace("Notify delay MS: {}", delay); - // we only get this when we are 100% done with the registration of all connection types. + // remove the ConnectionWrapper (that was used to upgrade the connection) + cleanupPipeline(metaChannel); - // have to get the delay before we update the round-trip time - final long delay = TimeUnit.NANOSECONDS.toMillis(metaChannel.getRoundTripTime() * 2); + // this tells the client we are ready to connect (we just bounce back the original message) + channel.writeAndFlush(registration); - // causes client to setup network connection & AES (we just bounce back the original message) - metaChannel.updateRoundTripOnWrite(); - channel.writeAndFlush(registration); - - setupConnectionCrypto(metaChannel, remoteAddress); - setupConnection(metaChannel, channel); - - // wait for a "round trip" amount of time, then notify the APP! - channel.eventLoop() - .schedule(new Runnable() { - @Override - public - void run() { - logger.trace("Notify Connection"); - notifyConnection(metaChannel); - } - }, delay, TimeUnit.MILLISECONDS); - } - - // otherwise we have more registrations... + // wait for a "round trip" amount of time, then notify the APP! + workerEventLoop.schedule(new Runnable() { + @Override + public + void run() { + logger.trace("Notify Connection"); + doConnect(metaChannel); + } + }, delay, TimeUnit.MILLISECONDS); } } diff --git a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerServerTCP.java b/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerServerTCP.java index 2c7b92e3..cc973876 100644 --- a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerServerTCP.java +++ b/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerServerTCP.java @@ -15,18 +15,22 @@ */ package dorkbox.network.connection.registration.remote; +import dorkbox.network.connection.ConnectionImpl; import dorkbox.network.connection.RegistrationWrapper; import dorkbox.network.connection.registration.MetaChannel; import dorkbox.network.connection.registration.Registration; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.EventLoopGroup; public class RegistrationRemoteHandlerServerTCP extends RegistrationRemoteHandlerServer { public - RegistrationRemoteHandlerServerTCP(final String name, final RegistrationWrapper registrationWrapper) { - super(name, registrationWrapper); + RegistrationRemoteHandlerServerTCP(final String name, + final RegistrationWrapper registrationWrapper, + final EventLoopGroup workerEventLoop) { + super(name, registrationWrapper, workerEventLoop); } /** @@ -47,6 +51,7 @@ class RegistrationRemoteHandlerServerTCP extends RegistrationRemoteHandlerServer if (sessionId == 0) { metaChannel = registrationWrapper.createSessionServer(); metaChannel.tcpChannel = channel; + // TODO: use this: channel.voidPromise(); logger.debug("New TCP connection. Saving meta-channel id: {}", metaChannel.sessionId); } else { @@ -59,11 +64,20 @@ class RegistrationRemoteHandlerServerTCP extends RegistrationRemoteHandlerServer } } - readServer(channel, registration, "TCP server", metaChannel); + readServer(context, channel, registration, "TCP server", metaChannel); } else { logger.error("Error registering TCP with remote client!"); - shutdown(channel, 0); + + // this is what happens when the registration happens too quickly... + Object connection = context.pipeline() + .last(); + if (connection instanceof ConnectionImpl) { + ((ConnectionImpl) connection).channelRead(context, message); + } + else { + shutdown(channel, 0); + } } } } diff --git a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerServerUDP.java b/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerServerUDP.java index 604d0c19..134e75a4 100644 --- a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerServerUDP.java +++ b/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerServerUDP.java @@ -15,20 +15,24 @@ */ package dorkbox.network.connection.registration.remote; +import dorkbox.network.connection.ConnectionImpl; import dorkbox.network.connection.RegistrationWrapper; import dorkbox.network.connection.registration.MetaChannel; import dorkbox.network.connection.registration.Registration; import io.netty.channel.Channel; import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.EventLoopGroup; @SuppressWarnings("Duplicates") @Sharable public class RegistrationRemoteHandlerServerUDP extends RegistrationRemoteHandlerServer { public - RegistrationRemoteHandlerServerUDP(final String name, final RegistrationWrapper registrationWrapper) { - super(name, registrationWrapper); + RegistrationRemoteHandlerServerUDP(final String name, + final RegistrationWrapper registrationWrapper, + final EventLoopGroup workerEventLoop) { + super(name, registrationWrapper, workerEventLoop); } @Override @@ -60,11 +64,20 @@ class RegistrationRemoteHandlerServerUDP extends RegistrationRemoteHandlerServer metaChannel.udpChannel = channel; } - readServer(channel, registration, "UDP server", metaChannel); + readServer(context, channel, registration, "UDP server", metaChannel); } else { logger.error("Error registering UDP with remote client!"); - shutdown(channel, 0); + + // this is what happens when the registration happens too quickly... + Object connection = context.pipeline() + .last(); + if (connection instanceof ConnectionImpl) { + ((ConnectionImpl) connection).channelRead(context, message); + } + else { + shutdown(channel, 0); + } } } } diff --git a/src/dorkbox/network/connection/wrapper/ChannelLocalWrapper.java b/src/dorkbox/network/connection/wrapper/ChannelLocalWrapper.java index 9c09211b..98b83bcf 100644 --- a/src/dorkbox/network/connection/wrapper/ChannelLocalWrapper.java +++ b/src/dorkbox/network/connection/wrapper/ChannelLocalWrapper.java @@ -42,6 +42,7 @@ class ChannelLocalWrapper implements ChannelWrapper, ConnectionPoint { ChannelLocalWrapper(MetaChannel metaChannel, final RmiObjectHandler rmiObjectHandler) { this.channel = metaChannel.localChannel; this.rmiObjectHandler = rmiObjectHandler; + this.remoteAddress = ((LocalAddress) this.channel.remoteAddress()).id(); } /** @@ -76,15 +77,6 @@ class ChannelLocalWrapper implements ChannelWrapper, ConnectionPoint { return this; } - /** - * Initialize the connection with any extra info that is needed but was unavailable at the channel construction. - */ - @Override - public final - void init() { - this.remoteAddress = ((LocalAddress) this.channel.remoteAddress()).id(); - } - /** * Flushes the contents of the LOCAL pipes to the actual transport. */ diff --git a/src/dorkbox/network/connection/wrapper/ChannelNetwork.java b/src/dorkbox/network/connection/wrapper/ChannelNetwork.java index b7c7fba0..30d14ea9 100644 --- a/src/dorkbox/network/connection/wrapper/ChannelNetwork.java +++ b/src/dorkbox/network/connection/wrapper/ChannelNetwork.java @@ -55,6 +55,7 @@ class ChannelNetwork implements ConnectionPoint { return channel.isWritable(); } + @Override public void flush() { if (shouldFlush.compareAndSet(true, false)) { @@ -71,7 +72,9 @@ class ChannelNetwork implements ConnectionPoint { public void close(long maxShutdownWaitTimeInMilliSeconds) { shouldFlush.set(false); - channel.close() - .awaitUninterruptibly(maxShutdownWaitTimeInMilliSeconds); + if (channel.isActive()) { + channel.close() + .awaitUninterruptibly(maxShutdownWaitTimeInMilliSeconds); + } } } diff --git a/src/dorkbox/network/connection/wrapper/ChannelNetworkWrapper.java b/src/dorkbox/network/connection/wrapper/ChannelNetworkWrapper.java index 694889da..ed0d45b6 100644 --- a/src/dorkbox/network/connection/wrapper/ChannelNetworkWrapper.java +++ b/src/dorkbox/network/connection/wrapper/ChannelNetworkWrapper.java @@ -27,6 +27,7 @@ import dorkbox.network.connection.ISessionManager; import dorkbox.network.connection.registration.MetaChannel; import dorkbox.network.rmi.RmiObjectHandler; import dorkbox.util.FastThreadLocal; +import io.netty.bootstrap.DatagramCloseMessage; import io.netty.util.NetUtil; public @@ -107,15 +108,6 @@ class ChannelNetworkWrapper implements ChannelWrapper { return this.udp; } - /** - * Initialize the connection with any extra info that is needed but was unavailable at the channel construction. - */ - @Override - public final - void init() { - // nothing to do. - } - /** * Flushes the contents of the TCP/UDP/etc pipes to the actual transport. */ @@ -170,6 +162,15 @@ class ChannelNetworkWrapper implements ChannelWrapper { } if (this.udp != null) { + // send a hint to the other connection that we should close. While not always 100% successful, this helps clean up connections + // on the remote end + try { + this.udp.write(new DatagramCloseMessage()); + this.udp.flush(); + Thread.yield(); + } catch (Exception e) { + e.printStackTrace(); + } this.udp.close(maxShutdownWaitTimeInMilliSeconds); } diff --git a/src/dorkbox/network/connection/wrapper/ChannelNull.java b/src/dorkbox/network/connection/wrapper/ChannelNull.java index 75b0882d..7cc79517 100644 --- a/src/dorkbox/network/connection/wrapper/ChannelNull.java +++ b/src/dorkbox/network/connection/wrapper/ChannelNull.java @@ -33,20 +33,23 @@ class ChannelNull implements ConnectionPoint { ChannelNull() { } - /** - * Write an object to the underlying channel - */ @Override public void write(Object object) { } + @Override + public + void flush() { + } + /** * @return true if the channel is writable. Useful when sending large amounts of data at once. */ @Override public boolean isWritable() { + // this channel is ALWAYS writable! (it just does nothing...) return true; } diff --git a/src/dorkbox/network/connection/wrapper/ChannelWrapper.java b/src/dorkbox/network/connection/wrapper/ChannelWrapper.java index cd7e4f4b..6c27c5ba 100644 --- a/src/dorkbox/network/connection/wrapper/ChannelWrapper.java +++ b/src/dorkbox/network/connection/wrapper/ChannelWrapper.java @@ -28,11 +28,6 @@ interface ChannelWrapper { ConnectionPoint tcp(); ConnectionPoint udp(); - /** - * Initialize the connection with any extra info that is needed but was unavailable at the channel construction. - */ - void init(); - /** * Flushes the contents of the TCP/UDP/etc pipes to the actual transport. */ diff --git a/src/dorkbox/network/pipeline/ConnectionType.java b/src/dorkbox/network/pipeline/ConnectionType.java new file mode 100644 index 00000000..d19c8869 --- /dev/null +++ b/src/dorkbox/network/pipeline/ConnectionType.java @@ -0,0 +1,24 @@ +/* + * Copyright 2018 dorkbox, llc. + * + * 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 dorkbox.network.pipeline; + +/** + * What is the underlying connection type? + */ +public +enum ConnectionType { + LOCAL, OIO, NIO, EPOLL, KQUEUE +} diff --git a/src/dorkbox/network/pipeline/MagicBytes.java b/src/dorkbox/network/pipeline/MagicBytes.java index 13e04e4f..bce0c5d3 100644 --- a/src/dorkbox/network/pipeline/MagicBytes.java +++ b/src/dorkbox/network/pipeline/MagicBytes.java @@ -15,30 +15,20 @@ */ package dorkbox.network.pipeline; -import io.netty.buffer.ByteBuf; - /** - * + * Magic bytes used to identify packets when they are specific types */ public class MagicBytes { - /** - * bit masks - *

- * 0 means it's not encrypted or anything.... - */ - public static final byte crypto = (byte) (1 << 1); - /** - * Determines if this buffer is encrypted or not. - * - * REGISTRATION is the ONLY thing NOT encrypted - * encrypted Y/N is always written by the serialization writer, so it is ALWAYS safe to check it here. - */ - public static - boolean isEncrypted(final ByteBuf buffer) { - // read off the magic byte - byte magicByte = buffer.getByte(buffer.readerIndex()); - return (magicByte & crypto) == crypto; - } + // BROADCAST ... + public static final byte broadcastID = (byte) 42; + public static final byte broadcastResponseID = (byte) 57; + + public static final byte HAS_TCP = (byte) (1 << 1); + public static final byte HAS_UDP = (byte) (1 << 2); + + // max number of bytes in a broadcast packet (if both TCP and UDP are enabled) + public static final int maxPacketSize = 6; + // END BROADCAST ... } diff --git a/src/dorkbox/network/pipeline/discovery/BroadcastResponse.java b/src/dorkbox/network/pipeline/discovery/BroadcastResponse.java new file mode 100644 index 00000000..5781c8e9 --- /dev/null +++ b/src/dorkbox/network/pipeline/discovery/BroadcastResponse.java @@ -0,0 +1,34 @@ +/* + * Copyright 2018 dorkbox, llc. + * + * 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 dorkbox.network.pipeline.discovery; + +import java.net.InetAddress; + +/** + * Holds the broadcast response data + */ +public +class BroadcastResponse { + public final InetAddress remoteAddress; + public final int tcpPort; + public final int udpPort; + + BroadcastResponse(final InetAddress remoteAddress, final int tcpPort, final int udpPort) { + this.remoteAddress = remoteAddress; + this.tcpPort = tcpPort; + this.udpPort = udpPort; + } +} diff --git a/src/dorkbox/network/pipeline/discovery/BroadcastServer.java b/src/dorkbox/network/pipeline/discovery/BroadcastServer.java index cb579f3d..2bb92a79 100644 --- a/src/dorkbox/network/pipeline/discovery/BroadcastServer.java +++ b/src/dorkbox/network/pipeline/discovery/BroadcastServer.java @@ -15,11 +15,12 @@ */ package dorkbox.network.pipeline.discovery; +import java.net.InetAddress; import java.net.InetSocketAddress; -import dorkbox.network.Broadcast; import dorkbox.network.Server; import dorkbox.network.connection.EndPoint; +import dorkbox.network.pipeline.MagicBytes; import io.netty.buffer.ByteBuf; import io.netty.channel.Channel; import io.netty.channel.socket.DatagramPacket; @@ -31,24 +32,77 @@ public class BroadcastServer { private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(Server.class.getSimpleName()); + private final int tcpPort; + private final int udpPort; + + private final int bufferSize; + public BroadcastServer() { + this.bufferSize = 0; + this.tcpPort = 0; + this.udpPort = 0; } + public + BroadcastServer(final int tcpPort, final int udpPort) { + this.tcpPort = tcpPort; + this.udpPort = udpPort; + + // either it will be TCP or UDP, or BOTH + if (tcpPort > 0 ^ udpPort > 0) { + // TCP or UDP + + // ID + TCP or UDP ID + TCP or UDP port + bufferSize = 4; + } + else { + // BOTH + + // ID + TCP and UDP ID + TCP and UDP port + bufferSize = 6; + } + } + + /** * @return true if the broadcast was responded to, false if it was not a broadcast (and there was no response) */ - public boolean isBroadcast(final Channel channel, ByteBuf byteBuf, final InetSocketAddress localAddress, InetSocketAddress remoteAddress) { + public boolean isDiscoveryRequest(final Channel channel, ByteBuf byteBuf, final InetSocketAddress localAddress, InetSocketAddress remoteAddress) { if (byteBuf.readableBytes() == 1) { // this is a BROADCAST discovery event. Don't read the byte unless it is... - if (byteBuf.getByte(0) == Broadcast.broadcastID) { + if (byteBuf.getByte(0) == MagicBytes.broadcastID) { byteBuf.readByte(); // read the byte to consume it (now that we verified it is a broadcast byte) // absolutely MUST send packet > 0 across, otherwise netty will think it failed to write to the socket, and keep trying. // (this bug was fixed by netty, however we are keeping this code) ByteBuf directBuffer = channel.alloc() - .ioBuffer(1); - directBuffer.writeByte(Broadcast.broadcastResponseID); + .ioBuffer(bufferSize); + directBuffer.writeByte(MagicBytes.broadcastResponseID); + + // now output the port information for TCP/UDP so the broadcast client knows which port to connect to + // either it will be TCP or UDP, or BOTH + + int enabledFlag = 0; + if (tcpPort > 0) { + enabledFlag |= MagicBytes.HAS_TCP; + } + + if (udpPort > 0) { + enabledFlag |= MagicBytes.HAS_UDP; + } + + directBuffer.writeByte(enabledFlag); + + // TCP is always first + if (tcpPort > 0) { + directBuffer.writeShort(tcpPort); + } + + if (udpPort > 0) { + directBuffer.writeShort(udpPort); + } + channel.writeAndFlush(new DatagramPacket(directBuffer, remoteAddress, localAddress)); @@ -61,4 +115,41 @@ class BroadcastServer { return false; } + + + /** + * @return true if this is a broadcast response, false if it was not a broadcast response + */ + public static + boolean isDiscoveryResponse(ByteBuf byteBuf, final InetAddress remoteAddress, final Channel channel) { + if (byteBuf.readableBytes() <= MagicBytes.maxPacketSize) { + // this is a BROADCAST discovery RESPONSE event. Don't read the byte unless it is... + if (byteBuf.getByte(0) == MagicBytes.broadcastResponseID) { + byteBuf.readByte(); // read the byte to consume it (now that we verified it is a broadcast byte) + + // either it will be TCP or UDP, or BOTH + int typeID = byteBuf.readByte(); + + int tcpPort = 0; + int udpPort = 0; + + // TCP is always first + if ((typeID & MagicBytes.HAS_TCP) == MagicBytes.HAS_TCP) { + tcpPort = byteBuf.readUnsignedShort(); + } + + if ((typeID & MagicBytes.HAS_UDP) == MagicBytes.HAS_UDP) { + udpPort = byteBuf.readUnsignedShort(); + } + + channel.attr(ClientDiscoverHostHandler.STATE) + .set(new BroadcastResponse(remoteAddress, tcpPort, udpPort)); + + byteBuf.release(); + return true; + } + } + + return false; + } } diff --git a/src/dorkbox/network/pipeline/discovery/ClientDiscoverHostHandler.java b/src/dorkbox/network/pipeline/discovery/ClientDiscoverHostHandler.java index 0cfe66fd..1d12b61e 100644 --- a/src/dorkbox/network/pipeline/discovery/ClientDiscoverHostHandler.java +++ b/src/dorkbox/network/pipeline/discovery/ClientDiscoverHostHandler.java @@ -15,29 +15,33 @@ */ package dorkbox.network.pipeline.discovery; -import dorkbox.network.Broadcast; +import java.net.InetAddress; + import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.socket.DatagramPacket; import io.netty.util.AttributeKey; -import java.net.InetSocketAddress; - public class ClientDiscoverHostHandler extends SimpleChannelInboundHandler { - // This uses CHANNEL LOCAL to save the data. + // This uses CHANNEL LOCAL DATA to save the data. - public static final AttributeKey STATE = AttributeKey.valueOf(ClientDiscoverHostHandler.class, "Discover.state"); + public static final AttributeKey STATE = AttributeKey.valueOf(ClientDiscoverHostHandler.class, "Discover.state"); + + ClientDiscoverHostHandler() { + } @Override protected void channelRead0(final ChannelHandlerContext context, final DatagramPacket message) throws Exception { - ByteBuf data = message.content(); - if (data.readableBytes() == 1 && data.readByte() == Broadcast.broadcastResponseID) { - context.channel() - .attr(STATE) - .set(message.sender()); + ByteBuf byteBuf = message.content(); + + InetAddress remoteAddress = message.sender() + .getAddress(); + + if (BroadcastServer.isDiscoveryResponse(byteBuf, remoteAddress, context.channel())) { + // the state/ports/etc are set inside the isDiscoveryResponse() method... context.channel() .close(); } diff --git a/src/dorkbox/network/pipeline/tcp/KryoDecoder.java b/src/dorkbox/network/pipeline/tcp/KryoDecoder.java index 0a7fa2d8..e769e2fe 100644 --- a/src/dorkbox/network/pipeline/tcp/KryoDecoder.java +++ b/src/dorkbox/network/pipeline/tcp/KryoDecoder.java @@ -36,7 +36,7 @@ class KryoDecoder extends ByteToMessageDecoder { @SuppressWarnings("unused") protected - Object readObject(CryptoSerializationManager serializationManager, ChannelHandlerContext context, ByteBuf in, int length) throws IOException { + Object readObject(CryptoSerializationManager serializationManager, ChannelHandlerContext context, ByteBuf in, int length) throws Exception { // no connection here because we haven't created one yet. When we do, we replace this handler with a new one. return serializationManager.read(in, length); } @@ -167,7 +167,7 @@ class KryoDecoder extends ByteToMessageDecoder { object = readObject(serializationManager, context, in, length); out.add(object); } catch (Exception ex) { - context.fireExceptionCaught(new IOException("Unable to deserialize object!", ex)); + context.fireExceptionCaught(new IOException("Unable to deserialize object for " + this.getClass(), ex)); } } } diff --git a/src/dorkbox/network/pipeline/tcp/KryoDecoderCrypto.java b/src/dorkbox/network/pipeline/tcp/KryoDecoderCrypto.java index 19f08b97..bee51e7d 100644 --- a/src/dorkbox/network/pipeline/tcp/KryoDecoderCrypto.java +++ b/src/dorkbox/network/pipeline/tcp/KryoDecoderCrypto.java @@ -15,9 +15,7 @@ */ package dorkbox.network.pipeline.tcp; -import java.io.IOException; - -import dorkbox.network.connection.ConnectionImpl; +import dorkbox.network.connection.CryptoConnection; import dorkbox.network.serialization.CryptoSerializationManager; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; @@ -37,10 +35,14 @@ class KryoDecoderCrypto extends KryoDecoder { Object readObject(final CryptoSerializationManager serializationManager, final ChannelHandlerContext context, final ByteBuf in, - final int length) throws IOException { + final int length) throws Exception { - ConnectionImpl connection = (ConnectionImpl) context.pipeline() - .last(); - return serializationManager.readWithCrypto(connection, in, length); + try { + CryptoConnection connection = (CryptoConnection) context.pipeline() + .last(); + return serializationManager.readWithCrypto(connection, in, length); + } catch (Exception e) { + throw e; + } } } diff --git a/src/dorkbox/network/pipeline/tcp/KryoEncoder.java b/src/dorkbox/network/pipeline/tcp/KryoEncoder.java index 0ade5a6c..16c1b79a 100644 --- a/src/dorkbox/network/pipeline/tcp/KryoEncoder.java +++ b/src/dorkbox/network/pipeline/tcp/KryoEncoder.java @@ -65,7 +65,7 @@ class KryoEncoder extends MessageToByteEncoder { int index = out.writerIndex(); // now set the frame length - // (reservedLengthLength) 5 is the reserved space for the integer. + // (reservedLengthLength) 4 is the reserved space for the integer. int length = index - startIndex; // specify the header. diff --git a/src/dorkbox/network/pipeline/tcp/KryoEncoderCrypto.java b/src/dorkbox/network/pipeline/tcp/KryoEncoderCrypto.java index 5759e446..f5ef41f1 100644 --- a/src/dorkbox/network/pipeline/tcp/KryoEncoderCrypto.java +++ b/src/dorkbox/network/pipeline/tcp/KryoEncoderCrypto.java @@ -17,7 +17,7 @@ package dorkbox.network.pipeline.tcp; import java.io.IOException; -import dorkbox.network.connection.ConnectionImpl; +import dorkbox.network.connection.CryptoConnection; import dorkbox.network.serialization.CryptoSerializationManager; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandler.Sharable; @@ -39,8 +39,8 @@ class KryoEncoderCrypto extends KryoEncoder { final Object msg, final ByteBuf buffer) throws IOException { - ConnectionImpl connection = (ConnectionImpl) context.pipeline() - .last(); + CryptoConnection connection = (CryptoConnection) context.pipeline() + .last(); serializationManager.writeWithCrypto(connection, buffer, msg); } } diff --git a/src/dorkbox/network/pipeline/udp/KryoDecoderUdpCrypto.java b/src/dorkbox/network/pipeline/udp/KryoDecoderUdpCrypto.java index fa79c7c1..36c5c073 100644 --- a/src/dorkbox/network/pipeline/udp/KryoDecoderUdpCrypto.java +++ b/src/dorkbox/network/pipeline/udp/KryoDecoderUdpCrypto.java @@ -20,10 +20,9 @@ import java.util.List; import org.slf4j.LoggerFactory; -import dorkbox.network.connection.ConnectionImpl; +import dorkbox.network.connection.CryptoConnection; import dorkbox.network.serialization.CryptoSerializationManager; import io.netty.buffer.ByteBuf; -import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.socket.DatagramPacket; @@ -43,12 +42,12 @@ class KryoDecoderUdpCrypto extends MessageToMessageDecoder { @Override public void decode(ChannelHandlerContext context, DatagramPacket in, List out) throws Exception { - ChannelHandler last = context.pipeline() - .last(); + CryptoConnection last = (CryptoConnection) context.pipeline() + .last(); try { ByteBuf data = in.content(); - Object object = serializationManager.readWithCrypto((ConnectionImpl) last, data, data.readableBytes()); + Object object = serializationManager.readWithCrypto(last, data, data.readableBytes()); out.add(object); } catch (IOException e) { String message = "Unable to deserialize object"; diff --git a/src/dorkbox/network/pipeline/udp/KryoEncoderUdpCrypto.java b/src/dorkbox/network/pipeline/udp/KryoEncoderUdpCrypto.java index 8373e0f1..83f4ebae 100644 --- a/src/dorkbox/network/pipeline/udp/KryoEncoderUdpCrypto.java +++ b/src/dorkbox/network/pipeline/udp/KryoEncoderUdpCrypto.java @@ -17,10 +17,9 @@ package dorkbox.network.pipeline.udp; import java.io.IOException; -import dorkbox.network.connection.ConnectionImpl; +import dorkbox.network.connection.CryptoConnection; import dorkbox.network.serialization.CryptoSerializationManager; import io.netty.buffer.ByteBuf; -import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelHandlerContext; @@ -37,8 +36,8 @@ class KryoEncoderUdpCrypto extends KryoEncoderUdp { void writeObject(CryptoSerializationManager serializationManager, ChannelHandlerContext ctx, Object msg, ByteBuf buffer) throws IOException { - ChannelHandler last = ctx.pipeline() - .last(); - serializationManager.writeWithCrypto((ConnectionImpl) last, buffer, msg); + CryptoConnection last = (CryptoConnection) ctx.pipeline() + .last(); + serializationManager.writeWithCrypto(last, buffer, msg); } } diff --git a/src/dorkbox/network/rmi/RmiBridge.java b/src/dorkbox/network/rmi/RmiBridge.java index 860334ab..46a59a0c 100644 --- a/src/dorkbox/network/rmi/RmiBridge.java +++ b/src/dorkbox/network/rmi/RmiBridge.java @@ -293,8 +293,7 @@ class RmiBridge { } // System.err.println("Sending: " + invokeMethod.responseID); - connection.send() - .TCP(invokeMethodResult); + connection.send(invokeMethodResult).flush(); // logger.error("{} sent data: {} with id ({})", connection, result, invokeMethod.responseID); } diff --git a/src/dorkbox/network/rmi/RmiObjectLocalHandler.java b/src/dorkbox/network/rmi/RmiObjectLocalHandler.java index 51ce3b23..5b824c7e 100644 --- a/src/dorkbox/network/rmi/RmiObjectLocalHandler.java +++ b/src/dorkbox/network/rmi/RmiObjectLocalHandler.java @@ -149,7 +149,8 @@ class RmiObjectLocalHandler extends RmiObjectHandler { Class rmiImpl = serialization.getRmiImpl(registration.interfaceClass); RmiRegistration registrationResult = connection.createNewRmiObject(interfaceClass, rmiImpl, callbackId); - connection.TCP(registrationResult); + connection.send(registrationResult); + // connection transport is flushed in calling method (don't need to do it here) } // Check if we are getting an already existing REMOTE object. This check is always AFTER the check to create a new object @@ -158,8 +159,8 @@ class RmiObjectLocalHandler extends RmiObjectHandler { // // GET a LOCAL rmi object, if none get a specific, GLOBAL rmi object (objects that are not bound to a single connection). RmiRegistration registrationResult = connection.getExistingRmiObject(interfaceClass, registration.rmiId, callbackId); - - connection.TCP(registrationResult); + connection.send(registrationResult); + // connection transport is flushed in calling method (don't need to do it here) } } else { diff --git a/src/dorkbox/network/rmi/RmiObjectNetworkHandler.java b/src/dorkbox/network/rmi/RmiObjectNetworkHandler.java index bc51573d..34d31756 100644 --- a/src/dorkbox/network/rmi/RmiObjectNetworkHandler.java +++ b/src/dorkbox/network/rmi/RmiObjectNetworkHandler.java @@ -56,7 +56,8 @@ class RmiObjectNetworkHandler extends RmiObjectHandler { // For network connections, the interface class kryo ID == implementation class kryo ID, so they switch automatically. RmiRegistration registrationResult = connection.createNewRmiObject(interfaceClass, interfaceClass, callbackId); - connection.TCP(registrationResult); + connection.send(registrationResult); + // connection transport is flushed in calling method (don't need to do it here) } // Check if we are getting an already existing REMOTE object. This check is always AFTER the check to create a new object @@ -65,7 +66,8 @@ class RmiObjectNetworkHandler extends RmiObjectHandler { // // GET a LOCAL rmi object, if none get a specific, GLOBAL rmi object (objects that are not bound to a single connection). RmiRegistration registrationResult = connection.getExistingRmiObject(interfaceClass, registration.rmiId, callbackId); - connection.TCP(registrationResult); + connection.send(registrationResult); + // connection transport is flushed in calling method (don't need to do it here) } } else { diff --git a/src/dorkbox/network/rmi/RmiProxyHandler.java b/src/dorkbox/network/rmi/RmiProxyHandler.java index cbb0fa7b..3cc71c3a 100644 --- a/src/dorkbox/network/rmi/RmiProxyHandler.java +++ b/src/dorkbox/network/rmi/RmiProxyHandler.java @@ -46,7 +46,11 @@ import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import dorkbox.network.connection.*; +import dorkbox.network.connection.Connection; +import dorkbox.network.connection.ConnectionImpl; +import dorkbox.network.connection.EndPoint; +import dorkbox.network.connection.KryoExtra; +import dorkbox.network.connection.Listener; import dorkbox.network.serialization.RmiSerializationManager; /** @@ -275,10 +279,12 @@ class RmiProxyHandler implements InvocationHandler { // Sends our invokeMethod to the remote connection, which the RmiBridge listens for if (this.udp) { - this.connection.UDP(invokeMethod); + // flush is necessary in case this is called outside of a network worker thread + this.connection.UDP(invokeMethod).flush(); } else { - this.connection.TCP(invokeMethod); + // flush is necessary in case this is called outside of a network worker thread + this.connection.send(invokeMethod).flush(); } if (logger.isTraceEnabled()) { diff --git a/src/dorkbox/network/rmi/RmiUtils.java b/src/dorkbox/network/rmi/RmiUtils.java index f1074581..31f44283 100644 --- a/src/dorkbox/network/rmi/RmiUtils.java +++ b/src/dorkbox/network/rmi/RmiUtils.java @@ -99,6 +99,15 @@ class RmiUtils { } + /** + * @param logger + * @param kryo + * @param asmEnabled + * @param iFace this is never null. + * @param impl this is NULL on the rmi "client" side. This is NOT NULL on the "server" side (where the object lives) + * @param classId + * @return + */ public static CachedMethod[] getCachedMethods(final Logger logger, final Kryo kryo, final boolean asmEnabled, final Class iFace, final Class impl, final int classId) { MethodAccess ifaceMethodAccess = null; @@ -145,28 +154,30 @@ class RmiUtils { // copy because they can be overridden boolean overriddenMethod = false; - Method tweakMethod = method; MethodAccess tweakMethodAccess = ifaceMethodAccess; + // this is how we detect if the method has been changed from the interface -> implementation + connection parameter if (declaringClass.equals(impl)) { tweakMethodAccess = implMethodAccess; overriddenMethod = true; - if (logger.isTraceEnabled()) - - logger.trace("Overridden method: {}.{}", impl, method.getName()); + if (logger.isTraceEnabled()) { + logger.trace("Overridden method: {}.{}", impl, method.getName()); + } } + CachedMethod cachedMethod = null; - if (tweakMethodAccess != null) { + // reflectAsm doesn't like "Object" class methods... + if (tweakMethodAccess != null && method.getDeclaringClass() != Object.class) { try { - final int index = tweakMethodAccess.getIndex(tweakMethod.getName(), parameterTypes); + final int index = tweakMethodAccess.getIndex(method.getName(), parameterTypes); AsmCachedMethod asmCachedMethod = new AsmCachedMethod(); asmCachedMethod.methodAccessIndex = index; asmCachedMethod.methodAccess = tweakMethodAccess; - asmCachedMethod.name = tweakMethod.getName(); + asmCachedMethod.name = method.getName(); if (overriddenMethod) { // logger.error(tweakMethod.getName() + " " + Arrays.toString(parameterTypes) + " index: " + index + @@ -188,7 +199,7 @@ class RmiUtils { cachedMethod = asmCachedMethod; } catch (Exception e) { - logger.trace("Unable to use ReflectAsm for {}.{}", declaringClass, tweakMethod.getName(), e); + logger.trace("Unable to use ReflectAsm for {}.{} (using java reflection instead)", declaringClass, method.getName(), e); } } @@ -200,7 +211,7 @@ class RmiUtils { cachedMethod.methodClassID = classId; // we ALSO have to setup "normal" reflection access to these methods - cachedMethod.method = tweakMethod; + cachedMethod.method = method; cachedMethod.methodIndex = i; // Store the serializer for each final parameter. @@ -363,5 +374,4 @@ class RmiUtils { return methodsArray; } - } diff --git a/src/dorkbox/network/serialization/CryptoSerializationManager.java b/src/dorkbox/network/serialization/CryptoSerializationManager.java index 52f8c3e4..cf865faf 100644 --- a/src/dorkbox/network/serialization/CryptoSerializationManager.java +++ b/src/dorkbox/network/serialization/CryptoSerializationManager.java @@ -17,7 +17,7 @@ package dorkbox.network.serialization; import java.io.IOException; -import dorkbox.network.connection.ConnectionImpl; +import dorkbox.network.connection.CryptoConnection; import io.netty.buffer.ByteBuf; /** @@ -25,14 +25,14 @@ import io.netty.buffer.ByteBuf; * defeats the point of multi-threaded */ public -interface CryptoSerializationManager extends RmiSerializationManager { +interface CryptoSerializationManager extends RmiSerializationManager { /** * Waits until a kryo is available to write, using CAS operations to prevent having to synchronize. *

* There is a small speed penalty if there were no kryo's available to use. */ - void writeWithCrypto(ConnectionImpl connection, ByteBuf buffer, Object message) throws IOException; + void writeWithCrypto(C connection, ByteBuf buffer, Object message) throws IOException; /** * Reads an object from the buffer. @@ -44,5 +44,5 @@ interface CryptoSerializationManager extends RmiSerializationManager { * @param length * should ALWAYS be the length of the expected object! */ - Object readWithCrypto(ConnectionImpl connection, ByteBuf buffer, int length) throws IOException; + Object readWithCrypto(C connection, ByteBuf buffer, int length) throws IOException; } diff --git a/src/dorkbox/network/serialization/Serialization.java b/src/dorkbox/network/serialization/Serialization.java index 07b82f78..6160014f 100644 --- a/src/dorkbox/network/serialization/Serialization.java +++ b/src/dorkbox/network/serialization/Serialization.java @@ -43,7 +43,7 @@ import com.esotericsoftware.kryo.util.IntMap; import com.esotericsoftware.kryo.util.MapReferenceResolver; import com.esotericsoftware.kryo.util.Util; -import dorkbox.network.connection.ConnectionImpl; +import dorkbox.network.connection.CryptoConnection; import dorkbox.network.connection.KryoExtra; import dorkbox.network.connection.ping.PingMessage; import dorkbox.network.rmi.CachedMethod; @@ -64,6 +64,7 @@ import dorkbox.util.serialization.EccPublicKeySerializer; import dorkbox.util.serialization.IesParametersSerializer; import dorkbox.util.serialization.IesWithCipherParametersSerializer; import dorkbox.util.serialization.UnmodifiableCollectionsSerializer; +import io.netty.bootstrap.DatagramCloseMessage; import io.netty.buffer.ByteBuf; /** @@ -75,7 +76,7 @@ import io.netty.buffer.ByteBuf; */ @SuppressWarnings({"unused", "StaticNonFinalField"}) public -class Serialization implements CryptoSerializationManager, RmiSerializationManager { +class Serialization implements CryptoSerializationManager, RmiSerializationManager { private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(Serialization.class.getSimpleName()); @@ -182,17 +183,18 @@ class Serialization implements CryptoSerializationManager, RmiSerializationManag * Kryo#newDefaultSerializer(Class) */ public static - Serialization DEFAULT(final boolean references, + Serialization DEFAULT(final boolean references, final boolean registrationRequired, final boolean implementationRequired, final SerializerFactory factory) { - final Serialization serialization = new Serialization(references, + final Serialization serialization = new Serialization(references, registrationRequired, implementationRequired, factory); serialization.register(PingMessage.class); + serialization.register(DatagramCloseMessage.class); serialization.register(byte[].class); serialization.register(IESParameters.class, new IesParametersSerializer()); @@ -241,7 +243,7 @@ class Serialization implements CryptoSerializationManager, RmiSerializationManag } } private boolean initialized = false; - private final ObjectPool kryoPool; + private final ObjectPool> kryoPool; // used to determine if we should forbid interface registration OUTSIDE of RMI registration. private final boolean forbidInterfaceRegistration; @@ -311,13 +313,13 @@ class Serialization implements CryptoSerializationManager, RmiSerializationManag this.forbidInterfaceRegistration = implementationRequired; - this.kryoPool = ObjectPool.NonBlockingSoftReference(new PoolableObject() { + this.kryoPool = ObjectPool.NonBlockingSoftReference(new PoolableObject>() { @Override public - KryoExtra create() { + KryoExtra create() { synchronized (Serialization.this) { // we HAVE to pre-allocate the KRYOs - KryoExtra kryo = new KryoExtra(Serialization.this); + KryoExtra kryo = new KryoExtra(Serialization.this); kryo.getFieldSerializerConfig() .setUseAsm(useAsm); @@ -662,7 +664,7 @@ class Serialization implements CryptoSerializationManager, RmiSerializationManag @Override public final void write(final ByteBuf buffer, final Object message) throws IOException { - final KryoExtra kryo = kryoPool.take(); + final KryoExtra kryo = kryoPool.take(); try { kryo.write(buffer, message); } finally { @@ -680,7 +682,7 @@ class Serialization implements CryptoSerializationManager, RmiSerializationManag @Override public final Object read(final ByteBuf buffer, final int length) throws IOException { - final KryoExtra kryo = kryoPool.take(); + final KryoExtra kryo = kryoPool.take(); try { return kryo.read(buffer); } finally { @@ -694,7 +696,7 @@ class Serialization implements CryptoSerializationManager, RmiSerializationManag @Override public void writeFullClassAndObject(final Logger logger, final Output output, final Object value) throws IOException { - KryoExtra kryo = kryoPool.take(); + KryoExtra kryo = kryoPool.take(); boolean prev = false; try { @@ -720,7 +722,7 @@ class Serialization implements CryptoSerializationManager, RmiSerializationManag @Override public Object readFullClassAndObject(final Logger logger, final Input input) throws IOException { - KryoExtra kryo = kryoPool.take(); + KryoExtra kryo = kryoPool.take(); boolean prev = false; try { @@ -751,9 +753,9 @@ class Serialization implements CryptoSerializationManager, RmiSerializationManag // initialize the kryo pool with at least 1 kryo instance. This ALSO makes sure that all of our class registration is done // correctly and (if not) we are are notified on the initial thread (instead of on the network update thread) - KryoExtra kryo = null; + KryoExtra kryo = null; try { - kryo = takeKryo(); + kryo = kryoPool.take(); ClassResolver classResolver = kryo.getClassResolver(); @@ -819,8 +821,8 @@ class Serialization implements CryptoSerializationManager, RmiSerializationManag */ @Override public final - void writeWithCrypto(final ConnectionImpl connection, final ByteBuf buffer, final Object message) throws IOException { - final KryoExtra kryo = kryoPool.take(); + void writeWithCrypto(final C connection, final ByteBuf buffer, final Object message) throws IOException { + final KryoExtra kryo = kryoPool.take(); try { // we only need to encrypt when NOT on loopback, since encrypting on loopback is a waste of CPU if (connection.isLoopback()) { @@ -845,8 +847,8 @@ class Serialization implements CryptoSerializationManager, RmiSerializationManag @SuppressWarnings("Duplicates") @Override public final - Object readWithCrypto(final ConnectionImpl connection, final ByteBuf buffer, final int length) throws IOException { - final KryoExtra kryo = kryoPool.take(); + Object readWithCrypto(final C connection, final ByteBuf buffer, final int length) throws IOException { + final KryoExtra kryo = kryoPool.take(); try { // we only need to encrypt when NOT on loopback, since encrypting on loopback is a waste of CPU if (connection.isLoopback()) { diff --git a/src/io/netty/bootstrap/DatagramCloseMessage.java b/src/io/netty/bootstrap/DatagramCloseMessage.java new file mode 100644 index 00000000..b656f639 --- /dev/null +++ b/src/io/netty/bootstrap/DatagramCloseMessage.java @@ -0,0 +1,22 @@ +/* + * Copyright 2018 dorkbox, llc. + * + * 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 io.netty.bootstrap; + +/** + * + */ +public +class DatagramCloseMessage {} diff --git a/src/io/netty/bootstrap/SessionBootstrap.java b/src/io/netty/bootstrap/SessionBootstrap.java index f7ffddc9..3e1e3e18 100644 --- a/src/io/netty/bootstrap/SessionBootstrap.java +++ b/src/io/netty/bootstrap/SessionBootstrap.java @@ -59,11 +59,18 @@ class SessionBootstrap extends AbstractBootstrap { private final SessionBootstrapConfig config = new SessionBootstrapConfig(this); private volatile EventLoopGroup childGroup; private volatile ChannelHandler childHandler; + @SuppressWarnings("unchecked") private volatile AddressResolverGroup resolver = (AddressResolverGroup) DEFAULT_RESOLVER; + private final int tcpPort; + private final int udpPort; + public - SessionBootstrap() { } + SessionBootstrap(final int tcpPort, final int udpPort) { + this.tcpPort = tcpPort; + this.udpPort = udpPort; + } private SessionBootstrap(SessionBootstrap bootstrap) { @@ -80,6 +87,9 @@ class SessionBootstrap extends AbstractBootstrap { synchronized (bootstrap.childAttrs) { childAttrs.putAll(bootstrap.childAttrs); } + + this.tcpPort = bootstrap.tcpPort; + this.udpPort = bootstrap.udpPort; } /** @@ -251,8 +261,8 @@ class SessionBootstrap extends AbstractBootstrap { @Override public void run() { - pipeline.addLast(new SessionManager(ch, - currentChildGroup, + pipeline.addLast(new SessionManager(tcpPort, udpPort, + ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs)); diff --git a/src/io/netty/bootstrap/SessionManager.java b/src/io/netty/bootstrap/SessionManager.java index 4aa115ee..b76df2c1 100644 --- a/src/io/netty/bootstrap/SessionManager.java +++ b/src/io/netty/bootstrap/SessionManager.java @@ -76,7 +76,7 @@ class SessionManager extends ChannelInboundHandlerAdapter { return Long_.from(combined); } - private final BroadcastServer broadcastServer = new BroadcastServer(); + private final BroadcastServer broadcastServer; // Does not need to be thread safe, because access only happens in the event loop private final LongObjectHashMap datagramChannels = new LongObjectHashMap(); @@ -91,7 +91,8 @@ class SessionManager extends ChannelInboundHandlerAdapter { private final DatagramSessionChannelConfig sessionConfig; - SessionManager(final Channel channel, + SessionManager(final int tcpPort, final int udpPort, + final Channel channel, EventLoopGroup childGroup, ChannelHandler childHandler, Entry, Object>[] childOptions, @@ -117,6 +118,8 @@ class SessionManager extends ChannelInboundHandlerAdapter { .setAutoRead(true); } }; + + broadcastServer = new BroadcastServer(tcpPort, udpPort); } @Override @@ -148,7 +151,7 @@ class SessionManager extends ChannelInboundHandlerAdapter { InetSocketAddress remoteAddress = packet.sender(); // check to see if it's a broadcast packet or not - if (broadcastServer.isBroadcast(channel, content, localAddress, remoteAddress)) { + if (broadcastServer.isDiscoveryRequest(channel, content, localAddress, remoteAddress)) { // don't bother creating channels if this is a broadcast event. Just respond and be finished return; } diff --git a/test/dorkbox/network/BaseTest.java b/test/dorkbox/network/BaseTest.java index 73c8f947..2b331760 100644 --- a/test/dorkbox/network/BaseTest.java +++ b/test/dorkbox/network/BaseTest.java @@ -43,7 +43,7 @@ import io.netty.util.ResourceLeakDetector; public abstract class BaseTest { - public static final String host = "localhost"; + public static final String host = "127.0.0.1"; public static final int tcpPort = 54558; public static final int udpPort = 54779; @@ -75,8 +75,9 @@ class BaseTest { // rootLogger.setLevel(Level.OFF); - rootLogger.setLevel(Level.DEBUG); -// rootLogger.setLevel(Level.TRACE); + // rootLogger.setLevel(Level.INFO); + // rootLogger.setLevel(Level.DEBUG); + rootLogger.setLevel(Level.TRACE); // rootLogger.setLevel(Level.ALL); @@ -116,25 +117,19 @@ class BaseTest { */ public void stopEndPoints() { - stopEndPoints(1); + stopEndPoints(0); } public void stopEndPoints(final int stopAfterMillis) { - final String name = Thread.currentThread().getThreadGroup() - .getName(); + ThreadGroup threadGroup = Thread.currentThread() + .getThreadGroup(); + final String name = threadGroup.getName(); - // no need to run inside another thread if we are not inside the client/server thread - if (!name.contains(THREADGROUP_NAME)) { - stopEndPoints_outsideThread(); - return; - } - - - if (stopAfterMillis > 0) { + if (name.contains(THREADGROUP_NAME)) { // We have to ALWAYS run this in a new thread, BECAUSE if stopEndPoints() is called from a client/server thread, it will // DEADLOCK - final Thread thread = new Thread(getThreadGroup(), new Runnable() { + final Thread thread = new Thread(threadGroup.getParent(), new Runnable() { @Override public void run() { @@ -143,106 +138,98 @@ class BaseTest { // ARE NOT in the same thread group as netty! Thread.sleep(stopAfterMillis); - stopEndPoints_outsideThread(); - } catch (InterruptedException e) { - e.printStackTrace(); + stopEndPoints(stopAfterMillis); + } catch (InterruptedException ignored) { } } }, "UnitTest shutdown"); thread.setDaemon(true); thread.start(); - } - } + } else { + synchronized (this.endPointConnections) { + for (EndPoint endPointConnection : this.endPointConnections) { + endPointConnection.stop(); + endPointConnection.waitForShutdown(); + } - private - void stopEndPoints_outsideThread() { - synchronized (BaseTest.this.endPointConnections) { - for (EndPoint endPointConnection : BaseTest.this.endPointConnections) { - endPointConnection.stop(); - endPointConnection.waitForShutdown(); + this.endPointConnections.clear(); + this.endPointConnections.notifyAll(); } - BaseTest.this.endPointConnections.clear(); } } - - private - ThreadGroup getThreadGroup() { - ThreadGroup threadGroup = Thread.currentThread() - .getThreadGroup(); - final String name = threadGroup.getName(); - if (name.contains(THREADGROUP_NAME)) { - threadGroup = threadGroup.getParent(); - } - return threadGroup; - } - - public - void waitForThreads(int stopAfterSecondsOrMillis) { - if (stopAfterSecondsOrMillis < 1000) { - stopAfterSecondsOrMillis *= 1000; - } - stopEndPoints(stopAfterSecondsOrMillis); - waitForThreads0(stopAfterSecondsOrMillis); - } - /** - * Wait for threads until they are done (no timeout) + * Wait for network client/server threads to shutdown on their own, BUT WILL ERROR (+ shutdown) if they take longer than 2 minutes. */ public void waitForThreads() { - waitForThreads0(0); + waitForThreads(0); } - private - void waitForThreads0(final int stopAfterMillis) { + /** + * Wait for network client/server threads to shutdown for the specified time. + * + * @param stopAfterSeconds how many seconds to wait + */ + public + void waitForThreads(int stopAfterSeconds) { + final int stopAfterMillis = stopAfterSeconds * 1000; // this must be in milliseconds + this.fail_check = false; - Thread thread = null; + synchronized (this.endPointConnections) { + Thread thread = null; + if (!this.endPointConnections.isEmpty()) { + // make sure to run this thread in the MAIN thread group.. + ThreadGroup threadGroup = Thread.currentThread() + .getThreadGroup(); + if (threadGroup.getName() + .contains(THREADGROUP_NAME)) { + threadGroup = threadGroup.getParent(); + } - if (stopAfterMillis > 0L) { - stopEndPoints(stopAfterMillis); - - // We have to ALWAYS run this in a new thread, BECAUSE if stopEndPoints() is called from a client/server thread, it will - // DEADLOCK - thread = new Thread(getThreadGroup(), new Runnable() { - @Override - public - void run() { - try { + thread = new Thread(threadGroup, new Runnable() { + @Override + public + void run() { // not the best, but this works for our purposes. This is a TAD hacky, because we ALSO have to make sure that we // ARE NOT in the same thread group as netty! - Thread.sleep(stopAfterMillis + 120000L); // test must run in 2 minutes or it fails + try { + if (stopAfterMillis > 0L) { + // if we specify a time, then we stop, otherwise we wait the timeout. + Thread.sleep(stopAfterMillis); + } + else { + Thread.sleep(120 * 1000L); // wait minimum of 2 minutes before we automatically fail the unit test. + } - BaseTest.this.fail_check = true; - } catch (InterruptedException ignored) { + System.err.println("Test did not complete in a timely manner..."); + BaseTest.this.fail_check = true; + stopEndPoints(); + } catch (InterruptedException ignored) { + } } - } - }, "UnitTest timeout"); + }, "UnitTest timeout fail condition"); + thread.setDaemon(true); + thread.start(); + } - thread.setDaemon(true); - thread.start(); - } - - while (true) { - synchronized (this.endPointConnections) { - if (this.endPointConnections.isEmpty()) { - break; + while (!this.endPointConnections.isEmpty()) { + try { + this.endPointConnections.wait(stopAfterMillis); + } catch (InterruptedException e) { + e.printStackTrace(); } } - try { - Thread.sleep(100); - } catch (InterruptedException ignored) { + + if (thread != null) { + thread.interrupt(); } - } - if (this.fail_check) { - fail("Test did not complete in a timely manner."); - } - - if (thread != null) { - thread.interrupt(); + if (this.fail_check) { + fail("Test did not complete in a timely manner."); + } } // Give sockets a chance to close before starting the next test. diff --git a/test/dorkbox/network/ChunkedDataIdleTest.java b/test/dorkbox/network/ChunkedDataIdleTest.java index 4846ce50..a0662d71 100644 --- a/test/dorkbox/network/ChunkedDataIdleTest.java +++ b/test/dorkbox/network/ChunkedDataIdleTest.java @@ -63,13 +63,32 @@ public class ChunkedDataIdleTest extends BaseTest { // have to test sending objects @Test - public void SendUdp() throws SecurityException, IOException { + public + void SendUdp() throws SecurityException, IOException { final Data mainData = new Data(); populateData(mainData); System.err.println("-- UDP"); Configuration configuration = new Configuration(); + // configuration.tcpPort = tcpPort; + configuration.udpPort = udpPort; + configuration.host = host; + configuration.serialization = Serialization.DEFAULT(); + register(configuration.serialization); + + sendObject(mainData, configuration, ConnectionType.UDP); + } + + // have to test sending objects + @Test + public void SendTcpUdp_Udp() throws SecurityException, IOException { + final Data mainData = new Data(); + populateData(mainData); + + + System.err.println("-- UDP (with TCP connection alive)"); + Configuration configuration = new Configuration(); configuration.tcpPort = tcpPort; configuration.udpPort = udpPort; configuration.host = host; @@ -81,7 +100,7 @@ public class ChunkedDataIdleTest extends BaseTest { // have to test sending objects @Test - public void SendTcpAndUdp() throws SecurityException, IOException { + public void SendTcpUdp_Tcp() throws SecurityException, IOException { final Data mainData = new Data(); populateData(mainData); @@ -95,7 +114,7 @@ public class ChunkedDataIdleTest extends BaseTest { configuration.serialization = Serialization.DEFAULT(); register(configuration.serialization); - sendObject(mainData, configuration, ConnectionType.UDP); + sendObject(mainData, configuration, ConnectionType.TCP); } diff --git a/test/dorkbox/network/ConnectionTest.java b/test/dorkbox/network/ConnectionTest.java index e567f8d7..c6ab92ef 100644 --- a/test/dorkbox/network/ConnectionTest.java +++ b/test/dorkbox/network/ConnectionTest.java @@ -28,19 +28,21 @@ import org.junit.Test; import dorkbox.network.connection.Connection; import dorkbox.network.connection.EndPoint; import dorkbox.network.connection.Listener; +import dorkbox.network.connection.Listener.OnConnected; +import dorkbox.network.connection.Listener.OnDisconnected; import dorkbox.network.serialization.Serialization; import dorkbox.util.exceptions.SecurityException; import dorkbox.util.serialization.SerializationManager; public class ConnectionTest extends BaseTest { - private AtomicInteger succesCount; + private AtomicInteger successCount; @Test public void connectLocal() throws SecurityException, IOException { System.out.println("---- " + "Local"); - succesCount = new AtomicInteger(0); + successCount = new AtomicInteger(0); Configuration configuration = new Configuration(); configuration.localChannelName = EndPoint.LOCAL_CHANNEL; @@ -51,14 +53,14 @@ class ConnectionTest extends BaseTest { startClient(configuration); waitForThreads(10); - Assert.assertEquals(3, succesCount.get()); + Assert.assertEquals(6, successCount.get()); } @Test public void connectTcp() throws SecurityException, IOException { System.out.println("---- " + "TCP"); - succesCount = new AtomicInteger(0); + successCount = new AtomicInteger(0); Configuration configuration = new Configuration(); configuration.tcpPort = tcpPort; @@ -71,14 +73,14 @@ class ConnectionTest extends BaseTest { startClient(configuration); waitForThreads(10); - Assert.assertEquals(3, succesCount.get()); + Assert.assertEquals(6, successCount.get()); } @Test public void connectUdp() throws SecurityException, IOException { System.out.println("---- " + "UDP"); - succesCount = new AtomicInteger(0); + successCount = new AtomicInteger(0); Configuration configuration = new Configuration(); configuration.udpPort = udpPort; @@ -91,14 +93,14 @@ class ConnectionTest extends BaseTest { startClient(configuration); waitForThreads(10); - Assert.assertEquals(3, succesCount.get()); + Assert.assertEquals(6, successCount.get()); } @Test public void connectTcpUdp() throws SecurityException, IOException { System.out.println("---- " + "TCP UDP"); - succesCount = new AtomicInteger(0); + successCount = new AtomicInteger(0); Configuration configuration = new Configuration(); configuration.tcpPort = tcpPort; @@ -112,35 +114,45 @@ class ConnectionTest extends BaseTest { startClient(configuration); waitForThreads(10); - Assert.assertEquals(3, succesCount.get()); + Assert.assertEquals(6, successCount.get()); } private - Server startServer(Configuration configuration) throws SecurityException { - Server server = new Server(configuration); + Server startServer(final Configuration configuration) throws SecurityException { + final Server server = new Server(configuration); addEndPoint(server); server.bind(false); server.listeners() - .add(new Listener.OnConnected() { + .add(new OnConnected() { @Override public void connected(final Connection connection) { - succesCount.getAndIncrement(); + successCount.getAndIncrement(); } - }); - - server.listeners() + }) + .add(new OnDisconnected() { + @Override + public + void disconnected(Connection connection) { + successCount.getAndIncrement(); + } + }) .add(new Listener.OnMessageReceived() { @Override public void received(Connection connection, Object message) { System.err.println("Received message from client: " + message.getClass().getSimpleName()); - succesCount.getAndIncrement(); - connection.send() - .UDP(message); - connection.close(); + successCount.getAndIncrement(); + if (configuration.tcpPort > 0) { + connection.send() + .TCP(message); + } + else { + connection.send() + .UDP(message); + } } }); @@ -148,7 +160,7 @@ class ConnectionTest extends BaseTest { } private - Client startClient(Configuration configuration) throws SecurityException, IOException { + Client startClient(final Configuration configuration) throws SecurityException, IOException { Client client; if (configuration != null) { client = new Client(configuration); @@ -159,12 +171,18 @@ class ConnectionTest extends BaseTest { addEndPoint(client); client.listeners() - .add(new Listener.OnDisconnected() { + .add(new OnConnected() { + @Override + public + void connected(final Connection connection) { + successCount.getAndIncrement(); + } + }) + .add(new OnDisconnected() { @Override public void disconnected(Connection connection) { - succesCount.getAndIncrement(); - stopEndPoints(); + successCount.getAndIncrement(); } }) .add(new Listener.OnMessageReceived() { @@ -173,18 +191,23 @@ class ConnectionTest extends BaseTest { void received(Connection connection, Object message) { System.err.println("Received message from server: " + message.getClass() .getSimpleName()); - System.out.println("Now disconnecting!"); - succesCount.getAndIncrement(); - connection.close(); + System.err.println("Now disconnecting!"); + successCount.getAndIncrement(); + + stopEndPoints(); } }); client.connect(5000); - client.send() - .UDP(new BMessage()); - if (true) { - throw new RuntimeException("wreha?"); + if (configuration.tcpPort > 0) { + client.send() + .TCP(new BMessage()); } + else { + client.send() + .UDP(new BMessage()); + } + return client; } diff --git a/test/dorkbox/network/DisconnectReconnectTest.java b/test/dorkbox/network/DisconnectReconnectTest.java index 0c8842d6..41a164f9 100644 --- a/test/dorkbox/network/DisconnectReconnectTest.java +++ b/test/dorkbox/network/DisconnectReconnectTest.java @@ -30,11 +30,11 @@ import dorkbox.util.exceptions.SecurityException; public class DisconnectReconnectTest extends BaseTest { + private final Timer timer = new Timer(); @Test public void reconnect() throws SecurityException, IOException { - final Timer timer = new Timer(); Configuration configuration = new Configuration(); configuration.tcpPort = tcpPort; @@ -51,11 +51,12 @@ class DisconnectReconnectTest extends BaseTest { @Override public void connected(final Connection connection) { + System.out.println("Disconnecting after 2 seconds."); timer.schedule(new TimerTask() { @Override public void run() { - System.out.println("Disconnecting after 2 seconds."); + System.out.println("Disconnecting...."); connection.close(); } }, 2000); @@ -72,24 +73,26 @@ class DisconnectReconnectTest extends BaseTest { @Override public void disconnected(Connection connection) { - if (reconnectCount.getAndIncrement() == 2) { + int count = reconnectCount.getAndIncrement(); + if (count == 3) { + System.out.println("Shutting down"); stopEndPoints(); - return; } - - System.out.println("Reconnecting: " + reconnectCount.get()); - try { - client.reconnect(); - } catch (IOException e) { - e.printStackTrace(); + else { + System.out.println("Reconnecting: " + count); + try { + client.reconnect(); + } catch (IOException e) { + e.printStackTrace(); + } } - } }); client.connect(5000); waitForThreads(); + System.err.println("Connection count (after reconnecting) is: " + reconnectCount.get()); - assertEquals(3, reconnectCount.get()); + assertEquals(4, reconnectCount.get()); } } diff --git a/test/dorkbox/network/DiscoverHostTest.java b/test/dorkbox/network/DiscoverHostTest.java index 31c2603d..aeb452ef 100644 --- a/test/dorkbox/network/DiscoverHostTest.java +++ b/test/dorkbox/network/DiscoverHostTest.java @@ -27,6 +27,7 @@ import org.junit.Test; import dorkbox.network.connection.Connection; import dorkbox.network.connection.Listener; +import dorkbox.network.pipeline.discovery.BroadcastResponse; import dorkbox.util.exceptions.SecurityException; public @@ -48,7 +49,7 @@ class DiscoverHostTest extends BaseTest { // ---- - String host = Broadcast.discoverHost(udpPort, 2000); + BroadcastResponse host = Broadcast.discoverHost(udpPort, 2000); if (host == null) { stopEndPoints(); fail("No servers found. Maybe you are behind a VPN service or your network is mis-configured?"); @@ -77,7 +78,7 @@ class DiscoverHostTest extends BaseTest { }); client.connect(2000); - waitForThreads(2); + waitForThreads(20); if (!this.connected) { fail("Unable to connect to server."); diff --git a/test/dorkbox/network/MultipleThreadTest.java b/test/dorkbox/network/MultipleThreadTest.java index 7f4f8d31..26764e3b 100644 --- a/test/dorkbox/network/MultipleThreadTest.java +++ b/test/dorkbox/network/MultipleThreadTest.java @@ -39,14 +39,19 @@ import dorkbox.util.exceptions.SecurityException; public class MultipleThreadTest extends BaseTest { private final Object lock = new Object(); + private volatile boolean stillRunning = false; + + private final Object finalRunLock = new Object(); + private volatile boolean finalStillRunning = false; + private final int messageCount = 150; private final int threadCount = 15; private final int clientCount = 13; private final List clients = new ArrayList(this.clientCount); - int perClientReceiveTotal = (MultipleThreadTest.this.messageCount * MultipleThreadTest.this.threadCount); - int serverReceiveTotal = perClientReceiveTotal * MultipleThreadTest.this.clientCount; + int perClientReceiveTotal = (this.messageCount * this.threadCount); + int serverReceiveTotal = perClientReceiveTotal * this.clientCount; AtomicInteger sent = new AtomicInteger(0); AtomicInteger totalClientReceived = new AtomicInteger(0); @@ -57,6 +62,14 @@ class MultipleThreadTest extends BaseTest { @Test public void multipleThreads() throws SecurityException, IOException { + // our clients should receive messageCount * threadCount * clientCount TOTAL messages + final int totalClientReceivedCountExpected = this.clientCount * this.messageCount * this.threadCount; + final int totalServerReceivedCountExpected = this.clientCount * this.messageCount; + + System.err.println("CLIENT RECEIVES: " + totalClientReceivedCountExpected); + System.err.println("SERVER RECEIVES: " + totalServerReceivedCountExpected); + + Configuration configuration = new Configuration(); configuration.tcpPort = tcpPort; configuration.host = host; @@ -66,6 +79,7 @@ class MultipleThreadTest extends BaseTest { final Server server = new Server(configuration); + server.disableRemoteKeyValidation(); addEndPoint(server); server.bind(false); @@ -94,8 +108,10 @@ class MultipleThreadTest extends BaseTest { //System.err.println(dataClass.data); MultipleThreadTest.this.sentStringsToClientDebug.put(incrementAndGet, dataClass); connection.send() - .TCP(dataClass); + .TCP(dataClass) + .flush(); } + } }.start(); } @@ -107,20 +123,31 @@ class MultipleThreadTest extends BaseTest { public void received(Connection connection, DataClass object) { int incrementAndGet = MultipleThreadTest.this.receivedServer.getAndIncrement(); - //System.err.println("server #" + incrementAndGet); - if (incrementAndGet == serverReceiveTotal) { - System.err.println("Server DONE " + incrementAndGet); - stopEndPoints(); + + + if (incrementAndGet % MultipleThreadTest.this.messageCount == 0) { + System.err.println("Server receive DONE for client " + incrementAndGet); + + stillRunning = false; synchronized (MultipleThreadTest.this.lock) { MultipleThreadTest.this.lock.notifyAll(); } } + + if (incrementAndGet == totalServerReceivedCountExpected) { + System.err.println("Server DONE: " + incrementAndGet); + + finalStillRunning = false; + synchronized (MultipleThreadTest.this.finalRunLock) { + MultipleThreadTest.this.finalRunLock.notifyAll(); + } + } } }); // ---- - + finalStillRunning = true; for (int i = 1; i <= this.clientCount; i++) { final int index = i; @@ -154,27 +181,36 @@ class MultipleThreadTest extends BaseTest { } } }); + + + stillRunning = true; + client.connect(5000); + + while (stillRunning) { + synchronized (this.lock) { + try { + this.lock.wait(5 * 1000); // 5 secs + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + } + + while (finalStillRunning) { + synchronized (this.finalRunLock) { + try { + this.finalRunLock.wait(5 * 1000); // 5 secs + } catch (InterruptedException e) { + e.printStackTrace(); + } + } } // CLIENT will wait until it's done connecting, but SERVER is async. // the ONLY way to safely work in the server is with LISTENERS. Everything else can FAIL, because of it's async nature. - // our clients should receive messageCount * threadCount * clientCount TOTAL messages - int totalClientReceivedCountExpected = this.threadCount * this.clientCount * this.messageCount; - int totalServerReceivedCountExpected = this.clientCount * this.messageCount; - - System.err.println("CLIENT RECEIVES: " + totalClientReceivedCountExpected); - System.err.println("SERVER RECEIVES: " + totalServerReceivedCountExpected); - - synchronized (this.lock) { - try { - this.lock.wait(5 * 1000); // 5 secs - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - if (!this.sentStringsToClientDebug.isEmpty()) { System.err.println("MISSED DATA: " + this.sentStringsToClientDebug.size()); for (Map.Entry i : this.sentStringsToClientDebug.entrySet()) { @@ -184,7 +220,9 @@ class MultipleThreadTest extends BaseTest { stopEndPoints(); assertEquals(totalClientReceivedCountExpected, totalClientReceived.get()); - assertEquals(totalServerReceivedCountExpected, this.receivedServer.get() - 1); // offset by 1 since we start at 1. + + // offset by 1 since we start at 1 + assertEquals(totalServerReceivedCountExpected, receivedServer.get()-1); } diff --git a/test/dorkbox/network/ReconnectTest.java b/test/dorkbox/network/ReconnectTest.java index 64237451..37f1b612 100644 --- a/test/dorkbox/network/ReconnectTest.java +++ b/test/dorkbox/network/ReconnectTest.java @@ -33,7 +33,7 @@ import dorkbox.util.exceptions.SecurityException; public class ReconnectTest extends BaseTest { - AtomicInteger receivedCount; + private final AtomicInteger receivedCount = new AtomicInteger(0); @Test public @@ -55,7 +55,7 @@ class ReconnectTest extends BaseTest { private void socketReuse(final boolean useTCP, final boolean useUDP) throws SecurityException, IOException { - this.receivedCount = new AtomicInteger(0); + receivedCount.set(0); Configuration configuration = new Configuration(); configuration.host = host; @@ -92,6 +92,10 @@ class ReconnectTest extends BaseTest { void received(Connection connection, String object) { int incrementAndGet = ReconnectTest.this.receivedCount.incrementAndGet(); System.out.println("----- " + incrementAndGet + " : " + object); + + synchronized (receivedCount) { + receivedCount.notifyAll(); + } } }); @@ -120,32 +124,45 @@ class ReconnectTest extends BaseTest { void received(Connection connection, String object) { int incrementAndGet = ReconnectTest.this.receivedCount.incrementAndGet(); System.out.println("----- " + incrementAndGet + " : " + object); + + synchronized (receivedCount) { + receivedCount.notifyAll(); + } } }); server.bind(false); - int count = 10; + int count = 100; int initialCount = 2; if (useTCP && useUDP) { initialCount += 2; } for (int i = 1; i < count + 1; i++) { + System.out.println("....."); client.connect(5000); - int waitingRetryCount = 10; + + int waitingRetryCount = 20; int target = i * initialCount; - while (this.receivedCount.get() != target) { - if (waitingRetryCount-- < 0) { - throw new IOException("Invalid target count..."); - } - try { - Thread.sleep(100); - } catch (InterruptedException ignored) { + + synchronized (receivedCount) { + while (this.receivedCount.get() != target) { + if (waitingRetryCount-- < 0) { + System.out.println("Aborting..."); + stopEndPoints(); + assertEquals(target, this.receivedCount.get()); + } + try { + receivedCount.wait(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } } } - client.closeConnections(); + client.close(); + System.out.println("....."); } assertEquals(count * initialCount, this.receivedCount.get()); @@ -157,7 +174,7 @@ class ReconnectTest extends BaseTest { @Test public void localReuse() throws SecurityException, IOException { - this.receivedCount = new AtomicInteger(0); + receivedCount.set(0); Server server = new Server(); addEndPoint(server); @@ -167,7 +184,7 @@ class ReconnectTest extends BaseTest { public void connected(Connection connection) { connection.send() - .TCP("-- LOCAL from server"); + .self("-- LOCAL from server"); } }); server.listeners() @@ -190,7 +207,7 @@ class ReconnectTest extends BaseTest { public void connected(Connection connection) { connection.send() - .TCP("-- LOCAL from client"); + .self("-- LOCAL from client"); } }); @@ -209,16 +226,16 @@ class ReconnectTest extends BaseTest { for (int i = 1; i < count + 1; i++) { client.connect(5000); - int target = i; + int target = i * 2; while (this.receivedCount.get() != target) { System.out.println("----- Waiting..."); try { Thread.sleep(100); - } catch (InterruptedException ex) { + } catch (InterruptedException ignored) { } } - client.closeConnections(); + client.close(); } assertEquals(count * 2, this.receivedCount.get()); diff --git a/test/dorkbox/network/rmi/RmiGlobalTest.java b/test/dorkbox/network/rmi/RmiGlobalTest.java index 2576f4d7..17c1cf72 100644 --- a/test/dorkbox/network/rmi/RmiGlobalTest.java +++ b/test/dorkbox/network/rmi/RmiGlobalTest.java @@ -34,7 +34,9 @@ */ package dorkbox.network.rmi; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import java.io.IOException; @@ -166,12 +168,14 @@ class RmiGlobalTest extends BaseTest { // Test sending a reference to a remote object (the receiving end should receive the IMPL object, not the proxy object) + System.out.println("Sending proxied object to remote..."); MessageWithTestCow m = new MessageWithTestCow(test); m.number = 678; m.text = "sometext"; connection.send() - .TCP(m); + .TCP(m) + .flush(); } diff --git a/test/dorkbox/network/rmi/RmiSendObjectOverrideMethodTest.java b/test/dorkbox/network/rmi/RmiSendObjectOverrideMethodTest.java index d7f28a50..d98d0135 100644 --- a/test/dorkbox/network/rmi/RmiSendObjectOverrideMethodTest.java +++ b/test/dorkbox/network/rmi/RmiSendObjectOverrideMethodTest.java @@ -30,6 +30,7 @@ import dorkbox.network.Server; import dorkbox.network.connection.Connection; import dorkbox.network.connection.EndPoint; import dorkbox.network.connection.Listener; +import dorkbox.network.connection.bridge.ConnectionBridge; import dorkbox.network.serialization.Serialization; import dorkbox.util.exceptions.SecurityException; @@ -39,7 +40,7 @@ class RmiSendObjectOverrideMethodTest extends BaseTest { @Test public - void rmiNetwork() throws SecurityException, IOException { + void rmiTcp() throws SecurityException, IOException { rmi(new Config() { @Override public @@ -50,6 +51,19 @@ class RmiSendObjectOverrideMethodTest extends BaseTest { }); } + @Test + public + void rmiUdp() throws SecurityException, IOException { + rmi(new Config() { + @Override + public + void apply(final Configuration configuration) { + configuration.udpPort = udpPort; + configuration.host = host; + } + }); + } + @Test public void rmiLocal() throws SecurityException, IOException { @@ -90,6 +104,8 @@ class RmiSendObjectOverrideMethodTest extends BaseTest { Configuration configuration = new Configuration(); config.apply(configuration); + final boolean isUDP = configuration.udpPort > 0; + configuration.serialization = Serialization.DEFAULT(); configuration.serialization.registerRmiImplementation(TestObject.class, TestObjectImpl.class); configuration.serialization.registerRmiImplementation(OtherObject.class, OtherObjectImpl.class); @@ -164,8 +180,15 @@ class RmiSendObjectOverrideMethodTest extends BaseTest { // When a proxy object is sent, the other side receives its ACTUAL object (not a proxy of it), because // that is where that object acutally exists. // we have to manually flush, since we are in a separate thread that does not auto-flush. - connection.send() - .TCP(otherObject); + ConnectionBridge send = connection.send(); + + if (isUDP) { + send.UDP(otherObject) + .flush(); + } else { + send.TCP(otherObject) + .flush(); + } } }.start(); } diff --git a/test/dorkbox/network/rmi/RmiSendObjectTest.java b/test/dorkbox/network/rmi/RmiSendObjectTest.java index 1660be4d..59d0fbc2 100644 --- a/test/dorkbox/network/rmi/RmiSendObjectTest.java +++ b/test/dorkbox/network/rmi/RmiSendObjectTest.java @@ -163,7 +163,8 @@ class RmiSendObjectTest extends BaseTest { // When a remote proxy object is sent, the other side receives its actual remote object. // we have to manually flush, since we are in a separate thread that does not auto-flush. connection.send() - .TCP(otherObject); + .TCP(otherObject) + .flush(); } }.start(); } diff --git a/test/dorkbox/network/rmi/RmiTest.java b/test/dorkbox/network/rmi/RmiTest.java index 9915f745..274dff82 100644 --- a/test/dorkbox/network/rmi/RmiTest.java +++ b/test/dorkbox/network/rmi/RmiTest.java @@ -34,7 +34,9 @@ */ package dorkbox.network.rmi; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import java.io.IOException; @@ -160,7 +162,8 @@ class RmiTest extends BaseTest { m.number = 678; m.text = "sometext"; connection.send() - .TCP(m); + .TCP(m) + .flush(); System.out.println("Finished tests"); }