Feat/client network #192

Merged
m.ginkel merged 5 commits from feat/client-network into main 2026-03-16 14:04:25 +01:00
6 changed files with 163 additions and 60 deletions
Showing only changes of commit 98df1dc227 - Show all commits
@@ -59,7 +59,28 @@ public class ClientService {
} }
/** /**
* Sends a Request to get all the messages, the other clients sent, and that the client is not currently aware of. * Sends a ping to the server
* Returns nothing, the method processMessage() already handles the cases "+OK" or "-ERROR"
*/
public void ping() {
processMessage("PING");
}
/**
* Sends a login request to the server to create a new user on the server
* @param username
* @return - a new username, if the same username is already used by someone else
*/
public String login(String username) {
String msg = "LOGIN USERNAME=" + username;
return processMessage(msg);
}
/**
* Send a Request to get the number of Messages currently in the Queue for the client.
* Then proceeds, if needed, to get the messages by sending
*/ */
public List<Message> getMessages() { public List<Message> getMessages() {
@@ -77,11 +98,26 @@ public class ClientService {
return messages; return messages;
} }
public String sendMessage(Message message) { /**
* Sends a Request to the Server containing all relevant information of the message the client wrote.
* @param message
* @return
*/
public void sendMessage(Message message) {
String request = "SEND_MESSAGE " + message.toArgsString(); String request = "SEND_MESSAGE " + message.toArgsString();
return processMessage(request); processMessage(request);
} }
/**
* Sends the Requests to the server and waits for the response
* If the response is "+OK" it proceeds normal
* If the response "-ERROR" it throws a runtime exception
* @param message
* @return - The response as a string, if it has to be returned
* (+OK will not be returned)
*/
private String processMessage(String message) { private String processMessage(String message) {
AtomicReference<String> response = new AtomicReference<>(); AtomicReference<String> response = new AtomicReference<>();
sendRequest(() -> { sendRequest(() -> {
@@ -93,7 +129,7 @@ public class ClientService {
System.out.println("Raw message '" + responseLine + "'"); System.out.println("Raw message '" + responseLine + "'");
if ("+OK".equals(responseLine)) { if ("+OK".equals(responseLine)) {
return; return;
} else if ("-FAIL".equals(responseLine)) { } else if (("-ERROR").equals(responseLine)) {
throw new RuntimeException(responseLine); throw new RuntimeException(responseLine);
} }
response.set(responseLine); response.set(responseLine);
@@ -105,6 +141,11 @@ public class ClientService {
return response.get(); return response.get();
} }
/**
*
* @param request
*/
private void sendRequest(Runnable request) { private void sendRequest(Runnable request) {
Future<?> future = executor.submit(request); Future<?> future = executor.submit(request);
try { try {
@@ -116,6 +157,12 @@ public class ClientService {
} }
} }
/**
* Returns a Runtime Exceptions thrown by the sendRequest method
* @param e - an Exception
* @return - a Runtime Exception
*/
private static RuntimeException getRuntimeException(Exception e) { private static RuntimeException getRuntimeException(Exception e) {
Throwable reason = e.getCause(); Throwable reason = e.getCause();
RuntimeException re; RuntimeException re;
@@ -127,6 +174,7 @@ public class ClientService {
re = new RuntimeException(reason); re = new RuntimeException(reason);
return re; return re;
} }
/** /**
* Closes the Socket and shuts down the Threadpool associated with that Socket-Connection. * Closes the Socket and shuts down the Threadpool associated with that Socket-Connection.
*/ */
@@ -142,6 +190,12 @@ public class ClientService {
} }
/**
* Method to write with the tcp transport to the server
* @param s - Message to be sent
* @throws IOException
*/
private void writeToTransport(String s) throws IOException { private void writeToTransport(String s) throws IOException {
int id = this.idGenerator.incrementAndGet(); int id = this.idGenerator.incrementAndGet();
this.clienttcptransport.write(new RawPacket(id, s)); this.clienttcptransport.write(new RawPacket(id, s));
@@ -1,47 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.client.network;
import java.io.BufferedReader;
import java.util.concurrent.Callable;
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
import java.util.ArrayList;
import java.io.*;
public class sendRequest implements Runnable {
final private String request;
final private BufferedReader input;
final private BufferedWriter output;
public sendRequest(String request, BufferedReader input, BufferedWriter output) {
this.request = request;
this.input = input;
this.output = output;
}
@Override
public void run() {
try {
output.write(request + "\r\n");
output.flush();
//System.out.println("Writing following request: " + request);
} catch (Exception e) {
System.out.println(e);
}
try {
while (true) {
String line;
line = input.readLine();
if (line.toLowerCase().startsWith("+ok")) {
break;
}
ClientService.response.add(line);
}
} catch (Exception e) {
System.out.println(e);
}
}
}
@@ -9,6 +9,11 @@ import java.util.ArrayDeque;
public class ChatHandler { public class ChatHandler {
private final EventBus eventBus; private final EventBus eventBus;
private final ArrayDeque<Message> receivedMessages = new ArrayDeque<>(); private final ArrayDeque<Message> receivedMessages = new ArrayDeque<>();
/**
* Creates a ChatHandler for a Session
* @param eventBus
*/
public ChatHandler(EventBus eventBus) { public ChatHandler(EventBus eventBus) {
eventBus.subscribe(ChatMessageEvent.class, (event) -> { eventBus.subscribe(ChatMessageEvent.class, (event) -> {
processMessageEvent(event.getMessage()); processMessageEvent(event.getMessage());
@@ -16,18 +21,34 @@ public class ChatHandler {
this.eventBus = eventBus; this.eventBus = eventBus;
} }
/**
* Sends the Chat Message that the server received to the queue
* @param message
*/
private synchronized void processMessageEvent(Message message) { private synchronized void processMessageEvent(Message message) {
// TODO: filter // TODO: filter for Messages that were sent by the Client
this.receivedMessages.offer(message); this.receivedMessages.offer(message);
} }
/**
* Returns the current size of the queue (Used for Command GET_MESSAGE_COUNT)
*/
public synchronized int getMessageCount() { public synchronized int getMessageCount() {
return this.receivedMessages.size(); return this.receivedMessages.size();
} }
/**
* Takes the next message out of the queue, to be sent to the Client
*/
public synchronized Message getNextMessage() { public synchronized Message getNextMessage() {
return this.receivedMessages.poll(); return this.receivedMessages.poll();
} }
/**
* Message is converted into a String and sent to the Client
*/
public void sendMessage(String messageString) { public void sendMessage(String messageString) {
Message msg = Message.toMessage(messageString); Message msg = Message.toMessage(messageString);
this.eventBus.publish(new ChatMessageEvent(msg)); this.eventBus.publish(new ChatMessageEvent(msg));
@@ -0,0 +1,57 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.handlers;
import jdk.jshell.spi.ExecutionControl;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class UserHandler {
private final List<String> usernames;
public UserHandler() {
this.usernames = new ArrayList<String>();
}
public Boolean checkUsername(String username) {
for (int i = 0; i < this.usernames.size(); i++) {
if (this.usernames.get(i).equals(username)) {
return false;
}
}
return true;
}
public Pattern usrRex = Pattern.compile("USERNAME=(?<user>\\w+)");
public String addUsername(String username) {
Matcher matcher = usrRex.matcher(username);
if (! matcher.find()) {
throw new RuntimeException("Cannot parse " + username);
}
String usr = matcher.group("user");
if (!this.checkUsername(usr)) {
this.usernames.add(usr);
} else {
this.usernames.add(getAltUsername(usr));
}
return String.format("USERNAME=%s", usr);
}
public void removeUsername(String username) {
this.usernames.remove(username);
}
public String getAltUsername(String username) {
int count = 0;
while (true) {
username = String.format("%s%d", username, ++count);
if (this.checkUsername(username)) {
return username;
}
}
}
}
@@ -5,6 +5,7 @@ import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import ch.unibas.dmi.dbis.cs108.casono.server.network.handlers.ChatHandler; import ch.unibas.dmi.dbis.cs108.casono.server.network.handlers.ChatHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.handlers.UserHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket; import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@@ -18,6 +19,7 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TransportLayer;
*/ */
public class Session implements Runnable { public class Session implements Runnable {
private final ChatHandler chatHandler; private final ChatHandler chatHandler;
private final UserHandler userHandler;
private SessionId id; private SessionId id;
private Thread thread; private Thread thread;
private TransportLayer transport; private TransportLayer transport;
@@ -44,6 +46,7 @@ public class Session implements Runnable {
this.logger.info("Created new session"); this.logger.info("Created new session");
this.idGenerator = new AtomicInteger(); this.idGenerator = new AtomicInteger();
this.chatHandler = new ChatHandler(eventBus); this.chatHandler = new ChatHandler(eventBus);
this.userHandler = new UserHandler();
} }
/** /**
@@ -84,11 +87,11 @@ public class Session implements Runnable {
String commandWord = commandAndArgs[0]; String commandWord = commandAndArgs[0];
System.out.println("Session "+id.value()+" Received: " + commandAndArgs[0]); System.out.println("Session "+id.value()+" Received: " + commandAndArgs[0]);
switch (commandWord) { switch (commandWord) {
case "SEND_MESSAGE": case "SEND_MESSAGE":
this.chatHandler.sendMessage(commandAndArgs[1]); this.chatHandler.sendMessage(commandAndArgs[1]);
writeToTransport("+OK"); writeToTransport("+OK");
writeToTransport("+OK"); writeToTransport("+OK");
break; break;
case "GET_MESSAGE_COUNT": case "GET_MESSAGE_COUNT":
int count = chatHandler.getMessageCount(); int count = chatHandler.getMessageCount();
writeToTransport(""+count); writeToTransport(""+count);
@@ -97,7 +100,7 @@ public class Session implements Runnable {
case "GET_NEXT_MESSAGE": case "GET_NEXT_MESSAGE":
if (chatHandler.getMessageCount() == 0) { if (chatHandler.getMessageCount() == 0) {
logger.warn("FAIL No more messages!"); logger.warn("FAIL No more messages!");
writeToTransport("-FAIL"); writeToTransport("-ERROR");
} else { } else {
String msgString = chatHandler.getNextMessage().toArgsString(); String msgString = chatHandler.getNextMessage().toArgsString();
logger.debug("Session "+id.value()+" Write next message "+msgString); logger.debug("Session "+id.value()+" Write next message "+msgString);
@@ -105,9 +108,19 @@ public class Session implements Runnable {
writeToTransport("+OK"); writeToTransport("+OK");
} }
break; break;
case "LOGIN":
String usr = userHandler.addUsername(commandAndArgs[1]);
writeToTransport(usr);
writeToTransport("+OK");
case "LOGOUT":
userHandler.removeUsername(commandAndArgs[1]);
writeToTransport("+OK");
case "PING":
default: default:
this.logger.warn("Unknown command: " + commandWord); this.logger.warn("Unknown command: " + commandWord);
writeToTransport("-FAIL"); writeToTransport("-ERROR");
} }
} catch (EOFException e) { } catch (EOFException e) {
logger.info("Client disconnected"); logger.info("Client disconnected");
@@ -33,15 +33,20 @@ public class ClientServiceTest {
@Test @Test
public void testClientService() throws ExecutionException, InterruptedException { public void testClientService() throws ExecutionException, InterruptedException {
ClientService client1 = new ClientService("localhost", 5000); ClientService client1 = new ClientService("localhost", 5000);
/*
ClientService client2 = new ClientService("localhost", 5000); ClientService client2 = new ClientService("localhost", 5000);
Thread.sleep(2000); Thread.sleep(2000);
String response = client1.sendMessage(new Message(Message.MessageType.GLOBAL, 0, "Mathis", null, "blahh")); client1.sendMessage(new Message(Message.MessageType.GLOBAL, 0, "Mathis", null, "blahh"));
Thread.sleep(5000); Thread.sleep(5000);
List<Message> received = client2.getMessages(); List<Message> received = client2.getMessages();
assertEquals(1, received.size()); assertEquals(1, received.size());
Message s = received.get(0); Message s = received.get(0);
assertEquals(Message.MessageType.GLOBAL, s.getMessageType()); assertEquals(Message.MessageType.GLOBAL, s.getMessageType());
*/
String username = client1.login("Mathis");
assertEquals("USERNAME=Mathis", username);
} }
@Test @Test