diff --git a/CHANGELOG.md b/CHANGELOG.md index dfe9aed..3151604 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ #### v0.1.0 -SSL Support for TCP channel + * SSL Support for TCP channel (#5, #6) + * Optimize network message transport (#4, #11) + * Better logging and enhanced test examples (#9) + * Better error reporting on non-serializable objects in a message (#14, #16) + * Separation of examples and library, and corresponding dependencies (#22) + * Fixed issue where messages would be transferred before connection listeners were notified (#20, #21) + * Fixed issue where dropped sockets would not be detected (#10, #23) #### V0.0.0 Initial Release \ No newline at end of file diff --git a/README.md b/README.md index a9be3b3..050b702 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,56 @@ # monkey-netty ![Build](https://github.com/tlf30/monkey-netty/workflows/Java%20CI%20with%20Gradle/badge.svg) -An implementation of a server-client communication system for jMonkeyEngine using Netty.IO that utilizes both TCP and UDP communication. -See example for server and client in `examples` module +An implementation of a server-client communication system for jMonkeyEngine using Netty.IO that utilizes both TCP and UDP communication. + +**Checkout our [Wiki](https://github.com/tlf30/monkey-netty/wiki) for getting started.** + +**See example for server and client in `examples` module.** + +## Installing with Gradle +In your `build.gradle` you will need to: + +1. Include the github repo: +```groovy +repositories { + ... + maven { + url = 'https://maven.pkg.github.com/tlf30/monkey-netty' + } +} +``` + +2. Specify the dependency: +```groovy +dependencies { + ... + implementation 'io.tlf.monkeynetty:monkey-netty:0.1.0' +} +``` + +## Installing with Maven +In your pom.xml you will need to: + +1. Include the github repo: +```xml + + ... + + monkey-netty + Monkey-Netty GitHub Packages + https://maven.pkg.github.com/tlf30/monkey-netty + + +``` + +2. Specify the dependency: +```xml + + ... + + io.tlf.monkeynetty + monkey-netty + 0.1.0 + + +``` + diff --git a/build.gradle b/build.gradle index ff929ed..e5df123 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ allprojects { - version = '0.1.0-SNAPSHOT' + version = '0.1.0' group = "io.tlf.monkeynetty" ext { diff --git a/examples/src/main/java/io/tlf/monkeynetty/test/JmeClient.java b/examples/src/main/java/io/tlf/monkeynetty/test/JmeClient.java index 16193ba..4030ae5 100644 --- a/examples/src/main/java/io/tlf/monkeynetty/test/JmeClient.java +++ b/examples/src/main/java/io/tlf/monkeynetty/test/JmeClient.java @@ -24,6 +24,7 @@ of this software and associated documentation files (the "Software"), to deal package io.tlf.monkeynetty.test; +import io.netty.handler.logging.LogLevel; import io.tlf.monkeynetty.ConnectionListener; import io.tlf.monkeynetty.test.messages.TestUDPBigMessageA; import io.tlf.monkeynetty.test.messages.TestTCPBigMessageA; @@ -50,8 +51,9 @@ public class JmeClient extends SimpleApplication { @Override public void simpleInitApp() { - client = new NettyClient("test", true, 10000, "localhost"); + client = new NettyClient("test", true, 10000, "localhost"); stateManager.attach(client); + client.setLogLevel(LogLevel.INFO); client.registerListener(new MessageListener() { @Override public void onMessage(NetworkMessage msg, NetworkServer server, NetworkClient client) { @@ -60,7 +62,7 @@ public void onMessage(NetworkMessage msg, NetworkServer server, NetworkClient cl @Override public Class[] getSupportedMessages() { - return new Class[] {TestTCPMessage.class, TestUDPMessage.class, TestTCPBigMessageA.class, TestTCPBigMessageB.class, TestUDPBigMessageA.class, TestUDPBigMessageB.class}; + return new Class[]{TestTCPMessage.class, TestUDPMessage.class, TestTCPBigMessageA.class, TestTCPBigMessageB.class, TestUDPBigMessageA.class, TestUDPBigMessageB.class}; } }); diff --git a/monkey-netty/build.gradle b/monkey-netty/build.gradle index 4c0f645..f250fd5 100644 --- a/monkey-netty/build.gradle +++ b/monkey-netty/build.gradle @@ -3,6 +3,11 @@ plugins { id 'maven-publish' } +java { + withJavadocJar() + withSourcesJar() +} + publishing { repositories { maven { @@ -17,6 +22,29 @@ publishing { publications { gpr(MavenPublication) { from(components.java) + pom { + name = 'Monkey Netty' + description = 'A implementation of a server-client communication system for jMonkeyEngine using Netty.IO that utilizes both TCP and UDP communication.' + url = 'https://github.com/tlf30/monkey-netty' + licenses { + license { + name = 'MIT License' + url = 'https://opensource.org/licenses/MIT' + } + } + developers { + developer { + id = 'tlf30' + name = 'Trevor Flynn' + email = 'liquidcrystalstudios@gmail.com' + } + } + scm { + connection = 'scm:git:git://github.com/tlf30/monkey-netty.git' + developerConnection = 'scm:git:ssh://tlf30/monkey-netty.git' + url = 'https://github.com/tlf30/monkey-netty/' + } + } } } } diff --git a/monkey-netty/src/main/java/io/tlf/monkeynetty/NetworkServer.java b/monkey-netty/src/main/java/io/tlf/monkeynetty/NetworkServer.java index 2fb4cc0..9e718b5 100644 --- a/monkey-netty/src/main/java/io/tlf/monkeynetty/NetworkServer.java +++ b/monkey-netty/src/main/java/io/tlf/monkeynetty/NetworkServer.java @@ -61,6 +61,35 @@ public interface NetworkServer { */ public NetworkProtocol[] getProtocol(); + /** + * @return true if the server is currently blocking new connections + */ + public boolean isBlocking(); + + /** + * Set if the server should block incoming connections. If true + * the server will close all incoming connections immediately without + * performing a handshake after establishing the connection. + * + * @param blocking If the server should block incoming connections. + */ + public void setBlocking(boolean blocking); + + /** + * @return The maximum number of connections the server will allow. + */ + public int getMaxConnections(); + + /** + * This sets the maximum number of connections the server will be allowed to have at any given time. + * If a connection is attempted to the server and the server currently has the maximum number of + * connections, it will immediately close the connection without performing a handshake after establishing + * the connection. + * + * @param maxConnections The maximum number of connections the server will allow. + */ + public void setMaxConnections(int maxConnections); + /** * Send a message to all clients connected to the server. * diff --git a/monkey-netty/src/main/java/io/tlf/monkeynetty/client/NettyClient.java b/monkey-netty/src/main/java/io/tlf/monkeynetty/client/NettyClient.java index ba353b5..305e105 100644 --- a/monkey-netty/src/main/java/io/tlf/monkeynetty/client/NettyClient.java +++ b/monkey-netty/src/main/java/io/tlf/monkeynetty/client/NettyClient.java @@ -99,14 +99,38 @@ public class NettyClient extends BaseAppState implements NetworkClient { private final Object handlerLock = new Object(); private final LinkedList messageCache = new LinkedList<>(); + /** + * Creates a new client configured to connect to the server. + * This connection will have SSL disabled. + * @param service The name of the service running on this client + * @param port The port the server is listening on + * @param server The host/ip of the server + */ public NettyClient(String service, int port, String server) { this(service, false, false, port, server); } + /** + * Creates a new client configured to connect to the server. + * + * @param service The name of the service running on this client + * @param ssl If the client should attempt to connect with ssl + * @param port The port the server is listening on + * @param server The host/ip of the server + */ public NettyClient(String service, boolean ssl, int port, String server) { this(service, ssl, true, port, server); } + /** + * Creates a new client configured to connect to the server. + * + * @param service The name of the service running on this client + * @param ssl If the client should attempt to connect with ssl + * @param sslSelfSigned If the client will allow the server to use a self signed ssl certificate + * @param port The port the server is listening on + * @param server The host/ip of the server + */ public NettyClient(String service, boolean ssl, boolean sslSelfSigned, int port, String server) { this.service = service; this.port = port; @@ -131,30 +155,68 @@ public void onEnable() { setupTcp(); } + @Override public void onDisable() { disconnect(); } - public int getConnectionTimeout() { - return connectionTimeout; - } - + /** + * Set the timeout duration in milliseconds for creating a new connection from the client to the server. + * This does not effect the read/write timeouts for messages after the connection has been established. + * @param connectionTimeout The timeout in milliseconds for creating a new connection. + */ public void setConnectionTimeout(int connectionTimeout) { this.connectionTimeout = connectionTimeout; } + /** + * @return The timeout in milliseconds for creating a new connection + */ + public int getConnectionTimeout() { + return connectionTimeout; + } + + /** + * Sets the Netty.IO internal log level. + * This will not change the java.util.logger Logger for Monkey-Netty. + * @param logLevel The internal Netty.IO log level + */ public void setLogLevel(LogLevel logLevel) { this.logLevel = logLevel; } + /** + * @return The internal Netty.IO log level + */ + public LogLevel getLogLevel() { + return logLevel; + } + + /** + * Sets the message cache mode. By default the mode is MessageCacheMode.ENABLE_TCP + * See MessageCacheMode for more information about the supported mode options. + * @param mode The desired message cache mode. + */ public void setMessageCacheMode(MessageCacheMode mode) { this.cacheMode = mode; } + /** + * @return The current message cache mode. + */ public MessageCacheMode getMessageCacheMode() { return cacheMode; } + /** + * Internal use only + * Setup the TCP netty.io pipeline. + * This will create a dedicated TCP channel to the server. + * The pipeline is setup to handle NetworkMessage message types. + * The pipeline will also send/receive a ping to/from the server to ensure the connection is still active. + * If the connection becomes inactive, the client will attempt a new connection. + * The pipeline will be configured with SSL if SSL parameters have been passed to the client. + */ private void setupTcp() { LOGGER.fine("Setting up tcp"); if (ssl) { @@ -208,6 +270,7 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { } else { LOGGER.log(Level.SEVERE, "Received message that was not a NetworkMessage object"); } + ctx.fireChannelRead(msg); } @Override @@ -225,14 +288,12 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { new ChannelDuplexHandler() { @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { - System.out.println("Got event: " + evt.getClass().getName()); if (evt instanceof IdleStateEvent) { - System.out.println("Got idle state: " + ((IdleStateEvent) evt).state()); IdleStateEvent e = (IdleStateEvent) evt; if (e.state() == IdleState.READER_IDLE) { handleInactiveConnection(); } else if (e.state() == IdleState.WRITER_IDLE) { - ctx.writeAndFlush(new PingMessage()); + send(new PingMessage()); } } } @@ -254,6 +315,13 @@ public void channelInactive(ChannelHandlerContext ctx) { } } + /** + * Internal use onle + * Setup the UDP netty.io pipeline. + * This will create a dedicated UDP channel to the server. + * The pipeline is setup to handle NetworkMessage message types. + * @param hash The hash that will be used to establish the UDP channel with the server. + */ private void setupUdp(String hash) { LOGGER.fine("Setting up udp"); udpGroup = new NioEventLoopGroup(); @@ -278,13 +346,17 @@ protected void initChannel(DatagramChannel socketChannel) { new ChannelInboundHandlerAdapter() { @Override public void channelRead(ChannelHandlerContext ctx, Object netObj) { - AddressedEnvelope envelope = (AddressedEnvelope) netObj; - Object msg = envelope.content(); - if (msg instanceof NetworkMessage) { - receive((NetworkMessage) msg); - } else { - LOGGER.log(Level.SEVERE, "Received message that was not a NetworkMessage object"); + if (netObj instanceof AddressedEnvelope) { + //We don't care about the envelope type, only the object within if it is a NetworkMessage + AddressedEnvelope envelope = (AddressedEnvelope) netObj; + Object msg = envelope.content(); + if (msg instanceof NetworkMessage) { + receive((NetworkMessage) msg); + } else { + LOGGER.log(Level.SEVERE, "Received message that was not a NetworkMessage object"); + } } + ctx.fireChannelRead(netObj); } @Override @@ -312,6 +384,11 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { } } + /** + * Internal use only + * Completes the connection and notifies all connection listeners + * that the client has connected. + */ protected void completeConnection() { pendingEstablish = false; LOGGER.log(Level.FINEST, "Connection established"); @@ -325,6 +402,11 @@ protected void completeConnection() { } } + /** + * Internal use only + * If the client is disconnecting from the server, then this function will not perform + * any action. Otherwise, we will attempt to reconnect. + */ private void handleInactiveConnection() { if (disconnecting) { return; //We will ignore inactive connections when disconnecting as this will get triggered @@ -334,6 +416,12 @@ private void handleInactiveConnection() { reconnect = true; } + /** + * Internal use only + * Catch a network error. This will cause the error to be sent to the logger. + * Catching the exception will cause the client to attempt to reconnect to the server. + * @param cause The error to catch + */ private void catchNetworkError(Throwable cause) { //if (cause instanceof java.net.SocketException) { //The server disconnected unexpectedly, we will not log it. @@ -401,6 +489,19 @@ public void send(NetworkMessage message) { send(message, true); } + /** + * Internal use only + * Send a message from the client to the server. + * If caching is enabled on the client, and @param enabledCache is true + * then the cache will be used if the client is not currently connected to the server. + * Otherwise, the client will attempt to send the message without the cache. + * + * The client will select the appropriate TCP or UDP channel to send the message + * to the server on, depending on the protocol specified in the message. + * + * @param message The message to send to the client + * @param enableCache If the client should attempt to use the message cache if required + */ private void send(NetworkMessage message, boolean enableCache) { if (!isConnected() && enableCache) { if (cacheMode == MessageCacheMode.ENABLED) { diff --git a/monkey-netty/src/main/java/io/tlf/monkeynetty/server/NettyServer.java b/monkey-netty/src/main/java/io/tlf/monkeynetty/server/NettyServer.java index 23859e8..a597e21 100644 --- a/monkey-netty/src/main/java/io/tlf/monkeynetty/server/NettyServer.java +++ b/monkey-netty/src/main/java/io/tlf/monkeynetty/server/NettyServer.java @@ -32,8 +32,6 @@ import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.serialization.ClassResolvers; -import io.netty.handler.codec.serialization.ObjectDecoder; -import io.netty.handler.codec.serialization.ObjectEncoder; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; import io.netty.handler.ssl.SslContext; @@ -53,7 +51,6 @@ import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.stream.Collectors; public class NettyServer extends BaseAppState implements NetworkServer { @@ -87,18 +84,57 @@ public class NettyServer extends BaseAppState implements NetworkServer { private File cert; private File key; + /** + * Create a new UDP/TCP server. + * The server will be created without SSL + * + * @param service The name of the service the server is running + * @param port The port the TCP/UDP server will listen on + */ public NettyServer(String service, int port) { this(service, false, port); } + /** + * Create a new UDP/TCP server. + * If ssl is enabled, the TCP server will generate a self signed certificate and use ssl. + * + * @param service The name of the service the server is running + * @param ssl If ssl should be used on the TCP server + * @param port The port the TCP/UDP server will listen on + */ public NettyServer(String service, boolean ssl, int port) { this(service, ssl, true, null, null, port); } + /** + * Create a new UDP/TCP server. + * If ssl is enabled, and a certificate key pair are provided, the TCP server will use ssl. + * If the server failes to load the cert key pair, or they are null, it will fail back to non-ssl. + * + * @param service The name of the service the server is running + * @param ssl If ssl should be used on the TCP server + * @param cert The certificate file, or null + * @param key The certificate key, or null + * @param port The port the TCP/UDP server will listen on + */ public NettyServer(String service, boolean ssl, File cert, File key, int port) { this(service, ssl, false, cert, key, port); } + /** + * Create a new UDP/TCP server. + * If ssl is enabled, and a certificate key pair are provided, the TCP server will use ssl. + * If the server failes to load the cert key pair, or they are null, it will fail back to + * a self signed certificate if enabled, otherwise will fail back to non-ssl. + * + * @param service The name of the service the server is running + * @param ssl If ssl should be used on the TCP server + * @param selfGenCert If a self signed certificate can be used. + * @param cert The certificate file, or null to use self signed cert + * @param key The certificate key, or null to use self signed cert + * @param port The port the TCP/UDP server will listen on + */ private NettyServer(String service, boolean ssl, boolean selfGenCert, File cert, File key, int port) { this.service = service; this.port = port; @@ -149,22 +185,33 @@ public int getConnections() { return tcpClients.size(); } + @Override public boolean isBlocking() { return blocking; } + @Override public void setBlocking(boolean blocking) { this.blocking = blocking; } + @Override public int getMaxConnections() { return maxConnections; } + @Override public void setMaxConnections(int maxConnections) { this.maxConnections = maxConnections; } + /** + * Internal use only + * Process an incoming client connection. + * Will handle max connections and blocking mode. + * Will fire connection listeners. + * @param client The client making the connection + */ private void receive(NetworkClient client) { if (isBlocking() || getConnections() >= getMaxConnections() || !(client instanceof NettyConnection)) { client.disconnect(); @@ -194,10 +241,18 @@ private void receive(NetworkClient client) { } } + /** + * Internal use only + * Process an incoming message from a client. + * Will notify message listeners. + * + * @param client The client the message was from + * @param message The message sent + */ private void receive(NetworkClient client, NetworkMessage message) { client.receive(message); for (MessageListener handler : messageListeners) { - for (Class a : handler.getSupportedMessages()) { + for (Class a : handler.getSupportedMessages()) { if (a.isInstance(message)) { try { handler.onMessage(message, this, client); @@ -220,7 +275,6 @@ public void send(NetworkMessage message, NetworkClient client) { client.send(message); } - @Override public int getPort() { return port; @@ -241,10 +295,31 @@ public NetworkProtocol[] getProtocol() { return new NetworkProtocol[]{NetworkProtocol.UDP, NetworkProtocol.TCP}; } + /** + * Sets the Netty.IO internal log level. + * This will not change the java.util.logger Logger for Monkey-Netty. + * @param logLevel The internal Netty.IO log level + */ public void setLogLevel(LogLevel logLevel) { this.logLevel = logLevel; } + /** + * @return The internal Netty.IO log level + */ + public LogLevel getLogLevel() { + return logLevel; + } + + /** + * Internal use only + * Setup the TCP netty.io server pipeline. + * This will create a dedicated TCP channel for each client. + * The pipeline is setup to handle NetworkMessage message types. + * The pipeline will also send/receive a ping to/from the client to ensure the connection is still active. + * If the connection becomes inactive, the server will disconnect the client. + * The pipeline will be configured with SSL if SSL parameters have been passed to the server. + */ private void setupTcp() { //Setup ssl if (ssl) { @@ -280,14 +355,19 @@ public void initChannel(SocketChannel ch) { //Disconnect client listener ch.closeFuture().addListener((ChannelFutureListener) future -> { - if (tcpClients.get(future.channel()) == null) { + NettyConnection connection = tcpClients.get(future.channel()); + if (connection == null) { return; //No client on this connection } - //Check if the client is currently trying to a udp connection - if (secrets.containsValue(tcpClients.get(future.channel()))) { //Client never established udp channel - //find secret for client - String secret = secrets.keySet().stream().filter(s -> secrets.get(s).equals(tcpClients.get(future.channel()))).collect(Collectors.toList()).get(0); + //find and remove secret for client if one exists + String secret = null; + for (String key : Collections.unmodifiableCollection(secrets.keySet())) { + if (secrets.get(key).equals(connection)) { + secret = key; + } + } + if (secret != null) { secrets.remove(secret); } @@ -327,6 +407,7 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { } else { LOGGER.log(Level.SEVERE, "Received message that was not a NetworkMessage object"); } + ctx.fireChannelRead(msg); } @Override @@ -349,7 +430,8 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { if (e.state() == IdleState.READER_IDLE) { ctx.close(); } else if (e.state() == IdleState.WRITER_IDLE) { - ctx.writeAndFlush(new PingMessage()); + NettyConnection conn = tcpClients.get(ctx.channel()); + conn.send(new PingMessage()); } } } @@ -365,6 +447,12 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { } } + /** + * Internal use onle + * Setup the UDP netty.io server pipeline. + * This will create a dedicated UDP channel for each client. + * The pipeline is setup to handle NetworkMessage message types. + */ private void setupUdp() { try { udpConGroup = new NioEventLoopGroup(); @@ -399,12 +487,14 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { NettyConnection client = secrets.get(((UdpConHashMessage) msg).getUdpHash()); if (client == null) { ctx.close(); + //Do not pass the message on, we are forcibly disconnecting the client return; } secrets.remove(((UdpConHashMessage) msg).getUdpHash()); client.setUdp((UdpChannel) ctx.channel()); udpClients.put(ctx.channel(), client); receive(client); + ctx.fireChannelRead(msg); return; } if (msg instanceof NetworkMessage) { @@ -417,6 +507,7 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { } else { LOGGER.log(Level.SEVERE, "Received message that was not a NetworkMessage object"); } + ctx.fireChannelRead(msg); } @Override @@ -438,6 +529,11 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { } } + /** + * Internal use only + * Catch a network error. This will cause the error to be sent to the logger. + * @param cause The error to catch + */ private void catchNetworkError(Throwable cause) { if (!(cause instanceof java.net.SocketException)) { LOGGER.log(Level.WARNING, "Network Server Error", cause); @@ -466,6 +562,7 @@ public void unregisterListener(ConnectionListener listener) { } /** + * Internal use only * Generates a base64 like hash * * @param len The number of characters to generate in the hash