From 2d8a0967dc30bb98fcafbdca39bcf665cd42f28a Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Wed, 8 Sep 2021 13:32:56 +0200 Subject: [PATCH] Added lots of javadoc. --- .../andrewl/concord_client/ConcordClient.java | 45 +++++++++++----- .../event/handlers/ChannelMovedHandler.java | 6 +++ .../handlers/ChannelUsersResponseHandler.java | 4 ++ .../andrewl/concord_core/msg/Serializer.java | 54 ++++++++++++++++--- .../msg/types/ChannelUsersResponse.java | 6 +++ .../andrewl/concord_core/msg/types/Error.java | 6 +++ .../nl/andrewl/concord_core/package-info.java | 3 +- .../nl/andrewl/concord_server/Channel.java | 24 ++++++++- .../andrewl/concord_server/ClientThread.java | 29 ++++++++-- .../andrewl/concord_server/ConcordServer.java | 14 ++++- 10 files changed, 161 insertions(+), 30 deletions(-) diff --git a/client/src/main/java/nl/andrewl/concord_client/ConcordClient.java b/client/src/main/java/nl/andrewl/concord_client/ConcordClient.java index 9e85986..2a2f3f9 100644 --- a/client/src/main/java/nl/andrewl/concord_client/ConcordClient.java +++ b/client/src/main/java/nl/andrewl/concord_client/ConcordClient.java @@ -29,6 +29,7 @@ public class ConcordClient implements Runnable { private final Socket socket; private final DataInputStream in; private final DataOutputStream out; + private final Serializer serializer; @Getter private final ClientModel model; @@ -42,16 +43,8 @@ public class ConcordClient implements Runnable { this.socket = new Socket(host, port); this.in = new DataInputStream(this.socket.getInputStream()); this.out = new DataOutputStream(this.socket.getOutputStream()); - Serializer.writeMessage(new Identification(nickname), this.out); - Message reply = Serializer.readMessage(this.in); - if (reply instanceof ServerWelcome welcome) { - this.model = new ClientModel(welcome.getClientId(), nickname, welcome.getCurrentChannelId(), welcome.getMetaData()); - // Start fetching initial data for the channel we were initially put into. - this.sendMessage(new ChannelUsersRequest(this.model.getCurrentChannelId())); - this.sendMessage(new ChatHistoryRequest(this.model.getCurrentChannelId(), "")); - } else { - throw new IOException("Unexpected response from the server after sending identification message."); - } + this.serializer = new Serializer(); + this.model = this.initializeConnectionToServer(nickname); // Add event listeners. this.eventManager.addHandler(MoveToChannel.class, new ChannelMovedHandler()); @@ -61,12 +54,38 @@ public class ConcordClient implements Runnable { this.eventManager.addHandler(ServerMetaData.class, new ServerMetaDataHandler()); } + /** + * Initializes the communication with the server by sending an {@link Identification} + * message, and waiting for a {@link ServerWelcome} response from the + * server. After that, we request some information about the channel we were + * placed in by the server. + * @param nickname The nickname to send to the server that it should know + * us by. + * @return The client model that contains the server's metadata and other + * information that should be kept up-to-date at runtime. + * @throws IOException If an error occurs while reading or writing the + * messages, or if the server sends an unexpected response. + */ + private ClientModel initializeConnectionToServer(String nickname) throws IOException { + this.serializer.writeMessage(new Identification(nickname), this.out); + Message reply = this.serializer.readMessage(this.in); + if (reply instanceof ServerWelcome welcome) { + var model = new ClientModel(welcome.getClientId(), nickname, welcome.getCurrentChannelId(), welcome.getMetaData()); + // Start fetching initial data for the channel we were initially put into. + this.sendMessage(new ChannelUsersRequest(this.model.getCurrentChannelId())); + this.sendMessage(new ChatHistoryRequest(this.model.getCurrentChannelId(), "")); + return model; + } else { + throw new IOException("Unexpected response from the server after sending identification message."); + } + } + public void sendMessage(Message message) throws IOException { - Serializer.writeMessage(message, this.out); + this.serializer.writeMessage(message, this.out); } public void sendChat(String message) throws IOException { - Serializer.writeMessage(new Chat(this.model.getId(), this.model.getNickname(), System.currentTimeMillis(), message), this.out); + this.serializer.writeMessage(new Chat(this.model.getId(), this.model.getNickname(), System.currentTimeMillis(), message), this.out); } public void shutdown() { @@ -85,7 +104,7 @@ public class ConcordClient implements Runnable { this.running = true; while (this.running) { try { - Message msg = Serializer.readMessage(this.in); + Message msg = this.serializer.readMessage(this.in); this.eventManager.handle(msg); } catch (IOException e) { e.printStackTrace(); diff --git a/client/src/main/java/nl/andrewl/concord_client/event/handlers/ChannelMovedHandler.java b/client/src/main/java/nl/andrewl/concord_client/event/handlers/ChannelMovedHandler.java index e59af02..5ca7ae0 100644 --- a/client/src/main/java/nl/andrewl/concord_client/event/handlers/ChannelMovedHandler.java +++ b/client/src/main/java/nl/andrewl/concord_client/event/handlers/ChannelMovedHandler.java @@ -6,6 +6,12 @@ import nl.andrewl.concord_core.msg.types.ChannelUsersRequest; import nl.andrewl.concord_core.msg.types.ChatHistoryRequest; import nl.andrewl.concord_core.msg.types.MoveToChannel; +/** + * When the client receives a {@link MoveToChannel} message, it means that the + * server has told the client that it has been moved to the indicated channel. + * Thus, the client must now update its model and request the relevant info from + * the server about the new channel it's in. + */ public class ChannelMovedHandler implements MessageHandler { @Override public void handle(MoveToChannel msg, ConcordClient client) throws Exception { diff --git a/client/src/main/java/nl/andrewl/concord_client/event/handlers/ChannelUsersResponseHandler.java b/client/src/main/java/nl/andrewl/concord_client/event/handlers/ChannelUsersResponseHandler.java index ef79234..5cbe745 100644 --- a/client/src/main/java/nl/andrewl/concord_client/event/handlers/ChannelUsersResponseHandler.java +++ b/client/src/main/java/nl/andrewl/concord_client/event/handlers/ChannelUsersResponseHandler.java @@ -4,6 +4,10 @@ import nl.andrewl.concord_client.ConcordClient; import nl.andrewl.concord_client.event.MessageHandler; import nl.andrewl.concord_core.msg.types.ChannelUsersResponse; +/** + * When the client receives information about the list of known users, it will + * update its model to show the new list. + */ public class ChannelUsersResponseHandler implements MessageHandler { @Override public void handle(ChannelUsersResponse msg, ConcordClient client) throws Exception { diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/Serializer.java b/core/src/main/java/nl/andrewl/concord_core/msg/Serializer.java index 9e9c924..180e2c9 100644 --- a/core/src/main/java/nl/andrewl/concord_core/msg/Serializer.java +++ b/core/src/main/java/nl/andrewl/concord_core/msg/Serializer.java @@ -11,9 +11,23 @@ import java.util.Map; * This class is responsible for reading and writing messages from streams. */ public class Serializer { - private static final Map> messageTypes = new HashMap<>(); - private static final Map, Byte> inverseMessageTypes = new HashMap<>(); - static { + /** + * The mapping which defines each supported message type and the byte value + * used to identify it when reading and writing messages. + */ + private final Map> messageTypes = new HashMap<>(); + + /** + * An inverse of {@link Serializer#messageTypes} which is used to look up a + * message's byte value when you know the class of the message. + */ + private final Map, Byte> inverseMessageTypes = new HashMap<>(); + + /** + * Constructs a new serializer instance, with a standard set of supported + * message types. + */ + public Serializer() { registerType(0, Identification.class); registerType(1, ServerWelcome.class); registerType(2, Chat.class); @@ -26,12 +40,29 @@ public class Serializer { registerType(9, Error.class); } - private static void registerType(int id, Class clazz) { - messageTypes.put((byte) id, clazz); - inverseMessageTypes.put(clazz, (byte) id); + /** + * Helper method which registers a message type to be supported by the + * serializer, by adding it to the normal and inverse mappings. + * @param id The byte which will be used to identify messages of the given + * class. The value should from 0 to 127. + * @param messageClass The class of message which is registered with the + * given byte identifier. + */ + private synchronized void registerType(int id, Class messageClass) { + messageTypes.put((byte) id, messageClass); + inverseMessageTypes.put(messageClass, (byte) id); } - public static Message readMessage(InputStream i) throws IOException { + /** + * Reads a message from the given input stream and returns it, or throws an + * exception if an error occurred while reading from the stream. + * @param i The input stream to read from. + * @return The message which was read. + * @throws IOException If an error occurs while reading, such as trying to + * read an unsupported message type, or if a message object could not be + * constructed for the incoming data. + */ + public Message readMessage(InputStream i) throws IOException { DataInputStream d = new DataInputStream(i); byte type = d.readByte(); var clazz = messageTypes.get(type); @@ -48,7 +79,14 @@ public class Serializer { } } - public static void writeMessage(Message msg, OutputStream o) throws IOException { + /** + * Writes a message to the given output stream. + * @param msg The message to write. + * @param o The output stream to write to. + * @throws IOException If an error occurs while writing, or if the message + * to write is not supported by this serializer. + */ + public void writeMessage(Message msg, OutputStream o) throws IOException { DataOutputStream d = new DataOutputStream(o); Byte type = inverseMessageTypes.get(msg.getClass()); if (type == null) { diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/ChannelUsersResponse.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/ChannelUsersResponse.java index c9c6d28..68ee08c 100644 --- a/core/src/main/java/nl/andrewl/concord_core/msg/types/ChannelUsersResponse.java +++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/ChannelUsersResponse.java @@ -12,6 +12,12 @@ import java.util.List; import static nl.andrewl.concord_core.msg.MessageUtils.*; +/** + * This message is sent from the server to the client when the information about + * the users in the channel that a client is in has changed. For example, when + * a user leaves a channel, all others in that channel will be sent this message + * to indicate that update. + */ @Data @NoArgsConstructor @AllArgsConstructor diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/Error.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/Error.java index 052b335..07de9e0 100644 --- a/core/src/main/java/nl/andrewl/concord_core/msg/types/Error.java +++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/Error.java @@ -19,6 +19,12 @@ import static nl.andrewl.concord_core.msg.MessageUtils.*; @NoArgsConstructor @AllArgsConstructor public class Error implements Message { + /** + * The error level gives an indication as to the severity of the error. + * Warnings indicate that a user has attempted to do something which they + * shouldn't, or some scenario which is not ideal but recoverable from. + * Errors indicate actual issues with the software which should be addressed. + */ public enum Level {WARNING, ERROR} private Level level; diff --git a/core/src/main/java/nl/andrewl/concord_core/package-info.java b/core/src/main/java/nl/andrewl/concord_core/package-info.java index e94402b..f4e9900 100644 --- a/core/src/main/java/nl/andrewl/concord_core/package-info.java +++ b/core/src/main/java/nl/andrewl/concord_core/package-info.java @@ -1,5 +1,6 @@ /** * This package contains all the components needed by both the server and the - * client. + * client. What that entails is mostly the communication infrastructure which + * they both share. */ package nl.andrewl.concord_core; \ No newline at end of file diff --git a/server/src/main/java/nl/andrewl/concord_server/Channel.java b/server/src/main/java/nl/andrewl/concord_server/Channel.java index 18eb515..7914f0a 100644 --- a/server/src/main/java/nl/andrewl/concord_server/Channel.java +++ b/server/src/main/java/nl/andrewl/concord_server/Channel.java @@ -37,6 +37,10 @@ public class Channel { this.initCollection(); } + /** + * Initializes this channel's nitrite database collection, which involves + * creating any indexes that don't yet exist. + */ private void initCollection() { if (!this.messageCollection.hasIndex("timestamp")) { System.out.println("Adding index on \"timestamp\" field to collection " + this.messageCollection.getName()); @@ -59,6 +63,11 @@ public class Channel { } } + /** + * Adds a client to this channel. Also sends an update to all clients, + * including the new one, telling them that a user has joined. + * @param clientThread The client to add. + */ public void addClient(ClientThread clientThread) { this.connectedClients.add(clientThread); try { @@ -68,6 +77,11 @@ public class Channel { } } + /** + * Removes a client from this channel. Also sends an update to all the + * clients that are still connected, telling them that a user has left. + * @param clientThread The client to remove. + */ public void removeClient(ClientThread clientThread) { this.connectedClients.remove(clientThread); try { @@ -79,19 +93,25 @@ public class Channel { /** * Sends a message to all clients that are currently connected to this - * channel. + * channel. Makes use of the server's serializer to preemptively serialize + * the data once, so that clients need only write a byte array to their + * respective output streams. * @param msg The message to send. * @throws IOException If an error occurs. */ public void sendMessage(Message msg) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(msg.getByteCount() + 1); - Serializer.writeMessage(msg, baos); + this.server.getSerializer().writeMessage(msg, baos); byte[] data = baos.toByteArray(); for (var client : this.connectedClients) { client.sendToClient(data); } } + /** + * Gets a list of information about each user in this channel. + * @return A list of {@link UserData} objects. + */ public List getUserData() { List users = new ArrayList<>(this.connectedClients.size()); for (var clientThread : this.getConnectedClients()) { diff --git a/server/src/main/java/nl/andrewl/concord_server/ClientThread.java b/server/src/main/java/nl/andrewl/concord_server/ClientThread.java index 10a4339..7eba133 100644 --- a/server/src/main/java/nl/andrewl/concord_server/ClientThread.java +++ b/server/src/main/java/nl/andrewl/concord_server/ClientThread.java @@ -3,7 +3,6 @@ package nl.andrewl.concord_server; import lombok.Getter; import lombok.Setter; import nl.andrewl.concord_core.msg.Message; -import nl.andrewl.concord_core.msg.Serializer; import nl.andrewl.concord_core.msg.types.Identification; import nl.andrewl.concord_core.msg.types.UserData; @@ -37,6 +36,13 @@ public class ClientThread extends Thread { private volatile boolean running; + /** + * Constructs a new client thread. + * @param socket The socket to use to communicate with the client. + * @param server The server to which this thread belongs. + * @throws IOException If we cannot obtain the input and output streams from + * the socket. + */ public ClientThread(Socket socket, ConcordServer server) throws IOException { this.socket = socket; this.server = server; @@ -44,14 +50,24 @@ public class ClientThread extends Thread { this.out = new DataOutputStream(socket.getOutputStream()); } + /** + * Sends the given message to the client. Note that this method is + * synchronized, such that multiple messages cannot be sent simultaneously. + * @param message The message to send. + */ public synchronized void sendToClient(Message message) { try { - Serializer.writeMessage(message, this.out); + this.server.getSerializer().writeMessage(message, this.out); } catch (IOException e) { e.printStackTrace(); } } + /** + * Sends the given bytes to the client. This is a shortcut for {@link ClientThread#sendToClient(Message)} + * which can be used to optimize message sending in certain instances. + * @param bytes The bytes to send. + */ public synchronized void sendToClient(byte[] bytes) { try { this.out.write(bytes); @@ -61,6 +77,11 @@ public class ClientThread extends Thread { } } + /** + * Shuts down this client thread, closing the underlying socket and setting + * {@link ClientThread#running} to false so that the main thread loop will + * exit shortly. + */ public void shutdown() { try { this.socket.close(); @@ -80,7 +101,7 @@ public class ClientThread extends Thread { while (this.running) { try { - var msg = Serializer.readMessage(this.in); + var msg = this.server.getSerializer().readMessage(this.in); this.server.getEventManager().handle(msg, this); } catch (IOException e) { this.running = false; @@ -110,7 +131,7 @@ public class ClientThread extends Thread { int attempts = 0; while (attempts < 5) { try { - var msg = Serializer.readMessage(this.in); + var msg = this.server.getSerializer().readMessage(this.in); if (msg instanceof Identification id) { this.server.registerClient(id, this); return true; diff --git a/server/src/main/java/nl/andrewl/concord_server/ConcordServer.java b/server/src/main/java/nl/andrewl/concord_server/ConcordServer.java index 10fe69d..56db50e 100644 --- a/server/src/main/java/nl/andrewl/concord_server/ConcordServer.java +++ b/server/src/main/java/nl/andrewl/concord_server/ConcordServer.java @@ -34,6 +34,14 @@ public class ConcordServer implements Runnable { private volatile boolean running; private final ServerSocket serverSocket; + /** + * A utility serializer that's mostly used when preparing a message to + * broadcast to a set of users, which is more efficient than having each + * individual client thread serialize the same message before sending it. + */ + @Getter + private final Serializer serializer; + /** * Server configuration data. This is used to define channels, discovery * server addresses, and more. @@ -83,6 +91,7 @@ public class ConcordServer implements Runnable { this.eventManager = new EventManager(this); this.channelManager = new ChannelManager(this); this.serverSocket = new ServerSocket(this.config.getPort()); + this.serializer = new Serializer(); } /** @@ -164,13 +173,14 @@ public class ConcordServer implements Runnable { } /** - * Sends a message to every connected client. + * Sends a message to every connected client, ignoring any channels. All + * clients connected to this server will receive this message. * @param message The message to send. */ public void broadcast(Message message) { ByteArrayOutputStream baos = new ByteArrayOutputStream(message.getByteCount()); try { - Serializer.writeMessage(message, baos); + this.serializer.writeMessage(message, baos); byte[] data = baos.toByteArray(); for (var client : this.clients.values()) { client.sendToClient(data);