Added lots of javadoc.

This commit is contained in:
Andrew Lalis 2021-09-08 13:32:56 +02:00
parent 96fd07b3fe
commit 2d8a0967dc
10 changed files with 161 additions and 30 deletions

View File

@ -29,6 +29,7 @@ public class ConcordClient implements Runnable {
private final Socket socket; private final Socket socket;
private final DataInputStream in; private final DataInputStream in;
private final DataOutputStream out; private final DataOutputStream out;
private final Serializer serializer;
@Getter @Getter
private final ClientModel model; private final ClientModel model;
@ -42,16 +43,8 @@ public class ConcordClient implements Runnable {
this.socket = new Socket(host, port); this.socket = new Socket(host, port);
this.in = new DataInputStream(this.socket.getInputStream()); this.in = new DataInputStream(this.socket.getInputStream());
this.out = new DataOutputStream(this.socket.getOutputStream()); this.out = new DataOutputStream(this.socket.getOutputStream());
Serializer.writeMessage(new Identification(nickname), this.out); this.serializer = new Serializer();
Message reply = Serializer.readMessage(this.in); this.model = this.initializeConnectionToServer(nickname);
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.");
}
// Add event listeners. // Add event listeners.
this.eventManager.addHandler(MoveToChannel.class, new ChannelMovedHandler()); this.eventManager.addHandler(MoveToChannel.class, new ChannelMovedHandler());
@ -61,12 +54,38 @@ public class ConcordClient implements Runnable {
this.eventManager.addHandler(ServerMetaData.class, new ServerMetaDataHandler()); 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 { 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 { 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() { public void shutdown() {
@ -85,7 +104,7 @@ public class ConcordClient implements Runnable {
this.running = true; this.running = true;
while (this.running) { while (this.running) {
try { try {
Message msg = Serializer.readMessage(this.in); Message msg = this.serializer.readMessage(this.in);
this.eventManager.handle(msg); this.eventManager.handle(msg);
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();

View File

@ -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.ChatHistoryRequest;
import nl.andrewl.concord_core.msg.types.MoveToChannel; 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<MoveToChannel> { public class ChannelMovedHandler implements MessageHandler<MoveToChannel> {
@Override @Override
public void handle(MoveToChannel msg, ConcordClient client) throws Exception { public void handle(MoveToChannel msg, ConcordClient client) throws Exception {

View File

@ -4,6 +4,10 @@ import nl.andrewl.concord_client.ConcordClient;
import nl.andrewl.concord_client.event.MessageHandler; import nl.andrewl.concord_client.event.MessageHandler;
import nl.andrewl.concord_core.msg.types.ChannelUsersResponse; 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<ChannelUsersResponse> { public class ChannelUsersResponseHandler implements MessageHandler<ChannelUsersResponse> {
@Override @Override
public void handle(ChannelUsersResponse msg, ConcordClient client) throws Exception { public void handle(ChannelUsersResponse msg, ConcordClient client) throws Exception {

View File

@ -11,9 +11,23 @@ import java.util.Map;
* This class is responsible for reading and writing messages from streams. * This class is responsible for reading and writing messages from streams.
*/ */
public class Serializer { public class Serializer {
private static final Map<Byte, Class<? extends Message>> messageTypes = new HashMap<>(); /**
private static final Map<Class<? extends Message>, Byte> inverseMessageTypes = new HashMap<>(); * The mapping which defines each supported message type and the byte value
static { * used to identify it when reading and writing messages.
*/
private final Map<Byte, Class<? extends Message>> 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<Class<? extends Message>, Byte> inverseMessageTypes = new HashMap<>();
/**
* Constructs a new serializer instance, with a standard set of supported
* message types.
*/
public Serializer() {
registerType(0, Identification.class); registerType(0, Identification.class);
registerType(1, ServerWelcome.class); registerType(1, ServerWelcome.class);
registerType(2, Chat.class); registerType(2, Chat.class);
@ -26,12 +40,29 @@ public class Serializer {
registerType(9, Error.class); registerType(9, Error.class);
} }
private static void registerType(int id, Class<? extends Message> clazz) { /**
messageTypes.put((byte) id, clazz); * Helper method which registers a message type to be supported by the
inverseMessageTypes.put(clazz, (byte) id); * 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<? extends Message> 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); DataInputStream d = new DataInputStream(i);
byte type = d.readByte(); byte type = d.readByte();
var clazz = messageTypes.get(type); 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); DataOutputStream d = new DataOutputStream(o);
Byte type = inverseMessageTypes.get(msg.getClass()); Byte type = inverseMessageTypes.get(msg.getClass());
if (type == null) { if (type == null) {

View File

@ -12,6 +12,12 @@ import java.util.List;
import static nl.andrewl.concord_core.msg.MessageUtils.*; 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 @Data
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor

View File

@ -19,6 +19,12 @@ import static nl.andrewl.concord_core.msg.MessageUtils.*;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class Error implements Message { 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} public enum Level {WARNING, ERROR}
private Level level; private Level level;

View File

@ -1,5 +1,6 @@
/** /**
* This package contains all the components needed by both the server and the * 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; package nl.andrewl.concord_core;

View File

@ -37,6 +37,10 @@ public class Channel {
this.initCollection(); this.initCollection();
} }
/**
* Initializes this channel's nitrite database collection, which involves
* creating any indexes that don't yet exist.
*/
private void initCollection() { private void initCollection() {
if (!this.messageCollection.hasIndex("timestamp")) { if (!this.messageCollection.hasIndex("timestamp")) {
System.out.println("Adding index on \"timestamp\" field to collection " + this.messageCollection.getName()); 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) { public void addClient(ClientThread clientThread) {
this.connectedClients.add(clientThread); this.connectedClients.add(clientThread);
try { 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) { public void removeClient(ClientThread clientThread) {
this.connectedClients.remove(clientThread); this.connectedClients.remove(clientThread);
try { try {
@ -79,19 +93,25 @@ public class Channel {
/** /**
* Sends a message to all clients that are currently connected to this * 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. * @param msg The message to send.
* @throws IOException If an error occurs. * @throws IOException If an error occurs.
*/ */
public void sendMessage(Message msg) throws IOException { public void sendMessage(Message msg) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream(msg.getByteCount() + 1); ByteArrayOutputStream baos = new ByteArrayOutputStream(msg.getByteCount() + 1);
Serializer.writeMessage(msg, baos); this.server.getSerializer().writeMessage(msg, baos);
byte[] data = baos.toByteArray(); byte[] data = baos.toByteArray();
for (var client : this.connectedClients) { for (var client : this.connectedClients) {
client.sendToClient(data); client.sendToClient(data);
} }
} }
/**
* Gets a list of information about each user in this channel.
* @return A list of {@link UserData} objects.
*/
public List<UserData> getUserData() { public List<UserData> getUserData() {
List<UserData> users = new ArrayList<>(this.connectedClients.size()); List<UserData> users = new ArrayList<>(this.connectedClients.size());
for (var clientThread : this.getConnectedClients()) { for (var clientThread : this.getConnectedClients()) {

View File

@ -3,7 +3,6 @@ package nl.andrewl.concord_server;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import nl.andrewl.concord_core.msg.Message; 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.Identification;
import nl.andrewl.concord_core.msg.types.UserData; import nl.andrewl.concord_core.msg.types.UserData;
@ -37,6 +36,13 @@ public class ClientThread extends Thread {
private volatile boolean running; 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 { public ClientThread(Socket socket, ConcordServer server) throws IOException {
this.socket = socket; this.socket = socket;
this.server = server; this.server = server;
@ -44,14 +50,24 @@ public class ClientThread extends Thread {
this.out = new DataOutputStream(socket.getOutputStream()); 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) { public synchronized void sendToClient(Message message) {
try { try {
Serializer.writeMessage(message, this.out); this.server.getSerializer().writeMessage(message, this.out);
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); 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) { public synchronized void sendToClient(byte[] bytes) {
try { try {
this.out.write(bytes); 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() { public void shutdown() {
try { try {
this.socket.close(); this.socket.close();
@ -80,7 +101,7 @@ public class ClientThread extends Thread {
while (this.running) { while (this.running) {
try { try {
var msg = Serializer.readMessage(this.in); var msg = this.server.getSerializer().readMessage(this.in);
this.server.getEventManager().handle(msg, this); this.server.getEventManager().handle(msg, this);
} catch (IOException e) { } catch (IOException e) {
this.running = false; this.running = false;
@ -110,7 +131,7 @@ public class ClientThread extends Thread {
int attempts = 0; int attempts = 0;
while (attempts < 5) { while (attempts < 5) {
try { try {
var msg = Serializer.readMessage(this.in); var msg = this.server.getSerializer().readMessage(this.in);
if (msg instanceof Identification id) { if (msg instanceof Identification id) {
this.server.registerClient(id, this); this.server.registerClient(id, this);
return true; return true;

View File

@ -34,6 +34,14 @@ public class ConcordServer implements Runnable {
private volatile boolean running; private volatile boolean running;
private final ServerSocket serverSocket; 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 configuration data. This is used to define channels, discovery
* server addresses, and more. * server addresses, and more.
@ -83,6 +91,7 @@ public class ConcordServer implements Runnable {
this.eventManager = new EventManager(this); this.eventManager = new EventManager(this);
this.channelManager = new ChannelManager(this); this.channelManager = new ChannelManager(this);
this.serverSocket = new ServerSocket(this.config.getPort()); 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. * @param message The message to send.
*/ */
public void broadcast(Message message) { public void broadcast(Message message) {
ByteArrayOutputStream baos = new ByteArrayOutputStream(message.getByteCount()); ByteArrayOutputStream baos = new ByteArrayOutputStream(message.getByteCount());
try { try {
Serializer.writeMessage(message, baos); this.serializer.writeMessage(message, baos);
byte[] data = baos.toByteArray(); byte[] data = baos.toByteArray();
for (var client : this.clients.values()) { for (var client : this.clients.values()) {
client.sendToClient(data); client.sendToClient(data);