Compare commits

..

15 Commits

16 changed files with 1012 additions and 201 deletions

28
.project Normal file
View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>ClientServer</name>
<comment>Project ClientServer created by Buildship.</comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
<filteredResources>
<filter>
<id>1743445961452</id>
<name></name>
<type>30</type>
<matcher>
<id>org.eclipse.core.resources.regexFilterMatcher</id>
<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
</matcher>
</filter>
</filteredResources>
</projectDescription>

View File

@@ -0,0 +1,13 @@
arguments=--init-script /home/xeon0x/.var/app/dev.zed.Zed/data/zed/extensions/work/java/jdtls/jdt-language-server-1.46.1-202504011455/configuration/org.eclipse.osgi/58/0/.cp/gradle/init/init.gradle
auto.sync=false
build.scans.enabled=false
connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
connection.project.dir=
eclipse.preferences.version=1
gradle.user.home=
java.home=/usr/lib64/jvm/java-21-openjdk-21
jvm.arguments=
offline.mode=false
override.workspace.settings=true
show.console.view=true
show.executions.view=true

106
README.md
View File

@@ -1,7 +1,105 @@
# ClientServer
A Java-based ☕️↔️ UDP client-server chat 💬 system with automatic 🔄 text-to-emoji 😊 conversion using Mistral API.
## Features
### Available Commands
- `/list` : Display connected clients list
- `/msg <nickname> <message>` : Send private message to specific client
- `/help` : Display help with all available commands
- `/disconnect` : Clean server disconnection
- `<message>` : Broadcast message translated to emojis to all connected clients
### Technical Features
- UDP Communication
- Multi-threading for client handling
- Mistral API for text-to-emoji conversion
- Disconnection handling
## Build
```bash
./gradlew build
```
## Run
### Client
```bash
./gradlew runClient
```
The client will ask for a pseudo, an IP (type `localhost` if server and clients are on the same machine), and a port (type `6666` by default).
### Server
```bash
./gradlew runServer
```
## Configuration
Create a `config.properties` file at project root (`ClientServer/app/config.properties`) with:
```properties
mistral.api.key=your_api_key
```
Replace `your_api_key` with your actual Mistral API key. Create one for free at https://console.mistral.ai/api-keys. If no API key is provided, the server will not be able to convert broadcasted messages to emojis and empty broadcasted messages will be sent.
## Documentation
### Sequence Diagram
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
Note over Client, Serveur: DatagramSocket("localhost", 66666) participant MainServer
Client->>+Serveur: DatagramPacket("Salve !", 7, "localhost", 6666)
Note over Serveur, Client: DatagramSocket("localhost", 66666) participant Alice
Serveur->>+Client: DatagramPacket("Accusé de réception", 19, "localhost", 6666) Note over Alice: Creates DatagramSocket
Alice->>+MainServer: Sends username "Alice"
Note over MainServer: Creates new socket<br/>for AliceHandler
create participant AliceHandler
MainServer->>AliceHandler: Create new handler thread
MainServer->>-Alice: PORT:{AlicePort}
participant Bob
Bob->>+MainServer: Sends username "Bob"
Note over MainServer: Creates new socket<br/>for BobHandler
create participant BobHandler
MainServer->>BobHandler: Create new handler thread
MainServer->>-Bob: PORT:{BobPort}
rect rgb(200, 200, 255)
Note over Alice,Bob: Chat loop
Alice->>+AliceHandler: "Hi everyone!"
Note over AliceHandler: Converts to emojis
AliceHandler->>+BobHandler: Broadcast message
BobHandler->>-Bob: "Alice: 👋👩‍👩🌍🎉"
AliceHandler->>-Alice: Message sent
Bob->>+BobHandler: "Hello Alice, how are you?"
Note over BobHandler: Converts to emojis
BobHandler->>+AliceHandler: Broadcast message
AliceHandler->>-Alice: "Bob: 👋🏻👩🏼‍🦰🐰🤔👩🏼‍🦰🐇💬👩🏼‍🦰🐰👍🏻😊👩🏼‍🦰🐇👋🏻"
BobHandler->>-Bob: Message sent
end
Alice->>+AliceHandler: /disconnect
Note over AliceHandler: Removes Alice<br/>from map
AliceHandler->>-Alice: Disconnection confirmation
AliceHandler->>MainServer: Thread terminated
MainServer-->>AliceHandler: destroy
Bob->>+BobHandler: /disconnect
Note over BobHandler: Removes Bob<br/>from map
BobHandler->>-Bob: Disconnection confirmation
BobHandler->>MainServer: Thread terminated
MainServer-->>BobHandler: destroy
Note over Alice,Bob: Close sockets
``` ```

19
app/.classpath Normal file
View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="bin/main" path="src/main/java">
<attributes>
<attribute name="gradle_scope" value="main"/>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="bin/test" path="src/test/java">
<attributes>
<attribute name="gradle_scope" value="test"/>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-21/"/>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
<classpathentry kind="output" path="bin/default"/>
</classpath>

34
app/.project Normal file
View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>app</name>
<comment>Project app created by Buildship.</comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
<filteredResources>
<filter>
<id>1743445961456</id>
<name></name>
<type>30</type>
<matcher>
<id>org.eclipse.core.resources.regexFilterMatcher</id>
<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
</matcher>
</filter>
</filteredResources>
</projectDescription>

View File

@@ -0,0 +1,2 @@
connection.project.dir=..
eclipse.preferences.version=1

View File

@@ -40,3 +40,20 @@ tasks.named('test') {
// Use JUnit Platform for unit tests. // Use JUnit Platform for unit tests.
useJUnitPlatform() useJUnitPlatform()
} }
task runServer(type: JavaExec) {
description = 'Runs the server'
group = 'application'
mainClass = application.mainClass
classpath = sourceSets.main.runtimeClasspath
args = ['--server']
}
task runClient(type: JavaExec) {
description = 'Runs the client'
group = 'application'
mainClass = application.mainClass
classpath = sourceSets.main.runtimeClasspath
args = ['--client']
standardInput = System.in
}

View File

@@ -1,5 +1,8 @@
package clientserver; package clientserver;
import clientserver.client.Client;
import clientserver.server.Server;
public class App { public class App {
public String getGreeting() { public String getGreeting() {
@@ -7,8 +10,49 @@ public class App {
} }
public static void main(String[] args) { public static void main(String[] args) {
System.out.println(new App().getGreeting()); String mode = null;
ClientHandler.main(new String[] {});
// MistralDirectAPI.main(new String[] {}); // Parse arguments
for (String arg : args) {
switch (arg.toLowerCase()) {
case "--client":
mode = "client";
break;
case "--server":
mode = "server";
break;
default:
System.out.println("Unknown argument: " + arg);
printUsage();
return;
}
}
// Check if mode is specified
if (mode == null) {
System.out.println("No mode specified.");
printUsage();
return;
}
// Execute based on mode
switch (mode) {
case "client":
System.out.println("Starting client...");
Client.main(new String[] {});
break;
case "server":
System.out.println("Starting server...");
Server server = new Server(6666);
server.run();
break;
}
}
private static void printUsage() {
System.out.println("Usage: ./gradlew run --args='option'");
System.out.println("Options:");
System.out.println(" --client Run in client mode");
System.out.println(" --server Run in server mode");
} }
} }

View File

@@ -1,48 +0,0 @@
package clientserver;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class Client {
public static void main(String[] args) {
try {
// 1 - Création du canal avec un port libre
DatagramSocket socketClient = new DatagramSocket();
InetAddress adresseServeur = InetAddress.getByName("localhost");
int portServeur = 6666;
// 2 - Envoyer un message au serveur
String message = "Hello Server";
byte[] envoyees = message.getBytes();
DatagramPacket paquetEnvoye = new DatagramPacket(
envoyees, envoyees.length, adresseServeur, portServeur);
socketClient.send(paquetEnvoye);
System.out.println("Message envoyé au serveur");
// 3 - Recevoir
byte[] recues = new byte[1024]; // tampon de réception
DatagramPacket paquetRecu = new DatagramPacket(recues, recues.length);
socketClient.receive(paquetRecu);
String reponse = new String(paquetRecu.getData(), 0, paquetRecu.getLength());
if (reponse.startsWith("PORT:")) {
int newPort = Integer.parseInt(reponse.substring(5));
System.out.println("Connexion au nouveau port: " + newPort);
// 4 - Communiquer sur le nouveau port
String messagePort = "Message au nouveau port";
byte[] envoyeesPort = messagePort.getBytes();
DatagramPacket paquetPort = new DatagramPacket(
envoyeesPort, envoyeesPort.length, adresseServeur, newPort);
socketClient.send(paquetPort);
}
// 5 - Libérer le canal
socketClient.close();
} catch (Exception e) {
System.err.println(e);
}
}
}

View File

@@ -1,136 +0,0 @@
package clientserver;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
public class ClientHandler {
static void main(String[] args) {
int port = 6666;
boolean running = true;
DatagramSocket socketServer = null;
try {
socketServer = new DatagramSocket(null);
// 2 - Réservation du port
InetSocketAddress address = new InetSocketAddress(
"localhost",
port
);
socketServer.bind(address);
} catch (Exception e) {
e.printStackTrace();
}
while (running) {
try {
// 1 - Création du canal
// 3 - Recevoir
byte[] recues = new byte[1024]; // tampon de réception
DatagramPacket receivedPacket = new DatagramPacket(
recues,
recues.length
);
socketServer.receive(receivedPacket);
InetAddress clientAddress = receivedPacket.getAddress();
int clientPort = receivedPacket.getPort();
System.out.println(
"Nouveau client : @" + clientAddress + ":" + clientPort
);
DatagramSocket socket = new DatagramSocket(); // Reserve new port
String response = "PORT:" + socket.getLocalPort();
System.out.println(response);
byte[] envoyees; // tampon d'émission
envoyees = response.getBytes();
DatagramPacket packetToSend = new DatagramPacket(
envoyees,
envoyees.length,
clientAddress,
clientPort
);
socketServer.send(packetToSend);
Thread thread = new Thread(() -> createThread(socket));
thread.start();
} catch (Exception e) {
System.err.println(e);
}
}
socketServer.close();
}
public static void createThread(DatagramSocket socketClientThread) {
try {
byte[] recues = new byte[1024]; // tampon de réception
DatagramPacket receivedPacket = new DatagramPacket(
recues,
recues.length
);
socketClientThread.receive(receivedPacket);
String message = new String(
receivedPacket.getData(),
0,
receivedPacket.getLength()
);
System.out.println("Received message: " + message);
} catch (Exception e) {
System.err.println(e);
}
}
static void scannerUDP(int startPort, int endPort) {
try {
for (int i = startPort; i < endPort; i++) {
try (DatagramSocket socket = new DatagramSocket(i)) {} catch (
Exception e
) {
System.out.println("Port n°" + i + " déjà occupé");
}
}
} catch (Exception e) {
System.out.println(e);
}
}
// public static void main(String[] args) {
// try {
// // 1 - Création du canal
// DatagramSocket socketServeur = new DatagramSocket(null);
// // 2 - Réservation du port
// InetSocketAddress adresse = new InetSocketAddress(
// "localhost",
// 6666
// );
// socketServeur.bind(adresse);
// byte[] recues = new byte[1024]; // tampon d'émission
// byte[] envoyees; // tampon de réception
// // 3 - Recevoir
// DatagramPacket paquetRecu = new DatagramPacket(
// recues,
// recues.length
// );
// socketServeur.receive(paquetRecu);
// InetAddress adrClient = paquetRecu.getAddress();
// int prtClient = paquetRecu.getPort();
// System.out.println(
// "Nouveau client : @" + adrClient + ":" + prtClient
// );
// // 4 - Émettre
// String reponse = "Serveur RX302 ready";
// envoyees = reponse.getBytes();
// DatagramPacket paquetEnvoye = new DatagramPacket(
// envoyees,
// envoyees.length,
// adrClient,
// prtClient
// );
// socketServeur.send(paquetEnvoye);
// // 5 - Libérer le canal
// socketServeur.close();
// } catch (Exception e) {
// System.err.println(e);
// }
// }
}

View File

@@ -0,0 +1,283 @@
package clientserver.client;
import clientserver.server.Server;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.Scanner;
public class Client {
InetAddress clientAddress;
int clientPort;
private InetAddress serverAddress;
private int serverPort;
private String pseudo;
private Scanner scan;
private DatagramSocket clientSocket;
private int handlerPort;
public Client(InetAddress address, int port) {
this.clientAddress = address;
this.clientPort = port;
}
private static class MessageReceiver implements Runnable {
private final DatagramSocket socket;
private volatile boolean running = true;
public MessageReceiver(DatagramSocket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
socket.setSoTimeout(500); // Petit timeout pour pouvoir vérifier régulièrement si on doit s'arrêter,
// evite thread zombie car receive() est bloquant
while (running && !socket.isClosed()) {
try {
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(
buffer,
buffer.length
);
socket.receive(packet);
String message = new String(
packet.getData(),
0,
packet.getLength()
);
// Afficher le message reçu avec une mise en forme claire
System.out.println(message);
} catch (SocketTimeoutException e) {}
}
} catch (IOException e) {
if (!socket.isClosed()) {
System.err.println(
"Error in the reception of messages: " + e.getMessage()
);
}
}
System.out.println("Message receiver stopped");
}
public void stop() {
running = false;
}
}
public void setPort(int port) {
this.clientPort = port;
}
public int getPort() {
return this.clientPort;
}
public InetAddress getAddress() {
return this.clientAddress;
}
public InetAddress getServerAddress() {
return this.serverAddress;
}
public int getServerPort() {
return this.serverPort;
}
public void setServerPort(int port) {
this.serverPort = port;
}
public String getPseudo() {
return this.pseudo;
}
public void setPseudo(String pseudo) {
this.pseudo = pseudo;
}
public Scanner getScanner() {
return this.scan;
}
public void setScanner(Scanner scan) {
this.scan = scan;
}
public DatagramSocket getClientSocket() {
return this.clientSocket;
}
public void setClientSocket(DatagramSocket clientSocket) {
this.clientSocket = clientSocket;
}
public int getHandlerPort() {
return handlerPort;
}
public void setHandlerPort(int handlerPort) {
this.handlerPort = handlerPort;
}
public static Client createAndConfigureClient(DatagramSocket socketClient) {
System.out.println("Enter your Pseudo :");
// Créer le client avec l'adresse locale et le port du socket
Client client = new Client(
socketClient.getLocalAddress(),
socketClient.getLocalPort()
);
client.setScanner(new Scanner(System.in));
String userPseudo = client.getScanner().next();
client.setPseudo(userPseudo);
System.out.println("Enter the IP address of the server :");
try {
client.serverAddress = InetAddress.getByName(
client.getScanner().next()
);
} catch (UnknownHostException e) {
System.err.println(
"The address you choose create an UnknownHostException: " + e
);
}
System.out.println("Enter the port of the server:");
client.setServerPort(client.getScanner().nextInt());
client.getScanner().nextLine(); // Consume the remaining newline to avoid sending a void line message like
// (morph (/127.0.0.1:39056) : ...)
if (socketClient != null && client.serverAddress != null) {
Server.sendMessage(
socketClient,
client.getPseudo(),
client.serverAddress,
client.getServerPort()
);
System.out.println("New connection request sent");
}
return client;
}
private static void deconnexionClient(
DatagramSocket socketClient,
Client client,
int handlerPort
) {
try {
String disconnectMsg = "/disconnect";
byte[] disconnectData = disconnectMsg.getBytes();
DatagramPacket disconnectPacket = new DatagramPacket(
disconnectData,
disconnectData.length,
client.getServerAddress(),
handlerPort
);
socketClient.send(disconnectPacket);
System.out.println(
"Deconnection sended on the server port: " + handlerPort
);
} catch (Exception e) {
System.err.println(
"Error during sending the deconnection message: " + e
);
}
}
public void run() {
// 1 - Boucle principale pour l'envoi des messages
boolean running = true;
System.out.println("\n=== Chat client ready ===");
// System.out.println(
// "Type your messages then press 'Enter' to send them to the server."
// );
System.out.println("Type /list to see all the clients connected.");
System.out.println(
"Type '/msg <target> <message>' to send a private message."
);
System.out.println("Type '/help' to see all the commands.");
System.out.println("Type '/disconnect' to quit.\n");
while (running) {
String message = this.getScanner().nextLine();
if ("/disconnect".equalsIgnoreCase(message)) {
running = false;
} else {
Server.sendMessage(
this.clientSocket,
message,
this.getServerAddress(),
this.getHandlerPort()
);
}
}
// 2 - Gérer la déconnexion proprement
System.out.println("Deconnection in progress...");
deconnexionClient(this.getClientSocket(), this, this.getHandlerPort());
}
public static void main(String[] args) {
try {
// 1 - Création du canal avec un port libre
DatagramSocket socketClient = new DatagramSocket();
// 2 - Création du client avec l'envoi du pseudo pour l'établissement de la
// connexion
Client client = createAndConfigureClient(socketClient);
// 3 - Recevoir la réponse initiale du serveur
DatagramPacket receivedPacket = Server.receivedPacket(socketClient);
String reponse = Server.packetToMessage(receivedPacket);
if (reponse.startsWith("PORT:")) {
int handlerPort = Integer.parseInt(reponse.substring(5));
System.out.println("Connected on handler port: " + handlerPort);
client.setClientSocket(socketClient);
client.setHandlerPort(handlerPort);
// 4 - Lancer uniquement le thread de réception des messages
MessageReceiver receiver = new MessageReceiver(socketClient);
Thread receiverThread = new Thread(receiver);
receiverThread.setDaemon(true);
receiverThread.start();
// 5 - Éxecution du client, bloquant tant que le client est connecté.
client.run();
// 6 - Arrêter le thread de réception
receiver.stop();
try {
receiverThread.join(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
} else {
System.err.println(
"Unexpected response from the server: " + reponse
);
}
// 8 - Libérer le canal
socketClient.close();
System.out.println("Client closed");
} catch (Exception e) {
System.err.println(e);
}
}
}

View File

@@ -1,4 +1,4 @@
package clientserver; package clientserver.common;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
@@ -10,10 +10,9 @@ import java.util.Properties;
public class MistralDirectAPI { public class MistralDirectAPI {
public static void main(String[] args) { private static String apiKey;
String apiKey;
// Load API key from properties file static {
try { try {
Properties props = new Properties(); Properties props = new Properties();
FileInputStream input = new FileInputStream("config.properties"); FileInputStream input = new FileInputStream("config.properties");
@@ -22,18 +21,22 @@ public class MistralDirectAPI {
input.close(); input.close();
} catch (IOException e) { } catch (IOException e) {
System.err.println("Could not load API key: " + e.getMessage()); System.err.println("Could not load API key: " + e.getMessage());
return;
} }
}
String payload = public static String translateToEmojis(String message) {
String payload = String.format(
""" """
{ {
"model": "mistral-medium", "model": "mistral-medium",
"messages": [ "messages": [
{ "role": "user", "content": "Reverse turing test." } { "role": "system", "content": "Please translate the following message to emojis only. Do not include any comments. End the emoji message with ;" },
{ "role": "user", "content": "%s" }
] ]
} }
"""; """,
message
);
HttpRequest request = HttpRequest.newBuilder() HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.mistral.ai/v1/chat/completions")) .uri(URI.create("https://api.mistral.ai/v1/chat/completions"))
@@ -49,10 +52,26 @@ public class MistralDirectAPI {
request, request,
HttpResponse.BodyHandlers.ofString() HttpResponse.BodyHandlers.ofString()
); );
System.out.println("Response Code: " + response.statusCode()); String responseBody = response.body();
System.out.println("Response Body: " + response.body()); return extractContent(responseBody);
} catch (IOException | InterruptedException e) { } catch (IOException | InterruptedException e) {
System.err.println("Error occurred: " + e.getMessage()); System.err.println("Error occurred: " + e.getMessage());
return message;
} }
} }
private static String extractContent(String jsonResponse) {
// Find the content field in the response
int contentIndex = jsonResponse.indexOf("\"content\":\"");
if (contentIndex != -1) {
int startIndex = contentIndex + "\"content\":\"".length();
int endIndex = jsonResponse.indexOf(";", startIndex);
if (endIndex != -1) {
return jsonResponse.substring(startIndex, endIndex);
}
}
return "";
}
} }

View File

@@ -0,0 +1,70 @@
package clientserver.server;
import clientserver.client.Client;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class ClientHandler implements Runnable {
private final DatagramSocket clientHandlerSocket;
private final Client client;
private boolean running = true;
private final MessageProcessor messageProcessor;
public ClientHandler(DatagramSocket socket, Client client) {
this.clientHandlerSocket = socket;
this.client = client;
this.messageProcessor = new MessageProcessor(
client,
clientHandlerSocket,
this
);
}
public Client getClient() {
return client;
}
public void stop() {
running = false;
if (clientHandlerSocket != null && !clientHandlerSocket.isClosed()) {
clientHandlerSocket.close();
}
}
public static String prettyPrint(Client client) {
StringBuilder str = new StringBuilder();
str.append(
client.getPseudo() +
" (" +
client.getAddress() +
":" +
client.getPort() +
")"
);
return str.toString();
}
@Override
public void run() {
System.out.println("Started handler for client " + prettyPrint(client));
while (running && !clientHandlerSocket.isClosed()) {
DatagramPacket packet = Server.receivedPacket(clientHandlerSocket);
if (packet == null) continue;
String message = new String(
packet.getData(),
0,
packet.getLength()
);
System.out.println(prettyPrint(client) + " : " + message);
messageProcessor.processMessage(message);
}
System.out.println(
"Client handler terminated for " + prettyPrint(client)
);
}
}

View File

@@ -0,0 +1,170 @@
package clientserver.server;
import clientserver.client.Client;
import clientserver.common.MistralDirectAPI;
import java.net.DatagramSocket;
public class MessageProcessor {
private final Client client;
private final DatagramSocket clientHandlerSocket;
private final ClientHandler clientHandler;
public MessageProcessor(
Client client,
DatagramSocket clientHandlerSocket,
ClientHandler clientHandler
) {
this.client = client;
this.clientHandlerSocket = clientHandlerSocket;
this.clientHandler = clientHandler;
}
public void processMessage(String message) {
if (message == null) {
return;
}
if (message.startsWith("/msg ")) {
handlePrivateMessage(message);
return;
}
switch (message) {
case "/list":
handleListCommand();
break;
case "/help":
handleHelpCommand();
break;
case "/msg":
handleMsgCommand();
break;
case "/disconnect":
handleDisconnect();
break;
default:
// Broadcast message to all clients
broadcastMessageEmoji(message);
break;
}
}
private void handlePrivateMessage(String message) {
String[] parts = message.split(" ", 3);
if (parts.length < 3) {
handleMsgCommand();
return;
}
String targetPseudo = parts[1];
String messageContent = parts[2];
ClientHandler targetHandler = Server.getClientHandler(targetPseudo);
if (targetHandler == null) {
Server.sendMessage(
clientHandlerSocket,
"> User " + targetPseudo + " not found",
client.getAddress(),
client.getPort()
);
return;
}
// Send private message to target
Client targetClient = targetHandler.getClient();
Server.sendMessage(
clientHandlerSocket,
"> " +
client.getPseudo() +
" -> " +
targetClient.getPseudo() +
": " +
messageContent,
targetClient.getAddress(),
targetClient.getPort()
);
// Send confirmation to sender
Server.sendMessage(
clientHandlerSocket,
"> " +
client.getPseudo() +
" -> " +
targetClient.getPseudo() +
": " +
messageContent,
client.getAddress(),
client.getPort()
);
}
private void broadcastMessage(String message) {
String formattedMessage = "> " + client.getPseudo() + ": " + message;
for (ClientHandler handler : Server.getAllClientHandlers()) {
Client targetClient = handler.getClient();
Server.sendMessage(
clientHandlerSocket,
formattedMessage,
targetClient.getAddress(),
targetClient.getPort()
);
}
}
private void broadcastMessageEmoji(String message) {
// Translate message to emojis
String emojiMessage = MistralDirectAPI.translateToEmojis(message);
String formattedMessage =
"> " + client.getPseudo() + ": " + emojiMessage;
for (ClientHandler handler : Server.getAllClientHandlers()) {
Client targetClient = handler.getClient();
Server.sendMessage(
clientHandlerSocket,
formattedMessage,
targetClient.getAddress(),
targetClient.getPort()
);
}
}
private void handleMsgCommand() {
Server.sendMessage(
clientHandlerSocket,
"> /msg <target> <message> : send a message to a client",
client.getAddress(),
client.getPort()
);
}
private void handleListCommand() {
Server.sendMessage(
clientHandlerSocket,
Server.getPseudos(),
client.getAddress(),
client.getPort()
);
}
private void handleHelpCommand() {
Server.sendMessage(
clientHandlerSocket,
"> /list : list all connected clients" +
"\n> /msg <target> <message> : send a message to a client" +
"\n> /disconnect : disconnect from the server",
client.getAddress(),
client.getPort()
);
}
private void handleDisconnect() {
System.out.println(
"Client deconnection : " + ClientHandler.prettyPrint(client)
);
Server.removeClientHandler(client.getPseudo());
clientHandler.stop();
}
}

View File

@@ -0,0 +1,197 @@
package clientserver.server;
import clientserver.client.Client;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class Server {
private int mainServerPort;
private DatagramSocket mainServerSocket;
private boolean isRunning;
private static Map<
String,
ClientHandler
> mapPseudosConnectedClientsHandlers = new ConcurrentHashMap<>();
public Server(int port) {
this.mainServerPort = port;
mainServerSocket = createSocket(this.mainServerPort);
isRunning = true;
}
private DatagramSocket createSocket(int port) {
try {
return new DatagramSocket(port);
} catch (Exception e) {
System.err.println("Failed to bind server socket on port " + port);
e.printStackTrace();
return null;
}
}
private DatagramSocket createNewSocket() {
try {
return new DatagramSocket(); // Reserve a random port
} catch (Exception e) {
System.err.println("Failed to create new server socket");
e.printStackTrace();
return null;
}
}
public static DatagramPacket receivedPacket(DatagramSocket socket) {
byte[] receivedData = new byte[1024];
DatagramPacket receivedPacket = new DatagramPacket(
receivedData,
receivedData.length
);
try {
socket.receive(receivedPacket); // Blocking call
return receivedPacket;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
public static String packetToMessage(DatagramPacket packet) {
return new String(packet.getData(), 0, packet.getLength());
}
public static void sendMessage(
DatagramSocket socket,
String message,
java.net.InetAddress address,
int port
) {
try {
byte[] sendData = message.getBytes();
DatagramPacket packetToSend = new DatagramPacket(
sendData,
sendData.length,
address,
port
);
socket.send(packetToSend);
} catch (IOException e) {
System.err.println(
"Failed to send message to " + address + ":" + port
);
e.printStackTrace();
}
}
private void handleNewConnection() {
DatagramPacket packet = receivedPacket(mainServerSocket);
if (packet == null) return;
int originalClientPort = packet.getPort();
InetAddress clientAddress = packet.getAddress();
// Log the initial connection request
System.out.println(
"New connection request from " +
clientAddress +
":" +
originalClientPort
);
// Process the received message
String pseudoMessage = new String(
packet.getData(),
0,
packet.getLength()
);
System.out.println(
"Received message from " +
clientAddress +
":" +
originalClientPort +
": " +
pseudoMessage
);
// Create a new socket for this client
DatagramSocket clientHandlerSocket = createNewSocket();
if (clientHandlerSocket == null) return;
int clientHandlerLocalPort = clientHandlerSocket.getLocalPort();
Client client = new Client(clientAddress, originalClientPort);
client.setPseudo(pseudoMessage);
// Send new port information to client
String response = "PORT:" + clientHandlerLocalPort;
sendMessage(
mainServerSocket,
response,
clientAddress,
packet.getPort()
);
// Create and start a ClientHandler for this connection
ClientHandler handler = new ClientHandler(clientHandlerSocket, client);
Thread thread = new Thread(handler);
thread.start();
// Add client handler to mapPseudosConnectedClients
addConnectedClientHandler(pseudoMessage, handler);
}
public void run() {
System.out.println("Server started on port " + mainServerPort);
while (isRunning) {
try {
handleNewConnection();
} catch (Exception e) {
System.err.println(
"Error handling connection: " + e.getMessage()
);
e.printStackTrace();
}
}
System.out.println("Server shutdown");
}
public static String getPseudos() {
StringBuilder pseudos = new StringBuilder();
int size = mapPseudosConnectedClientsHandlers.size();
int count = 0;
for (Map.Entry<
String,
ClientHandler
> entry : mapPseudosConnectedClientsHandlers.entrySet()) {
count++;
pseudos.append("> ").append(entry.getKey());
if (count < size) {
pseudos.append("\n");
}
}
return pseudos.toString();
}
public static ClientHandler getClientHandler(String pseudo) {
return mapPseudosConnectedClientsHandlers.get(pseudo);
}
public static Collection<ClientHandler> getAllClientHandlers() {
return mapPseudosConnectedClientsHandlers.values();
}
public void addConnectedClientHandler(
String pseudo,
ClientHandler clientHandler
) {
mapPseudosConnectedClientsHandlers.putIfAbsent(pseudo, clientHandler);
}
public static void removeClientHandler(String pseudo) {
mapPseudosConnectedClientsHandlers.remove(pseudo);
}
}

1
gradle.properties Normal file
View File

@@ -0,0 +1 @@
org.gradle.console=plain