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;
+ }
+}