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 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();

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.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> {
@Override
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_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> {
@Override
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.
*/
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<>();
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<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(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<? extends Message> 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<? 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);
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) {

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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<UserData> getUserData() {
List<UserData> users = new ArrayList<>(this.connectedClients.size());
for (var clientThread : this.getConnectedClients()) {

View File

@ -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;

View File

@ -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);