Added lots of javadoc.
This commit is contained in:
parent
96fd07b3fe
commit
2d8a0967dc
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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()) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue