diff --git a/.gitignore b/.gitignore index b3133ef..261a93f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +*.iml +.idea # Compiled class file /bin/ *.class diff --git a/src/com/blogspot/debukkitsblog/net/Client.java b/src/com/blogspot/debukkitsblog/net/Client.java index 4230fb5..2449cfa 100644 --- a/src/com/blogspot/debukkitsblog/net/Client.java +++ b/src/com/blogspot/debukkitsblog/net/Client.java @@ -1,11 +1,7 @@ package com.blogspot.debukkitsblog.net; -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.EOFException; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; +import javax.net.ssl.SSLSocketFactory; +import java.io.*; import java.net.ConnectException; import java.net.InetSocketAddress; import java.net.Socket; @@ -14,514 +10,486 @@ import java.util.HashMap; import java.util.UUID; -import javax.net.ssl.SSLSocketFactory; - /** * A very simple Client class for Java network applications
* originally created on March 9, 2016 in Horstmar, Germany - * + * * @author Leonard Bienbeck * @version 2.4.1 */ +@SuppressWarnings({"WeakerAccess", "unused"}) public class Client { - protected String id; - protected String group; - - protected Socket loginSocket; - protected InetSocketAddress address; - protected int timeout; - - protected Thread listeningThread; - protected HashMap idMethods = new HashMap(); - - protected int errorCount; - - protected boolean secureMode; - protected boolean muted; - protected boolean stopped; - - /** - * The default user id Datapackes are signed with. This is a type 4 pseudo - * randomly generated UUID. - */ - public static final String DEFAULT_USER_ID = UUID.randomUUID().toString(); - /** - * The default group id Datapackages are signed with: _DEFAULT_GROUP_ - */ - public static final String DEFAULT_GROUP_ID = "_DEFAULT_GROUP_"; - - /** - * Constructs a simple client with just a hostname and port to connect to - * - * @param hostname - * The hostname to connect to - * @param port - * The port to connect to - */ - public Client(String hostname, int port) { - this(hostname, port, 10000, false, DEFAULT_USER_ID, DEFAULT_GROUP_ID); - } - - public Client(String hostname, int port, int timeout) { - this(hostname, port, timeout, false, DEFAULT_USER_ID, DEFAULT_GROUP_ID); - } - - /** - * Constructs a simple client with a hostname and port to connect to and an id - * the server uses to identify this client in the future (e.g. for sending - * messages only this client should receive) - * - * @param hostname - * The hostname to connect to - * @param port - * The port to connect to - * @param id - * The id the server may use to identify this client - */ - public Client(String hostname, int port, String id) { - this(hostname, port, 10000, false, id, DEFAULT_GROUP_ID); - } - - /** - * Constructs a simple client with a hostname and port to connect to, an id the - * server uses to identify this client in the future (e.g. for sending messages - * only this client should receive) and a group name the server uses to identify - * this and some other clients in the future (e.g. for sending messages to the - * members of this group, but no other clients) - * - * @param hostname - * The hostname to connect to - * @param port - * The port to connect to - * @param id - * The id the server may use to identify this client - * @param group - * The group name the server may use to identify this and similar - * clients - */ - public Client(String hostname, int port, String id, String group) { - this(hostname, port, 10000, false, id, group); - } - - /** - * Constructs a simple client with all possible configurations - * - * @param hostname - * The hostname to connect to - * @param port - * The port to connect to - * @param timeout - * The timeout after a connection attempt will be given up - * @param useSSL - * Whether a secure SSL connection should be used - * @param id - * The id the server may use to identify this client - * @param group - * The group name the server may use to identify this and similar - * clients - */ - public Client(String hostname, int port, int timeout, boolean useSSL, String id, String group) { - this.id = id; - this.group = group; - - this.errorCount = 0; - this.address = new InetSocketAddress(hostname, port); - this.timeout = timeout; - - this.secureMode = useSSL; - if (secureMode) { - System.setProperty("javax.net.ssl.trustStore", "ssc.store"); - System.setProperty("javax.net.ssl.keyStorePassword", "SimpleServerClient"); - } - } - - /** - * Checks whether the client is connected to the server and waiting for incoming - * messages. - * - * @return true, if the client is connected to the server and waiting for - * incoming messages - */ - public boolean isListening() { - return isConnected() && listeningThread != null && listeningThread.isAlive() && errorCount == 0; - } - - /** - * Checks whether the persistent connection to the server listening for incoming - * messages is connected. This does not check whether the client actually waits - * for incoming messages with the help of the listening thread, but only - * the pure connection to the server. - * - * @return true, if connected - */ - public boolean isConnected() { - return loginSocket != null && loginSocket.isConnected(); - } - - /** - * Checks the connectivity to the server - * - * @return true, if the server can be reached at all using the given address - * data - */ - public boolean isServerReachable() { - try { - Socket tempSocket = new Socket(); - tempSocket.connect(this.address); - tempSocket.isConnected(); - tempSocket.close(); - return true; - } catch(IOException e) { - return false; - } - } - - /** - * Mutes the console output of this instance, stack traces will still be - * printed.
- * Be careful: This will not prevent processing of messages passed to the - * onLog and onLogError methods, if they were overwritten. - * - * @param muted - * true if there should be no console output - */ - public void setMuted(boolean muted) { - this.muted = muted; - } - - /** - * Starts the client. This will cause a connection attempt, a login on the - * server and the start of a new listening thread (both to receive messages and - * broadcasts from the server) - */ - public void start() { - stopped = false; - login(); - startListening(); - } - - /** - * Stops the client. The connection to the server is interrupted as soon as - * possible and then no further Datapackages are received. Warning: The - * whole process of stopping can take as long as the server needs to the next - * Datapackage, which will wake up the Client and cause him to stop. - */ - public void stop() { - stopped = true; - onLog("[Client] Stopping..."); - } - - /** - * Called to repair the connection if it is lost - */ - protected void repairConnection() { - onLog("[Client] [Connection-Repair] Repairing connection..."); - if (loginSocket != null) { - try { - loginSocket.close(); - } catch (IOException e) { - // This exception does not need to result in any further action or output - } - loginSocket = null; - } - - login(); - startListening(); - } - - /** - * Logs in to the server to receive messages and broadcasts from the server - * later - */ - protected void login() { - if(stopped) { - return; - } - - // 1. connect - try { - onLog("[Client] Connecting" + (secureMode ? " using SSL..." : "...")); - if (loginSocket != null && loginSocket.isConnected()) { - throw new AlreadyConnectedException(); - } - - if (secureMode) { - loginSocket = SSLSocketFactory.getDefault().createSocket(address.getAddress(), address.getPort()); - } else { - loginSocket = new Socket(); - loginSocket.connect(this.address, this.timeout); - } - - onLog("[Client] Connected to " + loginSocket.getRemoteSocketAddress()); - - // 2. login - try { - onLog("[Client] Logging in..."); - // open an outputstream - ObjectOutputStream out = new ObjectOutputStream(new BufferedOutputStream(loginSocket.getOutputStream())); - // create a magic login package - Datapackage loginPackage = new Datapackage("_INTERNAL_LOGIN_", id, group); - loginPackage.sign(id, group); - // send the package to the server - out.writeObject(loginPackage); - out.flush(); - // note: this special method does not expect the server to send a reply - onLog("[Client] Logged in."); - onReconnect(); - } catch (IOException ex) { - onLogError("[Client] Login failed."); - } - - } catch(ConnectException e) { - onLogError("[Client] Connection failed: " + e.getMessage()); - onConnectionProblem(); - } catch (IOException e) { - e.printStackTrace(); - onConnectionProblem(); - } - } - - /** - * Starts a new thread listening for messages from the server. A message will - * only be processed if a handler for its identifier has been registered before - * using registerMethod(String identifier, Executable executable) - */ - protected void startListening() { - - // do not restart the listening thread if it is already running - if (listeningThread != null && listeningThread.isAlive()) { - return; - } - - listeningThread = new Thread(new Runnable() { - @Override - public void run() { - - // always repeat if not stopped - while (!stopped) { - try { - // repait connection if something went wrong with the connection - if (loginSocket != null && !loginSocket.isConnected()) { - while (!loginSocket.isConnected()) { - repairConnection(); - if (loginSocket.isConnected()) { - break; - } - - Thread.sleep(5000); - repairConnection(); - } - } - - onConnectionGood(); - - // wait for incoming messages and read them - ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream(loginSocket.getInputStream())); - Object raw = ois.readObject(); - - // if the client has been stopped while this thread was listening to an arriving - // Datapackage, stop the proccess at this point - if (stopped) { - return; - } - - if (raw instanceof Datapackage) { - final Datapackage msg = (Datapackage) raw; - - // inspect all registered methods - for (final String current : idMethods.keySet()) { - // if the identifier of a method equals the identifier of the Datapackage... - if (current.equalsIgnoreCase(msg.id())) { - onLog("[Client] Message received. Executing method for '" + msg.id() + "'..."); - // execute the registered Executable on a new thread - new Thread(new Runnable() { - @Override - public void run() { - idMethods.get(current).run(msg, loginSocket); - } - }).start(); - break; - } - } - - } - - } catch(SocketException e) { - onConnectionProblem(); - onLogError("[Client] Connection lost"); - repairConnection(); - } catch (ClassNotFoundException | IOException | InterruptedException ex) { - ex.printStackTrace(); - onConnectionProblem(); - onLogError("[Client] Error: The connection to the server is currently interrupted!"); - repairConnection(); - } - - // reset errorCount if no errors occured until this point - errorCount = 0; - - } // while not stopped - - } // run - }); - - // start the thread - listeningThread.start(); - } - - /** - * Sends a message to the server using a brand new socket and returns the - * server's response - * - * @param message - * The message to send to the server - * @param timeout - * The timeout after a connection attempt will be given up - * @return The server's response. The identifier of this Datapackage should be - * "REPLY" by default, the rest is custom data. - */ - public Datapackage sendMessage(Datapackage message, int timeout) { - try { - // connect to the target client's socket - Socket tempSocket; - if (secureMode) { - tempSocket = SSLSocketFactory.getDefault().createSocket(address.getAddress(), address.getPort()); - } else { - tempSocket = new Socket(); - tempSocket.connect(address, timeout); - } - - // Open output stream and write message - ObjectOutputStream tempOOS = new ObjectOutputStream(new BufferedOutputStream(tempSocket.getOutputStream())); - message.sign(id, group); - tempOOS.writeObject(message); - tempOOS.flush(); - - // open input stream and wait for server's response. Warning: If the server - // won't send an answer, this lines might block the program or throw an - // EOFException - ObjectInputStream tempOIS = new ObjectInputStream(new BufferedInputStream(tempSocket.getInputStream())); - Object raw = tempOIS.readObject(); - - // close all streams and the socket - tempOOS.close(); - tempOIS.close(); - tempSocket.close(); - - // return the server's reply if it is a Datapackage - if (raw instanceof Datapackage) { - return (Datapackage) raw; - } - } catch(EOFException ex) { - onLogError("[Client] Error right after sending message: EOFException (did the server forget to send a reply?)"); - } catch (IOException | ClassNotFoundException ex) { - onLogError("[Client] Error while sending message"); - ex.printStackTrace(); - } - - return null; - } - - /** - * Sends a message to the server using a brand new socket and returns the - * server's response - * - * @param ID - * The ID of the message, allowing the server to decide what to do - * with its content - * @param content - * The content of the message - * @return The server's response. The identifier of this Datapackage should be - * "REPLY" by default, the rest is custom data. - */ - public Datapackage sendMessage(String ID, Object... content) { - return sendMessage(new Datapackage(ID, content)); - } - - /** - * Sends a message to the server using a brand new socket and returns the - * server's response - * - * @param message - * The message to send to the server - * @return The server's response. The identifier of this Datapackage should be - * "REPLY" by default, the rest is custom data. - */ - public Datapackage sendMessage(Datapackage message) { - return sendMessage(message, this.timeout); - } - - /** - * Registers a method that will be executed if a message containing - * identifier is received - * - * @param identifier - * The ID of the message to proccess - * @param executable - * The method to be called when a message with identifier is - * received - */ - public void registerMethod(String identifier, Executable executable) { - idMethods.put(identifier, executable); - } - - /** - * Called on the listener's main thread when there is a problem with the - * connection. Overwrite this method when extending this class. - */ - public void onConnectionProblem() { - // Overwrite this method when extending this class - } - - /** - * Called on the listener's main thread when there is no problem with the - * connection and everything is fine. Overwrite this method when extending this - * class. - */ - public void onConnectionGood() { - // Overwrite this method when extending this class - } - - /** - * Called on the listener's main thread when the client logs in to the server. - * This happens on the first and every further login (e.g. after a - * re-established connection). Overwrite this method when extending this class. - */ - public void onReconnect() { - // Overwrite this method when extending this class - } - - /** - * By default, this method is called whenever an output is to be made. If this - * method is not overwritten, the output is passed to the system's default - * output stream (if output is not muted).
- * Error messages are passed to the onLogError event listener.
- * Override this method to catch and process the message in a custom way. - * - * @param message - * The content of the output to be made - */ - public void onLog(String message) { - if (!muted) { - System.out.println(message); - } - } - - /** - * By default, this method is called whenever an error output is to be made. If - * this method is not overwritten, the output is passed to the system's default - * error output stream (if output is not muted).
- * Non-error messages are passed to the onLog event listener.
- * Override this method to catch and process the message in a custom way. - * - * @param message - * The content of the error output to be made - */ - public void onLogError(String message) { - if (!muted) { - System.err.println(message); - } - } + protected String id; + protected String group; + + protected Socket loginSocket; + protected InetSocketAddress address; + protected int timeout; + + protected Thread listeningThread; + protected HashMap idMethods = new HashMap(); + + protected int errorCount; + + protected boolean secureMode; + protected boolean muted; + protected boolean stopped; + + /** + * The default user id Datapackes are signed with. This is a type 4 pseudo + * randomly generated UUID. + */ + public static final String DEFAULT_USER_ID = UUID.randomUUID().toString(); + /** + * The default group id Datapackages are signed with: _DEFAULT_GROUP_ + */ + public static final String DEFAULT_GROUP_ID = "_DEFAULT_GROUP_"; + + /** + * Constructs a simple client with just a hostname and port to connect to + * + * @param hostname The hostname to connect to + * @param port The port to connect to + */ + public Client(String hostname, int port) { + this(hostname, port, 10000, false, DEFAULT_USER_ID, DEFAULT_GROUP_ID); + } + + public Client(String hostname, int port, int timeout) { + this(hostname, port, timeout, false, DEFAULT_USER_ID, DEFAULT_GROUP_ID); + } + + /** + * Constructs a simple client with a hostname and port to connect to and an id + * the server uses to identify this client in the future (e.g. for sending + * messages only this client should receive) + * + * @param hostname The hostname to connect to + * @param port The port to connect to + * @param id The id the server may use to identify this client + */ + public Client(String hostname, int port, String id) { + this(hostname, port, 10000, false, id, DEFAULT_GROUP_ID); + } + + /** + * Constructs a simple client with a hostname and port to connect to, an id the + * server uses to identify this client in the future (e.g. for sending messages + * only this client should receive) and a group name the server uses to identify + * this and some other clients in the future (e.g. for sending messages to the + * members of this group, but no other clients) + * + * @param hostname The hostname to connect to + * @param port The port to connect to + * @param id The id the server may use to identify this client + * @param group The group name the server may use to identify this and similar + * clients + */ + public Client(String hostname, int port, String id, String group) { + this(hostname, port, 10000, false, id, group); + } + + /** + * Constructs a simple client with all possible configurations + * + * @param hostname The hostname to connect to + * @param port The port to connect to + * @param timeout The timeout after a connection attempt will be given up + * @param useSSL Whether a secure SSL connection should be used + * @param id The id the server may use to identify this client + * @param group The group name the server may use to identify this and similar + * clients + */ + public Client(String hostname, int port, int timeout, boolean useSSL, String id, String group) { + this.id = id; + this.group = group; + + this.errorCount = 0; + this.address = new InetSocketAddress(hostname, port); + this.timeout = timeout; + + this.secureMode = useSSL; + if (secureMode) { + System.setProperty("javax.net.ssl.trustStore", "ssc.store"); + System.setProperty("javax.net.ssl.keyStorePassword", "SimpleServerClient"); + } + } + + /** + * Checks whether the client is connected to the server and waiting for incoming + * messages. + * + * @return true, if the client is connected to the server and waiting for + * incoming messages + */ + public boolean isListening() { + return isConnected() && errorCount == 0; + } + + /** + * Checks whether the persistent connection to the server listening for incoming + * messages is connected. This does not check whether the client actually waits + * for incoming messages with the help of the listening thread, but only + * the pure connection to the server. + * + * @return true, if connected + */ + public boolean isConnected() { + return loginSocket != null && loginSocket.isConnected(); + } + + /** + * Checks the connectivity to the server + * + * @return true, if the server can be reached at all using the given address + * data + */ + public boolean isServerReachable() { + try { + Socket tempSocket = new Socket(); + tempSocket.connect(this.address); + tempSocket.isConnected(); + tempSocket.close(); + return true; + } catch (IOException e) { + return false; + } + } + + /** + * Mutes the console output of this instance, stack traces will still be + * printed.
+ * Be careful: This will not prevent processing of messages passed to the + * onLog and onLogError methods, if they were overwritten. + * + * @param muted true if there should be no console output + */ + public void setMuted(boolean muted) { + this.muted = muted; + } + + /** + * Starts the client. This will cause a connection attempt, a login on the + * server and the start of a new listening thread (both to receive messages and + * broadcasts from the server) + */ + public void start() { + stopped = false; + login(); + startListening(); + } + + /** + * Stops the client. The connection to the server is interrupted as soon as + * possible and then no further Datapackages are received. Warning: The + * whole process of stopping can take as long as the server needs to the next + * Datapackage, which will wake up the Client and cause him to stop. + */ + public void stop() { + stopped = true; + onLog("[Client] Stopping..."); + } + + /** + * Called to repair the connection if it is lost + */ + protected void repairConnection() { + onLog("[Client] [Connection-Repair] Repairing connection..."); + if (loginSocket != null) { + try { + loginSocket.close(); + } catch (IOException e) { + // This exception does not need to result in any further action or output + } + loginSocket = null; + } + + login(); + startListening(); + } + + /** + * Logs in to the server to receive messages and broadcasts from the server + * later + */ + protected void login() { + if (stopped) { + return; + } + + // 1. connect + try { + onLog("[Client] Connecting" + (secureMode ? " using SSL..." : "...")); + if (loginSocket != null && loginSocket.isConnected()) { + throw new AlreadyConnectedException(); + } + + if (secureMode) { + loginSocket = SSLSocketFactory.getDefault().createSocket(address.getAddress(), address.getPort()); + } else { + loginSocket = new Socket(); + loginSocket.connect(this.address, this.timeout); + } + + onLog("[Client] Connected to " + loginSocket.getRemoteSocketAddress()); + + // 2. login + try { + onLog("[Client] Logging in..."); + // open an outputstream + ObjectOutputStream out = new ObjectOutputStream(new BufferedOutputStream(loginSocket.getOutputStream())); + // create a magic login package + Datapackage loginPackage = new Datapackage("_INTERNAL_LOGIN_", id, group); + loginPackage.sign(id, group); + // send the package to the server + out.writeObject(loginPackage); + out.flush(); + // note: this special method does not expect the server to send a reply + onLog("[Client] Logged in."); + onReconnect(); + } catch (IOException ex) { + onLogError("[Client] Login failed."); + } + + } catch (ConnectException e) { + onLogError("[Client] Connection failed: " + e.getMessage()); + onConnectionProblem(); + } catch (IOException e) { + e.printStackTrace(); + onConnectionProblem(); + } + } + + /** + * Starts a new thread listening for messages from the server. A message will + * only be processed if a handler for its identifier has been registered before + * using registerMethod(String identifier, Executable executable) + */ + protected void startListening() { + + // do not restart the listening thread if it is already running + if (listeningThread != null && listeningThread.isAlive()) { + return; + } + + // run + listeningThread = new Thread(() -> { + + // always repeat if not stopped + while (!stopped) { + try { + // repait connection if something went wrong with the connection + if (loginSocket != null && !loginSocket.isConnected()) { + while (!loginSocket.isConnected()) { + repairConnection(); + if (loginSocket.isConnected()) { + break; + } + + Thread.sleep(5000); + repairConnection(); + } + } + + onConnectionGood(); + + // wait for incoming messages and read them + ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream(loginSocket.getInputStream())); + Object raw = ois.readObject(); + + // if the client has been stopped while this thread was listening to an arriving + // Datapackage, stop the proccess at this point + if (stopped) { + return; + } + + if (raw instanceof Datapackage) { + final Datapackage msg = (Datapackage) raw; + + // inspect all registered methods + for (final String current : idMethods.keySet()) { + // if the identifier of a method equals the identifier of the Datapackage... + if (current.equalsIgnoreCase(msg.id())) { + onLog("[Client] Message received. Executing method for '" + msg.id() + "'..."); + // execute the registered Executable on a new thread + new Thread(new Runnable() { + @Override + public void run() { + idMethods.get(current).run(msg, loginSocket); + } + }).start(); + break; + } + } + + } + + } catch (SocketException e) { + onConnectionProblem(); + onLogError("[Client] Connection lost"); + repairConnection(); + } catch (ClassNotFoundException | IOException | InterruptedException ex) { + ex.printStackTrace(); + onConnectionProblem(); + onLogError("[Client] Error: The connection to the server is currently interrupted!"); + repairConnection(); + } + + // reset errorCount if no errors occured until this point + errorCount = 0; + + } // while not stopped + + }); + + // start the thread + listeningThread.start(); + } + + /** + * Sends a message to the server using a brand new socket and returns the + * server's response + * + * @param message The message to send to the server + * @param timeout The timeout after a connection attempt will be given up + * @return The server's response. The identifier of this Datapackage should be + * "REPLY" by default, the rest is custom data. + */ + public Datapackage sendMessage(Datapackage message, int timeout) { + try { + // connect to the target client's socket + Socket tempSocket; + if (secureMode) { + tempSocket = SSLSocketFactory.getDefault().createSocket(address.getAddress(), address.getPort()); + } else { + tempSocket = new Socket(); + tempSocket.connect(address, timeout); + } + + // Open output stream and write message + ObjectOutputStream tempOOS = new ObjectOutputStream(new BufferedOutputStream(tempSocket.getOutputStream())); + message.sign(id, group); + tempOOS.writeObject(message); + tempOOS.flush(); + + // open input stream and wait for server's response. Warning: If the server + // won't send an answer, this lines might block the program or throw an + // EOFException + ObjectInputStream tempOIS = new ObjectInputStream(new BufferedInputStream(tempSocket.getInputStream())); + Object raw = tempOIS.readObject(); + + // close all streams and the socket + tempOOS.close(); + tempOIS.close(); + tempSocket.close(); + + // return the server's reply if it is a Datapackage + if (raw instanceof Datapackage) { + return (Datapackage) raw; + } + } catch (EOFException ex) { + onLogError("[Client] Error right after sending message: EOFException (did the server forget to send a reply?)"); + } catch (IOException | ClassNotFoundException ex) { + onLogError("[Client] Error while sending message"); + ex.printStackTrace(); + } + + return null; + } + + /** + * Sends a message to the server using a brand new socket and returns the + * server's response + * + * @param ID The ID of the message, allowing the server to decide what to do + * with its content + * @param content The content of the message + * @return The server's response. The identifier of this Datapackage should be + * "REPLY" by default, the rest is custom data. + */ + public Datapackage sendMessage(String ID, Object... content) { + return sendMessage(new Datapackage(ID, content)); + } + + /** + * Sends a message to the server using a brand new socket and returns the + * server's response + * + * @param message The message to send to the server + * @return The server's response. The identifier of this Datapackage should be + * "REPLY" by default, the rest is custom data. + */ + public Datapackage sendMessage(Datapackage message) { + return sendMessage(message, this.timeout); + } + + /** + * Registers a method that will be executed if a message containing + * identifier is received + * + * @param identifier The ID of the message to proccess + * @param executable The method to be called when a message with identifier is + * received + */ + public void registerMethod(String identifier, Executable executable) { + idMethods.put(identifier, executable); + } + + /** + * Called on the listener's main thread when there is a problem with the + * connection. Overwrite this method when extending this class. + */ + public void onConnectionProblem() { + // Overwrite this method when extending this class + } + + /** + * Called on the listener's main thread when there is no problem with the + * connection and everything is fine. Overwrite this method when extending this + * class. + */ + public void onConnectionGood() { + // Overwrite this method when extending this class + } + + /** + * Called on the listener's main thread when the client logs in to the server. + * This happens on the first and every further login (e.g. after a + * re-established connection). Overwrite this method when extending this class. + */ + public void onReconnect() { + // Overwrite this method when extending this class + } + + /** + * By default, this method is called whenever an output is to be made. If this + * method is not overwritten, the output is passed to the system's default + * output stream (if output is not muted).
+ * Error messages are passed to the onLogError event listener.
+ * Override this method to catch and process the message in a custom way. + * + * @param message The content of the output to be made + */ + public void onLog(String message) { + if (!muted) { + System.out.println(message); + } + } + + /** + * By default, this method is called whenever an error output is to be made. If + * this method is not overwritten, the output is passed to the system's default + * error output stream (if output is not muted).
+ * Non-error messages are passed to the onLog event listener.
+ * Override this method to catch and process the message in a custom way. + * + * @param message The content of the error output to be made + */ + public void onLogError(String message) { + if (!muted) { + System.err.println(message); + } + } } \ No newline at end of file diff --git a/src/com/blogspot/debukkitsblog/net/Server.java b/src/com/blogspot/debukkitsblog/net/Server.java index ec031f6..15512b1 100644 --- a/src/com/blogspot/debukkitsblog/net/Server.java +++ b/src/com/blogspot/debukkitsblog/net/Server.java @@ -1,714 +1,598 @@ package com.blogspot.debukkitsblog.net; -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; +import javax.net.ssl.SSLServerSocketFactory; +import java.io.*; import java.net.ConnectException; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; import java.nio.channels.IllegalBlockingModeException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.UUID; - -import javax.net.ssl.SSLServerSocketFactory; +import java.util.*; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; /** - * A very simple-to-use Server class for Java network applications
- * originally created on March 9, 2016 in Horstmar, Germany - * + * A very simple-to-use Server class for Java network applications
originally created on March + * 9, 2016 in Horstmar, Germany + * * @author Leonard Bienbeck * @version 2.4.2 */ +@SuppressWarnings({"WeakerAccess", "unused"}) public abstract class Server { - protected HashMap idMethods = new HashMap(); - - protected ServerSocket server; - protected int port; - protected ArrayList clients; - protected ArrayList toBeDeleted; - - protected Thread listeningThread; - - protected boolean autoRegisterEveryClient; - protected boolean secureMode; - - protected boolean stopped; - protected boolean muted; - protected long pingInterval = 30*1000; // 30 seconds - - protected static final String INTERNAL_LOGIN_ID = "_INTERNAL_LOGIN_"; - - /** - * Constructs a simple server listening on the given port. Every client that - * connects to this server is registered and can receive broadcast and direct - * messages, the connection will be kept alive using a ping and ssl will not be - * used. This constructor is deprecated! It is strongly recommended to - * substitute it with the constructor that has the option muted as its - * last parameter. - * - * @param port - * The port to listen on - */ - @Deprecated - public Server(int port) { - this(port, true, true, false); - } - - /** - * Constructs a simple server listening on the given port. Every client that - * connects to this server is registered and can receive broadcast and direct - * messages, the connection will be kept alive using a ping and ssl will not be - * used. - * - * @param port - * The port to listen on - * @param muted - * Whether the mute mode should be activated on startup - */ - public Server(int port, boolean muted) { - this(port, true, true, false, muted); - } - - /** - * Constructs a simple server with all possible configurations. This - * constructor is deprecated! It is strongly recommended to substitute it with - * the constructor that has the option muted as its last parameter. - * - * @param port - * The port to listen on - * @param autoRegisterEveryClient - * Whether a client that connects should be registered to send it - * broadcast and direct messages later - * @param keepConnectionAlive - * Whether the connection should be kept alive using a ping package. - * The transmission interval can be set using - * setPingInterval(int seconds). - * @param useSSL - * Whether SSL should be used to establish a secure connection - */ - @Deprecated - public Server(int port, boolean autoRegisterEveryClient, boolean keepConnectionAlive, boolean useSSL) { - this.clients = new ArrayList(); - this.port = port; - this.autoRegisterEveryClient = autoRegisterEveryClient; - this.muted = false; - - this.secureMode = useSSL; - if (secureMode) { - System.setProperty("javax.net.ssl.keyStore", "ssc.store"); - System.setProperty("javax.net.ssl.keyStorePassword", "SimpleServerClient"); - } - if (autoRegisterEveryClient) { - registerLoginMethod(); - } - preStart(); - - start(); - - if (keepConnectionAlive) { - startPingThread(); - } - } - - /** - * Constructs a simple server with all possible configurations - * - * @param port - * The port to listen on - * @param autoRegisterEveryClient - * Whether a client that connects should be registered to send it - * broadcast and direct messages later - * @param keepConnectionAlive - * Whether the connection should be kept alive using a ping package. - * The transmission interval can be set using - * setPingInterval(int seconds). - * @param useSSL - * Whether SSL should be used to establish a secure connection - * @param muted - * Whether the mute mode should be activated on startup - */ - public Server(int port, boolean autoRegisterEveryClient, boolean keepConnectionAlive, boolean useSSL, boolean muted) { - this.clients = new ArrayList(); - this.port = port; - this.autoRegisterEveryClient = autoRegisterEveryClient; - this.muted = muted; - - this.secureMode = useSSL; - if (secureMode) { - System.setProperty("javax.net.ssl.keyStore", "ssc.store"); - System.setProperty("javax.net.ssl.keyStorePassword", "SimpleServerClient"); - } - if (autoRegisterEveryClient) { - registerLoginMethod(); - } - preStart(); - - start(); - - if (keepConnectionAlive) { - startPingThread(); - } - } - - /** - * Mutes the console output of this instance, stack traces will still be - * printed.
- * Be careful: This will not prevent processing of messages passed to the - * onLog and onLogError methods, if they were overwritten. - * - * @param muted - * true if there should be no console output - */ - public void setMuted(boolean muted) { - this.muted = muted; - } - - /** - * Sets the interval in which ping packages should be sent to keep the - * connection alive. Default is 30 seconds. - * - * @param seconds - * The interval in which ping packages should be sent - */ - public void setPingInterval(int seconds) { - this.pingInterval = seconds * 1000; - } - - /** - * Starts the thread sending a dummy package every pingInterval seconds. - * Adjust the interval using setPingInterval(int seconds). - */ - protected void startPingThread() { - new Thread(new Runnable() { - @Override - public void run() { - - while (server != null) { - try { - Thread.sleep(pingInterval); - } catch (InterruptedException e) { - // This exception does not need to result in any further action or output - } - broadcastMessage(new Datapackage("_INTERNAL_PING_", "OK")); - } - - } - }).start(); - } - - /** - * Starts the listening thread waiting for messages from clients - */ - protected void startListening() { - if (listeningThread == null && server != null) { - listeningThread = new Thread(new Runnable() { - - @Override - public void run() { - while (!Thread.interrupted() && !stopped && server != null) { - - try { - // Wait for client to connect - onLog("[Server] Waiting for connection" + (secureMode ? " using SSL..." : "...")); - @SuppressWarnings("resource") - final Socket tempSocket = server.accept(); // potential resource leak, tempSocket might not be closed! - - // Read the client's message - ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream(tempSocket.getInputStream())); - Object raw = ois.readObject(); - - if (raw instanceof Datapackage) { - final Datapackage msg = (Datapackage) raw; - onLog("[Server] Message received: " + msg); - - // inspect all registered methods - for (final String current : idMethods.keySet()) { - // if the current method equals the identifier of the Datapackage... - if (msg.id().equalsIgnoreCase(current)) { - onLog("[Server] Executing method for identifier '" + msg.id() + "'"); - // execute the Executable on a new thread - new Thread(new Runnable() { - @Override - public void run() { - // Run the method registered for the ID of this Datapackage - idMethods.get(current).run(msg, tempSocket); - // and close the temporary socket if it is no longer needed - if (!msg.id().equals(INTERNAL_LOGIN_ID)) { - try { - tempSocket.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - }).start(); - break; - } - } - - } - - } catch (SocketException e) { - onLog("Server stopped."); - onServerStopped(); - } catch (IllegalBlockingModeException | IOException | ClassNotFoundException e) { - e.printStackTrace(); - } - - } - } - - }); - - listeningThread.start(); - } - } - - /** - * Sends a reply to client. This method should only be called from within the - * run-Method of an Executable implementation. - * - * @param toSocket - * The socket the message should be delivered to - * @param datapackageContent - * The content of the message to be delivered. The ID of this - * Datapackage will be "REPLY". - */ - public synchronized void sendReply(Socket toSocket, Object... datapackageContent) { - sendMessage(new RemoteClient(null, toSocket), new Datapackage("REPLY", datapackageContent)); - } - - /** - * Sends a message to a client with specified id - * - * @param remoteClientId - * The id of the client it registered on login - * @param datapackageId - * The id of message - * @param datapackageContent - * The content of the message - */ - public synchronized void sendMessage(String remoteClientId, String datapackageId, Object... datapackageContent) { - sendMessage(remoteClientId, new Datapackage(datapackageId, datapackageContent)); - } - - /** - * Sends a message to a client with specified id - * - * @param remoteClientId - * The id of the client it registered on login - * @param message - * The message - */ - public synchronized void sendMessage(String remoteClientId, Datapackage message) { - for (RemoteClient current : clients) { - if (current.getId().equals(remoteClientId)) { - sendMessage(current, message); - } - } - } - - /** - * Sends a message to a client - * - * @param remoteClient - * The target client - * @param datapackageId - * The id of message - * @param datapackageContent - * The content of the message - */ - public synchronized void sendMessage(RemoteClient remoteClient, String datapackageId, Object... datapackageContent) { - sendMessage(remoteClient, new Datapackage(datapackageId, datapackageContent)); - } - - /** - * Sends a message to a client - * - * @param remoteClient - * The target client - * @param message - * The message - */ - public synchronized void sendMessage(RemoteClient remoteClient, Datapackage message) { - try { - // send message - if (!remoteClient.getSocket().isConnected()) { - throw new ConnectException("Socket not connected."); - } - ObjectOutputStream out = new ObjectOutputStream(new BufferedOutputStream(remoteClient.getSocket().getOutputStream())); - out.writeObject(message); - out.flush(); - } catch (IOException e) { - onLogError("[Server] [Send Message] Error: " + e.getMessage()); - - // if an error occured: remove client from list - if (toBeDeleted != null) { - toBeDeleted.add(remoteClient); - } else { - clients.remove(remoteClient); - onClientRemoved(remoteClient); - } - } - } - - /** - * Broadcasts a message to a group of clients - * - * @param group - * The group name the clients registered on their login - * @param message - * The message - * @return The number of clients reached - */ - public synchronized int broadcastMessageToGroup(String group, Datapackage message) { - toBeDeleted = new ArrayList(); - - // send message to all clients - int txCounter = 0; - for (RemoteClient current : clients) { - if (current.getGroup().equals(group)) { - sendMessage(current, message); - txCounter++; - } - } - - // remove all clients which produced errors while sending - txCounter -= toBeDeleted.size(); - for (RemoteClient current : toBeDeleted) { - clients.remove(current); - onClientRemoved(current); - } - - toBeDeleted = null; - - return txCounter; - } - - /** - * Broadcasts a message to a group of clients - * - * @param message - * The message - * @return The number of clients reached - */ - public synchronized int broadcastMessage(Datapackage message) { - toBeDeleted = new ArrayList(); - - // send message to all clients - int txCounter = 0; - for (RemoteClient current : clients) { - sendMessage(current, message); - txCounter++; - } - - // remove all clients which produced errors while sending - txCounter -= toBeDeleted.size(); - for (RemoteClient current : toBeDeleted) { - clients.remove(current); - onClientRemoved(current); - } - - toBeDeleted = null; - - return txCounter; - } - - /** - * Registers a method that will be executed if a message containing - * identifier is received - * - * @param identifier - * The ID of the message to proccess - * @param executable - * The method to be called when a message with identifier is - * received - */ - public void registerMethod(String identifier, Executable executable) { - if (identifier.equalsIgnoreCase(INTERNAL_LOGIN_ID) && autoRegisterEveryClient) { - throw new IllegalArgumentException("Identifier may not be '" + INTERNAL_LOGIN_ID + "'. " - + "Since v1.0.1 the server automatically registers new clients. " - + "To react on new client registed, use the onClientRegisters() listener by overwriting it."); - } - idMethods.put(identifier, executable); - } - - /** - * Registers a login handler. This method is called only if the constructor has - * been applied to register clients. - */ - protected void registerLoginMethod() { - idMethods.put(INTERNAL_LOGIN_ID, new Executable() { - @Override - public void run(Datapackage msg, Socket socket) { - if (msg.size() == 3) { - registerClient((String) msg.get(1), (String) msg.get(2), socket); - } else if (msg.size() == 2) { - registerClient((String) msg.get(1), socket); - } else { - registerClient(UUID.randomUUID().toString(), socket); - } - onClientRegistered(msg, socket); - onClientRegistered(); - } - }); - } - - /** - * Registers a client to allow sending it direct and broadcast messages later - * - * @param id - * The client's id - * @param newClientSocket - * The client's socket - */ - protected synchronized void registerClient(String id, Socket newClientSocket) { - clients.add(new RemoteClient(id, newClientSocket)); - } - - /** - * Registers a client to allow sending it direct and broadcast messages later - * - * @param id - * The client's id - * @param group - * The client's group name - * @param newClientSocket - * The client's socket - */ - protected synchronized void registerClient(String id, String group, Socket newClientSocket) { - clients.add(new RemoteClient(id, group, newClientSocket)); - } - - /** - * Starts the server. This method is automatically called after - * preStart() and starts the actual and the listening thread. - */ - protected void start() { - stopped = false; - server = null; - try { - - if (secureMode) { - server = SSLServerSocketFactory.getDefault().createServerSocket(port); - } else { - server = new ServerSocket(port); - } - - } catch (IOException e) { - onLogError("Error opening ServerSocket"); - e.printStackTrace(); - } - startListening(); - } - - /** - * Stops the server - * - * @throws IOException - * If closing the ServerSocket fails - */ - public void stop() throws IOException { - stopped = true; - - if (listeningThread.isAlive()) { - listeningThread.interrupt(); - } - - if (server != null) { - server.close(); - } - } - - /** - * Counts the number of clients registered - * - * @return The number of clients registered - */ - public synchronized int getClientCount() { - return clients != null ? clients.size() : 0; - } - - /** - * Checks whether a RemoteClient with the given ID is currently connected to the - * server - * - * @param clientId - * The clients ID - * @return true, if a RemoteClient with ID clientId is connected to the - * server - */ - public boolean isClientIdConnected(String clientId) { - if(clients != null && clients.size() > 0) { - // Iterate all clients connected - for(RemoteClient c : clients) { - // Check client exists and its socket is connected - if(c.getId().equals(clientId) && c.getSocket() != null && c.getSocket().isConnected()) { - return true; - } - } - } - return false; - } - - /** - * Checks whether any client is currently connected to the server - * - * @return true, if at least one client is connected to the server - */ - public boolean isAnyClientConnected() { - return getClientCount() > 0; - } - - /** - * Called just before the actual server starts. Register your handler methods in - * here using - * registerMethod(String identifier, Executable executable)! - */ - public abstract void preStart(); - - /** - * Called on the listener's main thread when a new client registers - */ - public void onClientRegistered() { - // Overwrite this method when extending this class - } - - /** - * Called on the listener's main thread when a new client registers - * - * @param msg - * The message the client registered with - * @param socket - * The socket the client registered with. Be careful with this! You - * should not close this socket, because the server should have - * stored it normally to reach this client later. - */ - public void onClientRegistered(Datapackage msg, Socket socket) { - // Overwrite this method when extending this class - } - - /** - * Called on the listener's main thread when a client is removed from the list. - * This normally happens if there was a problem with its connection. You should - * wait for the client to connect again. - * - * @param remoteClient - * The client that was removed from the list of reachable clients - */ - public void onClientRemoved(RemoteClient remoteClient) { - // Overwrite this method when extending this class - } - - /** - * Called when the server finally stops after the stop () method has been - * called. - */ - public void onServerStopped() { - // Overwrite this method when extending this class - } - - /** - * By default, this method is called whenever an output is to be made. If this - * method is not overwritten, the output is passed to the system's default - * output stream (if output is not muted).
- * Error messages are passed to the onLogError event listener.
- * Override this method to catch and process the message in a custom way. - * - * @param message - * The content of the output to be made - */ - public void onLog(String message) { - if (!muted) { - System.out.println(message); - } - } - - /** - * By default, this method is called whenever an error output is to be made. If - * this method is not overwritten, the output is passed to the system's default - * error output stream (if output is not muted).
- * Non-error messages are passed to the onLog event listener.
- * Override this method to catch and process the message in a custom way. - * - * @param message - * The content of the error output to be made - */ - public void onLogError(String message) { - if (!muted) { - System.err.println(message); - } - } - - /** - * A RemoteClient representating a client connected to this server storing an id - * for identification and a socket for communication. - */ - protected class RemoteClient { - private String id; - private String group; - private Socket socket; - - /** - * Creates a RemoteClient representating a client connected to this server - * storing an id for identification and a socket for communication. The client - * will be member of the default group. - * - * @param id - * The clients id (to use for identification; choose a custom String) - * @param socket - * The socket (to use for communication) - */ - public RemoteClient(String id, Socket socket) { - this.id = id; - this.group = "_DEFAULT_GROUP_"; - this.socket = socket; - } - - /** - * Creates a RemoteClient representating a client connected to this server - * storing an id for identification and a socket for communication. The client - * can be set as a member of a group of clients to receive messages broadcasted - * to a group. - * - * @param id - * The clients id (to use for identification; choose a custom String) - * @param group - * The group the client is member of - * @param socket - * The socket (to use for communication) - */ - public RemoteClient(String id, String group, Socket socket) { - this.id = id; - this.group = group; - this.socket = socket; - } - - public String getId() { - return id; - } - - public String getGroup() { - return group; - } - - public Socket getSocket() { - return socket; - } - - /** - * Returns a String representing the RemoteClient, format is [RemoteClient ID - * (GROUP) @ SOCKET_REMOTE_ADDRESS] - */ - @Override - public String toString() { - return "[RemoteClient: " + id + " (" + group + ") @ " + socket.getRemoteSocketAddress() + "]"; - } - } + protected static final String INTERNAL_LOGIN_ID = "_INTERNAL_LOGIN_"; + + private final ScheduledExecutorService executorService; + + protected Map idMethods = new HashMap<>(); + + protected ServerSocket server; + protected int port; + protected List clients; + protected List toBeDeleted; + + protected boolean autoRegisterEveryClient; + protected boolean secureMode; + + protected boolean stopped; + protected boolean muted; + protected long pingInterval = 30 * 1000; // 30 seconds + + /** + * Constructs a simple server listening on the given port. Every client that connects to this + * server is registered and can receive broadcast and direct messages, the connection will be kept + * alive using a ping and ssl will not be used. This constructor is deprecated! It is strongly + * recommended to substitute it with the constructor that has the option muted as its last + * parameter. + * + * @param port The port to listen on + */ + @Deprecated + public Server(int port) { + this(port, true, true, false); + } + + /** + * Constructs a simple server listening on the given port. Every client that connects to this + * server is registered and can receive broadcast and direct messages, the connection will be kept + * alive using a ping and ssl will not be used. + * + * @param port The port to listen on + * @param muted Whether the mute mode should be activated on startup + */ + public Server(int port, boolean muted) { + this(port, true, true, false, muted); + } + + /** + * Constructs a simple server with all possible configurations. This constructor is deprecated! + * It is strongly recommended to substitute it with the constructor that has the option + * muted as its last parameter. + * + * @param port The port to listen on + * @param autoRegisterEveryClient Whether a client that connects should be registered to send it + * broadcast and direct messages later + * @param keepConnectionAlive Whether the connection should be kept alive using a ping package. + * The transmission interval can be set using + * setPingInterval(int seconds). + * @param useSSL Whether SSL should be used to establish a secure connection + */ + @Deprecated + public Server(int port, boolean autoRegisterEveryClient, boolean keepConnectionAlive, + boolean useSSL) { + this(port, autoRegisterEveryClient, keepConnectionAlive, useSSL, false); + } + + /** + * Constructs a simple server with all possible configurations + * + * @param port The port to listen on + * @param autoRegisterEveryClient Whether a client that connects should be registered to send it + * broadcast and direct messages later + * @param keepConnectionAlive Whether the connection should be kept alive using a ping package. + * The transmission interval can be set using + * setPingInterval(int seconds). + * @param useSSL Whether SSL should be used to establish a secure connection + * @param muted Whether the mute mode should be activated on startup + */ + public Server(int port, boolean autoRegisterEveryClient, boolean keepConnectionAlive, + boolean useSSL, boolean muted) { + this.executorService = Executors.newSingleThreadScheduledExecutor(ServerThreadFactory.getDefault()); + this.clients = new ArrayList<>(); + this.port = port; + this.autoRegisterEveryClient = autoRegisterEveryClient; + this.muted = muted; + + this.secureMode = useSSL; + if (secureMode) { + System.setProperty("javax.net.ssl.keyStore", "ssc.store"); + System.setProperty("javax.net.ssl.keyStorePassword", "SimpleServerClient"); + } + if (autoRegisterEveryClient) { + registerLoginMethod(); + } + preStart(); + + start(); + + if (keepConnectionAlive) { + startPing(); + } + } + + /** + * Mutes the console output of this instance, stack traces will still be printed.
+ * Be careful: This will not prevent processing of messages passed to the + * onLog and onLogError methods, if they were overwritten. + * + * @param muted true if there should be no console output + */ + public void setMuted(boolean muted) { + this.muted = muted; + } + + /** + * Sets the interval in which ping packages should be sent to keep the connection alive. Default + * is 30 seconds. + * + * @param seconds The interval in which ping packages should be sent + */ + public void setPingInterval(int seconds) { + this.pingInterval = seconds * 1000; + } + + /** + * Starts the thread sending a dummy package every pingInterval seconds. Adjust the + * interval using setPingInterval(int seconds). + */ + protected void startPing() { + executorService.scheduleAtFixedRate(() -> { + if (!server.isClosed()) { + broadcastMessage(new Datapackage("_INTERNAL_PING_", "OK")); + } + }, 0, pingInterval, TimeUnit.MILLISECONDS); + } + + /** + * Starts the listening thread waiting for messages from clients + */ + protected void startListening() { + while (!Thread.interrupted() && !stopped) { + try { + // Wait for client to connect + onLog("[Server] Waiting for connection" + (secureMode ? " using SSL..." : "...")); + @SuppressWarnings("resource") final Socket tempSocket = server + .accept(); // potential resource leak, tempSocket might not be closed! + + // Read the client's message + ObjectInputStream ois = new ObjectInputStream( + new BufferedInputStream(tempSocket.getInputStream())); + Object raw = ois.readObject(); + + if (raw instanceof Datapackage) { + final Datapackage msg = (Datapackage) raw; + onLog("[Server] Message received: " + msg); + + // inspect all registered methods + for (final String current : idMethods.keySet()) { + // if the current method equals the identifier of the Datapackage... + if (msg.id().equalsIgnoreCase(current)) { + onLog("[Server] Executing method for identifier '" + msg.id() + "'"); + // execute the Executable on a new thread + new Thread(() -> { + // Run the method registered for the ID of this Datapackage + idMethods.get(current).run(msg, tempSocket); + // and close the temporary socket if it is no longer needed + if (!msg.id().equals(INTERNAL_LOGIN_ID)) { + try { + tempSocket.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + }).start(); + break; + } + } + + } + + } catch (SocketException e) { + onLog("Server stopped."); + onServerStopped(); + } catch (IllegalBlockingModeException | IOException | ClassNotFoundException e) { + e.printStackTrace(); + } + + } + } + + /** + * Sends a reply to client. This method should only be called from within the run-Method of an + * Executable implementation. + * + * @param toSocket The socket the message should be delivered to + * @param datapackageContent The content of the message to be delivered. The ID of this + * Datapackage will be "REPLY". + */ + public synchronized void sendReply(Socket toSocket, Object... datapackageContent) { + sendMessage(new RemoteClient(null, toSocket), new Datapackage("REPLY", datapackageContent)); + } + + /** + * Sends a message to a client with specified id + * + * @param remoteClientId The id of the client it registered on login + * @param datapackageId The id of message + * @param datapackageContent The content of the message + */ + public synchronized void sendMessage(String remoteClientId, String datapackageId, + Object... datapackageContent) { + sendMessage(remoteClientId, new Datapackage(datapackageId, datapackageContent)); + } + + /** + * Sends a message to a client with specified id + * + * @param remoteClientId The id of the client it registered on login + * @param message The message + */ + public synchronized void sendMessage(String remoteClientId, Datapackage message) { + for (RemoteClient current : clients) { + if (current.getId().equals(remoteClientId)) { + sendMessage(current, message); + } + } + } + + /** + * Sends a message to a client + * + * @param remoteClient The target client + * @param datapackageId The id of message + * @param datapackageContent The content of the message + */ + public synchronized void sendMessage(RemoteClient remoteClient, String datapackageId, + Object... datapackageContent) { + sendMessage(remoteClient, new Datapackage(datapackageId, datapackageContent)); + } + + /** + * Sends a message to a client + * + * @param remoteClient The target client + * @param message The message + */ + public synchronized void sendMessage(RemoteClient remoteClient, Datapackage message) { + try { + // send message + if (!remoteClient.getSocket().isConnected()) { + throw new ConnectException("Socket not connected."); + } + ObjectOutputStream out = new ObjectOutputStream( + new BufferedOutputStream(remoteClient.getSocket().getOutputStream())); + out.writeObject(message); + out.flush(); + } catch (IOException e) { + onLogError("[Server] [Send Message] Error: " + e.getMessage()); + + // if an error occured: remove client from list + toBeDeleted.add(remoteClient); + } + } + + /** + * Broadcasts a message to a group of clients + * + * @param group The group name the clients registered on their login + * @param message The message + * @return The number of clients reached + */ + public synchronized int broadcastMessageToGroup(String group, Datapackage message) { + // send message to all clients + int txCounter = 0; + for (RemoteClient current : clients) { + if (current.getGroup().equals(group)) { + sendMessage(current, message); + txCounter++; + } + } + + // remove all clients which produced errors while sending + txCounter -= toBeDeleted.size(); + removeClients(); + + return txCounter; + } + + /** + * Broadcasts a message to a group of clients + * + * @param message The message + * @return The number of clients reached + */ + public synchronized int broadcastMessage(Datapackage message) { + // send message to all clients + int txCounter = 0; + for (RemoteClient current : clients) { + sendMessage(current, message); + txCounter++; + } + + // remove all clients which produced errors while sending + removeClients(); + + return txCounter; + } + + /** + * Removes all clients in the {@link #toBeDeleted} list. + */ + private void removeClients() { + for (RemoteClient current : toBeDeleted) { + clients.remove(current); + onClientRemoved(current); + } + + toBeDeleted = new ArrayList<>(); + } + + /** + * Registers a method that will be executed if a message containing + * identifier is received + * + * @param identifier The ID of the message to proccess + * @param executable The method to be called when a message with identifier is received + */ + public void registerMethod(String identifier, Executable executable) { + if (identifier.equalsIgnoreCase(INTERNAL_LOGIN_ID) && autoRegisterEveryClient) { + throw new IllegalArgumentException("Identifier may not be '" + INTERNAL_LOGIN_ID + "'. " + + "Since v1.0.1 the server automatically registers new clients. " + + "To react on new client registed, use the onClientRegisters() listener by overwriting it."); + } + idMethods.put(identifier, executable); + } + + /** + * Registers a login handler. This method is called only if the constructor has been applied to + * register clients. + */ + protected void registerLoginMethod() { + idMethods.put(INTERNAL_LOGIN_ID, (msg, socket) -> { + if (msg.size() == 3) { + registerClient((String) msg.get(1), (String) msg.get(2), socket); + } else if (msg.size() == 2) { + registerClient((String) msg.get(1), socket); + } else { + registerClient(UUID.randomUUID().toString(), socket); + } + onClientRegistered(msg, socket); + onClientRegistered(); + }); + } + + /** + * Registers a client to allow sending it direct and broadcast messages later + * + * @param id The client's id + * @param newClientSocket The client's socket + */ + protected synchronized void registerClient(String id, Socket newClientSocket) { + clients.add(new RemoteClient(id, newClientSocket)); + } + + /** + * Registers a client to allow sending it direct and broadcast messages later + * + * @param id The client's id + * @param group The client's group name + * @param newClientSocket The client's socket + */ + protected synchronized void registerClient(String id, String group, Socket newClientSocket) { + clients.add(new RemoteClient(id, group, newClientSocket)); + } + + /** + * Starts the server. This method is automatically called after + * preStart() and starts the actual and the listening thread. + */ + protected void start() { + stopped = false; + try { + + if (secureMode) { + server = SSLServerSocketFactory.getDefault().createServerSocket(port); + } else { + server = new ServerSocket(port); + } + + } catch (IOException e) { + onLogError("Error opening ServerSocket"); + e.printStackTrace(); + } + startListening(); + } + + /** + * Stops the server + * + * @throws IOException If closing the ServerSocket fails + */ + public void stop() throws IOException { + stopped = true; + + if (!server.isClosed()) { + server.close(); + } + } + + /** + * Counts the number of clients registered + * + * @return The number of clients registered + */ + public synchronized int getClientCount() { + return clients.size(); + } + + /** + * Checks whether a RemoteClient with the given ID is currently connected to the server + * + * @param clientId The clients ID + * @return true, if a RemoteClient with ID clientId is connected to the server + */ + public boolean isClientIdConnected(String clientId) { + // Iterate all clients connected + for (RemoteClient c : clients) { + // Check client exists and its socket is connected + if (c.getId().equals(clientId) && c.getSocket().isConnected()) { + return true; + } + } + return false; + } + + /** + * Checks whether any client is currently connected to the server + * + * @return true, if at least one client is connected to the server + */ + public boolean isAnyClientConnected() { + return getClientCount() > 0; + } + + /** + * Called just before the actual server starts. Register your handler methods in here using + * registerMethod(String identifier, Executable executable)! + */ + public abstract void preStart(); + + /** + * Called on the listener's main thread when a new client registers + */ + public void onClientRegistered() { + // Overwrite this method when extending this class + } + + /** + * Called on the listener's main thread when a new client registers + * + * @param msg The message the client registered with + * @param socket The socket the client registered with. Be careful with this! You should not close + * this socket, because the server should have stored it normally to reach this client later. + */ + public void onClientRegistered(Datapackage msg, Socket socket) { + // Overwrite this method when extending this class + } + + /** + * Called on the listener's main thread when a client is removed from the list. This normally + * happens if there was a problem with its connection. You should wait for the client to connect + * again. + * + * @param remoteClient The client that was removed from the list of reachable clients + */ + public void onClientRemoved(RemoteClient remoteClient) { + // Overwrite this method when extending this class + } + + /** + * Called when the server finally stops after the stop () method has been called. + */ + public void onServerStopped() { + // Overwrite this method when extending this class + } + + /** + * By default, this method is called whenever an output is to be made. If this method is not + * overwritten, the output is passed to the system's default output stream (if output is not + * muted).
Error messages are passed to the onLogError event listener.
+ * Override this method to catch and process the message in a custom way. + * + * @param message The content of the output to be made + */ + public void onLog(String message) { + if (!muted) { + System.out.println(message); + } + } + + /** + * By default, this method is called whenever an error output is to be made. If this method is not + * overwritten, the output is passed to the system's default error output stream (if output is not + * muted).
Non-error messages are passed to the onLog event listener.
+ * Override this method to catch and process the message in a custom way. + * + * @param message The content of the error output to be made + */ + public void onLogError(String message) { + if (!muted) { + System.err.println(message); + } + } + + /** + * A RemoteClient representating a client connected to this server storing an id for + * identification and a socket for communication. + */ + private class RemoteClient { + + private String id; + private String group; + private Socket socket; + + /** + * Creates a RemoteClient representating a client connected to this server storing an id for + * identification and a socket for communication. The client will be member of the default + * group. + * + * @param id The clients id (to use for identification; choose a custom String) + * @param socket The socket (to use for communication) + */ + public RemoteClient(String id, Socket socket) { + Objects.requireNonNull(socket, "Socket can not be null"); + this.id = id; + this.group = "_DEFAULT_GROUP_"; + this.socket = socket; + } + + /** + * Creates a RemoteClient representating a client connected to this server storing an id for + * identification and a socket for communication. The client can be set as a member of a group + * of clients to receive messages broadcasted to a group. + * + * @param id The clients id (to use for identification; choose a custom String) + * @param group The group the client is member of + * @param socket The socket (to use for communication) + */ + public RemoteClient(String id, String group, Socket socket) { + this.id = id; + this.group = group; + this.socket = socket; + } + + public String getId() { + return id; + } + + public String getGroup() { + return group; + } + + public Socket getSocket() { + return socket; + } + + /** + * Returns a String representing the RemoteClient, format is [RemoteClient ID (GROUP) @ + * SOCKET_REMOTE_ADDRESS] + */ + @Override + public String toString() { + return "[RemoteClient: " + id + " (" + group + ") @ " + socket.getRemoteSocketAddress() + "]"; + } + } } \ No newline at end of file diff --git a/src/com/blogspot/debukkitsblog/net/ServerThreadFactory.java b/src/com/blogspot/debukkitsblog/net/ServerThreadFactory.java new file mode 100644 index 0000000..571f73a --- /dev/null +++ b/src/com/blogspot/debukkitsblog/net/ServerThreadFactory.java @@ -0,0 +1,30 @@ +package com.blogspot.debukkitsblog.net; + +import java.util.concurrent.ThreadFactory; + +/** + * https://github.com/Stupremee + * + * @author Stu + * @since 20.03.2019 + */ +public class ServerThreadFactory implements ThreadFactory { + + private static final class Lazy { + private static ServerThreadFactory INSTANCE = new ServerThreadFactory(); + } + + private int threadCount = 0; + + private ServerThreadFactory() { + } + + @Override + public Thread newThread(Runnable r) { + return new Thread(r, "ServerThread-" + threadCount++); + } + + public static ThreadFactory getDefault() { + return Lazy.INSTANCE; + } +}