add: first version of client to client chat

This commit is contained in:
Mathis Ginkel
2026-03-15 15:12:25 +01:00
parent 3809b7e0e2
commit d6468c9595
8 changed files with 214 additions and 79 deletions
@@ -1,6 +1,7 @@
package ch.unibas.dmi.dbis.cs108.casono.client.chat; package ch.unibas.dmi.dbis.cs108.casono.client.chat;
import java.time.LocalTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -11,7 +12,7 @@ import java.util.regex.Pattern;
public class Message { public class Message {
private final MessageType type; private final MessageType type;
private String message; private final String message;
public String user; public String user;
public String timestamp; public String timestamp;
public int game_id = 0; public int game_id = 0;
@@ -20,8 +21,6 @@ public class Message {
GLOBAL, LOBBY, WHISPER GLOBAL, LOBBY, WHISPER
}; };
public static final String SEPERATOR = " ";
/** /**
* Constructor for creating Messages with all information given * Constructor for creating Messages with all information given
* @param type - Either global, local or whisper * @param type - Either global, local or whisper
@@ -55,8 +54,9 @@ public class Message {
this.user = user; this.user = user;
this.target = target; this.target = target;
this.message = message; this.message = message;
LocalTime now = LocalTime.now(); LocalDateTime now = LocalDateTime.now();
this.timestamp = now.toString(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm");
this.timestamp = now.format(formatter);
} }
@@ -64,33 +64,33 @@ public class Message {
return message; return message;
} }
public String getMessageType() { public MessageType getMessageType() {
return type.toString(); return type;
} }
/** /*
* Method to test the system * Method to test the system
* @return - String representation of the Message instance, as for example "player1: Hello World" * @return - String representation of the Message instance, as for example "player1: Hello World"
*/
public String toString() { public String toString() {
return String.format("%s: %s", this.user, this.message); return String.format("%s: %s", this.user, this.message);
} }
*/
/** /**
* Method to create the request representation of the message object, to be sent to the server * Method to create the request representation of the message object, to be sent to the server
* @return - request as specified in the network protocol, as String * @return - request as specified in the network protocol, as String
*/ */
public String toRequest() { public String toArgsString() {
String request = String.format("SEND_MESSAGE TYPE=%s GAME=%d USER=%s TARGET=%s TIME=%s TEXT=%s", return String.format("TYPE=%s GAME=%d USER=%s TARGET=%s TIME=%s TEXT=%s",
this.type.toString(), this.game_id, this.user, this.target, this.timestamp, this.message); this.type.toString(), this.game_id, this.user, this.target, this.timestamp, this.message);
return request;
} }
/** /**
* Pattern, to analyze the response String with the given parameters * Pattern, to analyze the response String with the given parameters
*/ */
public static Pattern msgRex = Pattern.compile("TYPE=(?<type>\\w+) GAME=(?<game>\\w+) USER=(?<user>\\w+) TARGET=(?<target>\\w+) TIME=(?<time>[0-9:]+) TEXT=(?<text>.*)$"); public static Pattern msgRex = Pattern.compile(
"TYPE=(?<type>\\w+) GAME=(?<game>\\w+) USER=(?<user>\\w+) TARGET=(?<target>\\w+) TIME=(?<time>[0-9:.]+) TEXT=(?<text>.*)$");
/** /**
* Method to create a Message Object, from the information given by the String * Method to create a Message Object, from the information given by the String
@@ -98,10 +98,9 @@ public class Message {
* @return - New Message Object * @return - New Message Object
*/ */
public static Message toMessage(String response) { public static Message toMessage(String response) {
var parts = response.split(SEPERATOR);
Matcher m = msgRex.matcher(response); Matcher m = msgRex.matcher(response);
if (! m.matches()) { if (! m.matches()) {
throw new RuntimeException("Can not parse message: " + response); throw new RuntimeException("Can not parse message: '" + response+"'");
} }
String typeString=m.group("type"); String typeString=m.group("type");
@@ -1,9 +1,5 @@
package ch.unibas.dmi.dbis.cs108.casono.client.network; package ch.unibas.dmi.dbis.cs108.casono.client.network;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket; import java.net.Socket;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -13,9 +9,13 @@ import java.util.concurrent.Executors;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.io.IOException; import java.io.IOException;
import java.net.UnknownHostException; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message; import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TcpTransport;
/** /**
* Responsible for the transferring of the data from the Client to the Server and the other way around * Responsible for the transferring of the data from the Client to the Server and the other way around
@@ -23,16 +23,16 @@ import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
public class ClientService { public class ClientService {
private Socket socket; private final TcpTransport clienttcptransport;
private BufferedReader input; private final Socket socket;
private BufferedWriter output;
private String ip; private final String ip;
private int port; private final int port;
private ExecutorService executor; private final ExecutorService executor;
public static ArrayList<String> response; public static ArrayList<String> response;
private final AtomicInteger idGenerator;
/** /**
* Creates a new ClientSession with a Socket, a Reader and Writer of the Input- and the Outputstream and a pool of threads to send requests and receive responses. * Creates a new ClientSession with a Socket, a Reader and Writer of the Input- and the Outputstream and a pool of threads to send requests and receive responses.
@@ -44,20 +44,15 @@ public class ClientService {
this.ip = ip; this.ip = ip;
this.port = port; this.port = port;
this.idGenerator = new AtomicInteger(0);
try { try {
socket = new Socket(this.ip, this.port); socket = new Socket(this.ip, this.port);
System.out.println("Connected"); System.out.println("Connected");
input = new BufferedReader(new InputStreamReader(socket.getInputStream())); clienttcptransport = new TcpTransport(socket);
output = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
}
catch (UnknownHostException u) {
System.out.println(u);
return;
} }
catch (IOException i) { catch (IOException i) {
System.out.println(i); throw new RuntimeException(i);
return;
} }
executor = Executors.newSingleThreadExecutor(); executor = Executors.newSingleThreadExecutor();
@@ -67,30 +62,47 @@ 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 Request to get all the messages, the other clients sent, and that the client is not currently aware of.
*/ */
public List<String> getMessage() { public List<Message> getMessages() {
return processMessage("GET_MESSAGE"); String countStr = processMessage("GET_MESSAGE_COUNT");
int count = Integer.parseInt(countStr);
System.out.println("Got " + count + " messages");
List<Message> messages = new ArrayList<>();
for (int i = 0; i < count; i++) {
String message = processMessage("GET_NEXT_MESSAGE");
if (message != null) {
Message message1 = Message.toMessage(message);
messages.add(message1);
}
}
return messages;
} }
public List<String> sendMessage(Message message) { return processMessage(message.toRequest()); } public String sendMessage(Message message) {
String request = "SEND_MESSAGE " + message.toArgsString();
return processMessage(request);
}
private List<String> processMessage(String message) { private String processMessage(String message) {
List<String> response = new ArrayList<>(); AtomicReference<String> response = new AtomicReference<>();
sendRequest(() -> { sendRequest(() -> {
try { try {
output.write(message+"\n"); writeToTransport(message);
output.flush(); String responseLine=null;
String line; do {
while ((line = input.readLine()) != null) { responseLine = clienttcptransport.read().payload();
if (line.toLowerCase().startsWith(".")) { System.out.println("Raw message '" + responseLine + "'");
break; if ("+OK".equals(responseLine)) {
} return;
response.add(line); } else if ("-FAIL".equals(responseLine)) {
throw new RuntimeException(responseLine);
} }
response.set(responseLine);
} while(true);
} catch (Exception e) { } catch (Exception e) {
throw getRuntimeException(e); throw getRuntimeException(e);
} }
}); });
return response; return response.get();
} }
private void sendRequest(Runnable request) { private void sendRequest(Runnable request) {
@@ -122,12 +134,17 @@ public class ClientService {
public void closeSocket() { public void closeSocket() {
try { try {
executor.shutdown(); executor.shutdown();
input.close(); clienttcptransport.close();
output.close();
socket.close(); socket.close();
} catch (IOException j) { } catch (IOException j) {
System.out.println(j); System.out.println(j);
} }
} }
private void writeToTransport(String s) throws IOException {
int id = this.idGenerator.incrementAndGet();
this.clienttcptransport.write(new RawPacket(id, s));
}
} }
@@ -25,6 +25,10 @@ public class ServerApp {
Logger logger = LogManager.getLogger(ServerApp.class); Logger logger = LogManager.getLogger(ServerApp.class);
logger.info("Starting server at port {}", port); logger.info("Starting server at port {}", port);
startServerCore(port);
}
public static void startServerCore(int port) {
EventBus eventBus = new EventBus(); EventBus eventBus = new EventBus();
SessionManager sessionManager = new SessionManager(); SessionManager sessionManager = new SessionManager();
eventBus.subscribe( eventBus.subscribe(
@@ -0,0 +1,15 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.events;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
public class ChatMessageEvent implements Event {
private final Message message;
public ChatMessageEvent(Message message) {
this.message = message;
}
public Message getMessage() {
return message;
}
}
@@ -0,0 +1,35 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.handlers;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.ChatMessageEvent;
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
import java.util.ArrayDeque;
public class ChatHandler {
private final EventBus eventBus;
private final ArrayDeque<Message> receivedMessages = new ArrayDeque<>();
public ChatHandler(EventBus eventBus) {
eventBus.subscribe(ChatMessageEvent.class, (event) -> {
processMessageEvent(event.getMessage());
});
this.eventBus = eventBus;
}
private synchronized void processMessageEvent(Message message) {
// TODO: filter
this.receivedMessages.offer(message);
}
public synchronized int getMessageCount() {
return this.receivedMessages.size();
}
public synchronized Message getNextMessage() {
return this.receivedMessages.poll();
}
public void sendMessage(String messageString) {
Message msg = Message.toMessage(messageString);
this.eventBus.publish(new ChatMessageEvent(msg));
}
}
@@ -1,24 +1,30 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions; package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions;
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent;
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.PrimitiveRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.ProtocolParser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TransportLayer;
import java.io.EOFException; import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
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.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;
/** Represents a client session in the network server. */ import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent;
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TransportLayer;
/**
* Represents a client session in the network server.
*/
public class Session implements Runnable { public class Session implements Runnable {
private final ChatHandler chatHandler;
private SessionId id; private SessionId id;
private Thread thread; private Thread thread;
private TransportLayer transport; private TransportLayer transport;
private Logger logger; private Logger logger;
private Boolean running; private Boolean running;
private EventBus eventBus; private EventBus eventBus;
private AtomicInteger idGenerator;
/** /**
* Creates a new Session with the given transport and event bus. * Creates a new Session with the given transport and event bus.
@@ -36,6 +42,8 @@ public class Session implements Runnable {
this.logger = LogManager.getLogger(Session.class.toString() + id.value()); this.logger = LogManager.getLogger(Session.class.toString() + id.value());
this.logger.info("Created new session"); this.logger.info("Created new session");
this.idGenerator = new AtomicInteger();
this.chatHandler = new ChatHandler(eventBus);
} }
/** /**
@@ -47,7 +55,9 @@ public class Session implements Runnable {
return this.id; return this.id;
} }
/** Starts the session thread. */ /**
* Starts the session thread.
*/
public void start() { public void start() {
thread.start(); thread.start();
} }
@@ -62,16 +72,43 @@ public class Session implements Runnable {
this.running = false; this.running = false;
} }
/** Runs the session loop, reading from the transport. */ /**
* Runs the session loop, reading from the transport.
*/
@Override @Override
public void run() { public void run() {
while (running) { while (running) {
try { try {
RawPacket rawPacket = transport.read(); String clientCommand = transport.read().payload();
logger.debug("Recieved: {}", rawPacket); String[] commandAndArgs = clientCommand.split("\\s+", 2);
String commandWord = commandAndArgs[0];
PrimitiveRequest primitiveRequest = ProtocolParser.parse(rawPacket); System.out.println("Session "+id.value()+" Received: " + commandAndArgs[0]);
logger.debug("Parsed request to {}", primitiveRequest); switch (commandWord) {
case "SEND_MESSAGE":
this.chatHandler.sendMessage(commandAndArgs[1]);
writeToTransport("+OK");
writeToTransport("+OK");
break;
case "GET_MESSAGE_COUNT":
int count = chatHandler.getMessageCount();
writeToTransport(""+count);
writeToTransport("+OK");
break;
case "GET_NEXT_MESSAGE":
if (chatHandler.getMessageCount() == 0) {
logger.warn("FAIL No more messages!");
writeToTransport("-FAIL");
} else {
String msgString = chatHandler.getNextMessage().toArgsString();
logger.debug("Session "+id.value()+" Write next message "+msgString);
writeToTransport(msgString);
writeToTransport("+OK");
}
break;
default:
this.logger.warn("Unknown command: " + commandWord);
writeToTransport("-FAIL");
}
} catch (EOFException e) { } catch (EOFException e) {
logger.info("Client disconnected"); logger.info("Client disconnected");
eventBus.publish(new DisconnectEvent(id)); eventBus.publish(new DisconnectEvent(id));
@@ -82,4 +119,9 @@ public class Session implements Runnable {
} }
} }
} }
private void writeToTransport(String s) throws IOException {
int id = this.idGenerator.incrementAndGet();
this.transport.write(new RawPacket(id, s));
}
} }
@@ -8,8 +8,8 @@ import java.nio.charset.StandardCharsets;
/** Implements TCP-based transport layer for network communication. */ /** Implements TCP-based transport layer for network communication. */
public class TcpTransport implements TransportLayer { public class TcpTransport implements TransportLayer {
private Socket socket; private final Socket socket;
private DataInputStream in; private final DataInputStream in;
private DataOutputStream out; private DataOutputStream out;
/** /**
@@ -49,9 +49,8 @@ public class TcpTransport implements TransportLayer {
public void write(RawPacket data) throws IOException { public void write(RawPacket data) throws IOException {
int requestId = data.requestId(); int requestId = data.requestId();
byte[] rawPayload = data.payload().getBytes(StandardCharsets.UTF_8); byte[] rawPayload = data.payload().getBytes(StandardCharsets.UTF_8);
out.writeInt(requestId);
out.writeInt(rawPayload.length); out.writeInt(rawPayload.length);
out.writeInt(requestId);
out.write(rawPayload); out.write(rawPayload);
out.flush(); out.flush();
} }
@@ -2,14 +2,15 @@ package ch.unibas.dmi.dbis.cs108.casono.client.network;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message; import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
import ch.unibas.dmi.dbis.cs108.casono.server.Server; import ch.unibas.dmi.dbis.cs108.casono.server.Server;
import ch.unibas.dmi.dbis.cs108.casono.server.ServerApp;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class ClientServiceTest { public class ClientServiceTest {
@@ -18,19 +19,42 @@ public class ClientServiceTest {
@BeforeEach @BeforeEach
public void setUp() { public void setUp() {
server = new Server(5000); //server = new Server(5000);
serverThread = new Thread(()-> server.simpleListenLoop()); //serverThread = new Thread(()-> server.simpleListenLoop());
serverThread.start(); //serverThread.start();
ServerApp.startServerCore(5000);
} }
@AfterEach @AfterEach
public void tearDown() { public void tearDown() {
serverThread.interrupt();
//serverThread.interrupt();
} }
@Test @Test
public void testClientService() throws ExecutionException, InterruptedException { public void testClientService() throws ExecutionException, InterruptedException {
ClientService client = new ClientService("localhost", 5000); ClientService client1 = new ClientService("localhost", 5000);
List<String> response = client.sendMessage(new Message(Message.MessageType.GLOBAL, 0, "Mathis", null, "blahh")); ClientService client2 = new ClientService("localhost", 5000);
client.closeSocket(); Thread.sleep(2000);
String response = client1.sendMessage(new Message(Message.MessageType.GLOBAL, 0, "Mathis", null, "blahh"));
Thread.sleep(5000);
List<Message> received = client2.getMessages();
assertEquals(1, received.size());
Message s = received.get(0);
assertEquals(Message.MessageType.GLOBAL, s.getMessageType());
}
@Test
public void testMessage() {
Message msg = new Message(Message.MessageType.GLOBAL, 0, "Mathis", null, "blah");
Message msg2 = new Message(Message.MessageType.WHISPER, 1, "Mathis", "Julian", "blah");
String serial = msg.toArgsString();
Message roundTripped = Message.toMessage(serial);
String serial2 = msg2.toArgsString();
Message roundTripped2 = Message.toMessage(serial2);
assert(roundTripped.game_id == msg.game_id);
assertEquals(0, roundTripped.game_id);
assert(roundTripped2.game_id == msg2.game_id);
} }
} }