Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d62e0b7188 | |||
| ef7bbc8492 | |||
|
|
eee63cc5c6 | ||
|
|
a4c5d3f67b | ||
|
|
709a92aa4c | ||
|
|
67124f4731 | ||
|
|
631cd25a9d | ||
|
|
dfdaae163b | ||
|
|
0b6f5193a0 | ||
|
|
4bb45cc3db | ||
|
|
39a65e1ca6 | ||
|
|
2b17891379 | ||
|
|
bfd3c10e9e | ||
|
|
11fc2e2ac8 | ||
|
|
2be11ec4a8 | ||
| 3eec95e420 | |||
| 0900113bb8 | |||
|
|
07272732d4 | ||
| 3abdc09819 | |||
| 980527f45f | |||
|
|
a2a5e96dc5 | ||
|
|
3ad5bcf819 | ||
| 462307dabc | |||
| 72c62bb1b4 | |||
| c8a748fe71 | |||
|
|
6950810b95 | ||
|
|
f87145ed69 | ||
|
|
5cefe42a99 | ||
|
|
6d228aee55 | ||
|
|
a83104d322 | ||
| 583505d93a | |||
| 0a8006fd56 | |||
|
|
0560d23cd3 | ||
|
|
7866984e19 | ||
|
|
5669859ac1 | ||
| 2bb3e64f2b | |||
| 76da347fb9 | |||
|
|
e536a45266 | ||
| 3115d397a4 | |||
| a5a41f573b | |||
| 8f30f139cd | |||
|
|
c83e39ea4b | ||
|
|
5befdd3080 | ||
|
|
4b8adef72f | ||
|
|
39a2afcd6e | ||
|
|
c9e564370e | ||
|
|
e52066ce17 | ||
|
|
e9f1feaaad | ||
|
|
7adb581e33 | ||
|
|
10f6b059b1 | ||
|
|
f8f740f799 | ||
|
|
a2c4319182 | ||
| 90f92281ef | |||
| 0533c16cf2 | |||
| 63ec7b3aaa | |||
| 07ad2ba05e |
5
ChatApp/.gitignore
vendored
5
ChatApp/.gitignore
vendored
@@ -1,3 +1,8 @@
|
||||
.vscode
|
||||
bin
|
||||
lib
|
||||
# Ignore Gradle project-specific cache directory
|
||||
.gradle
|
||||
|
||||
# Ignore Gradle build output directory
|
||||
build
|
||||
|
||||
@@ -3,15 +3,56 @@ An instant messaging app using Java and the UDP protocol.
|
||||
|
||||
## How to run
|
||||
|
||||
> [!WARNING]
|
||||
> As of now, the app rely on ANSI escape character, thus not available on Windows.
|
||||
> However, this feature is under development.
|
||||
### Console
|
||||
|
||||
You can run the app by launching the `ChatApp` class. This will create the server (port `6665`) and a first client,
|
||||
(creating by the way a room (`101`)).
|
||||
#### Server + Client
|
||||
|
||||
You can then create more clients by launching the `Client` class. It will connect to the server
|
||||
(be sure you launched it first) on the default port `6665`.
|
||||
You can create a server and an "admin" client by using:
|
||||
|
||||
```shell
|
||||
./gradlew admin # Linux
|
||||
.\gradlew.bat admin # Windows
|
||||
```
|
||||
|
||||
You will receive the notifications of the server (handshakes, ...).
|
||||
|
||||
#### Server
|
||||
|
||||
You can create a server by launching:
|
||||
|
||||
```shell
|
||||
./gradlew server # Linux
|
||||
.\gradlew server # Windows
|
||||
```
|
||||
|
||||
If you want to create a server on a specific port, you can use:
|
||||
|
||||
```shell
|
||||
./gradlew server --args="port" # Linux
|
||||
.\gradlew server --args="port" # Windows
|
||||
```
|
||||
|
||||
where `port` is the port you want to use.
|
||||
|
||||
This will create the server on a random port available and indicate it through the terminal.
|
||||
|
||||
To launch the server on a particular port, simply use:
|
||||
|
||||
```shell
|
||||
./gradlew server --args="port" # Linux
|
||||
.\gradlew server --args="port" # Windows
|
||||
```
|
||||
|
||||
where `port` is the port you want to use.
|
||||
|
||||
#### Client
|
||||
|
||||
To create a client, you may use:
|
||||
|
||||
```shell
|
||||
./gradlew client # Linux
|
||||
.\gradlew client # Windows
|
||||
```
|
||||
|
||||
## How to use
|
||||
As soon as you launch a client, you will be prompted to enter your name. You will then be in the lobby. From here you
|
||||
@@ -29,4 +70,13 @@ You will also be able to create a new room.
|
||||
- /listRooms
|
||||
- /joinRoom *roomName*
|
||||
- /leaveRoom
|
||||
- /room
|
||||
- /bye
|
||||
- /help
|
||||
|
||||
> [!NOTE]
|
||||
> There are some aliases for the commands:
|
||||
> - /create
|
||||
> - /list
|
||||
> - /join
|
||||
> - /leave
|
||||
62
ChatApp/app/build.gradle
Normal file
62
ChatApp/app/build.gradle
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* This file was generated by the Gradle 'init' task.
|
||||
*
|
||||
* This generated file contains a sample Java application project to get you started.
|
||||
* For more details on building Java & JVM projects, please refer to https://docs.gradle.org/8.13/userguide/building_java_projects.html in the Gradle documentation.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
// Apply the application plugin to add support for building a CLI application in Java.
|
||||
id 'application'
|
||||
id 'org.openjfx.javafxplugin' version '0.1.0'
|
||||
}
|
||||
|
||||
repositories {
|
||||
// Use Maven Central for resolving dependencies.
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Use JUnit Jupiter for testing.
|
||||
testImplementation libs.junit.jupiter
|
||||
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
|
||||
// This dependency is used by the application.
|
||||
implementation libs.guava
|
||||
}
|
||||
|
||||
// Apply a specific Java toolchain to ease working on different environments.
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(21)
|
||||
}
|
||||
}
|
||||
|
||||
javafx {
|
||||
modules = [ 'javafx.graphics', 'javafx.controls', 'javafx.fxml' ]
|
||||
}
|
||||
|
||||
application {
|
||||
// Define the main class for the application.
|
||||
if (project.hasProperty('admin')) {
|
||||
mainClass = 'ChatApp'
|
||||
} else if (project.hasProperty('server')) {
|
||||
mainClass = 'ChatAppServer'
|
||||
} else if (project.hasProperty('client')) {
|
||||
mainClass = 'ChatAppClient'
|
||||
} else if (project.hasProperty('serverGui')) {
|
||||
mainClass = 'server.ServerGui'
|
||||
} else {
|
||||
mainClass = 'client.ClientGui'
|
||||
}
|
||||
}
|
||||
|
||||
run {
|
||||
standardInput = System.in
|
||||
}
|
||||
|
||||
tasks.named('test') {
|
||||
// Use JUnit Platform for unit tests.
|
||||
useJUnitPlatform()
|
||||
}
|
||||
23
ChatApp/app/src/main/java/ChatApp.java
Normal file
23
ChatApp/app/src/main/java/ChatApp.java
Normal file
@@ -0,0 +1,23 @@
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
import client.ClientConsole;
|
||||
import server.Server;
|
||||
|
||||
public class ChatApp {
|
||||
public static void main(String[] args) throws Exception {
|
||||
Server server = new Server(6665);
|
||||
ClientConsole client = new ClientConsole(new InetSocketAddress("localhost", 6665));
|
||||
|
||||
client.onConnect.connect((arg) -> {
|
||||
client.getClientInterface().SendCreateRoom("101");
|
||||
});
|
||||
|
||||
client.onDisconnect.connect((arg) -> {
|
||||
System.out.println("Stopping server ...");
|
||||
|
||||
server.close();
|
||||
|
||||
System.out.println("Done !");
|
||||
});
|
||||
}
|
||||
}
|
||||
15
ChatApp/app/src/main/java/ChatAppClient.java
Normal file
15
ChatApp/app/src/main/java/ChatAppClient.java
Normal file
@@ -0,0 +1,15 @@
|
||||
import client.ClientConsole;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
public class ChatAppClient {
|
||||
public static void main(String[] args) {
|
||||
ClientConsole console = new ClientConsole(new InetSocketAddress("localhost", 6665));
|
||||
try {
|
||||
console.joinThread();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
System.out.println("End !");
|
||||
}
|
||||
}
|
||||
15
ChatApp/app/src/main/java/ChatAppServer.java
Normal file
15
ChatApp/app/src/main/java/ChatAppServer.java
Normal file
@@ -0,0 +1,15 @@
|
||||
import java.net.SocketException;
|
||||
|
||||
import server.Server;
|
||||
|
||||
public class ChatAppServer {
|
||||
public static void main(String[] args) {
|
||||
// run ./gradlew server --args="port" to launch the server with a specific port
|
||||
try {
|
||||
Server server = new Server(args.length > 0 ? Integer.parseInt(args[0]) : 0);
|
||||
System.out.println("Server running on port " + server.getRunningPort() + "!");
|
||||
} catch (SocketException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
58
ChatApp/app/src/main/java/client/Client.java
Normal file
58
ChatApp/app/src/main/java/client/Client.java
Normal file
@@ -0,0 +1,58 @@
|
||||
package client;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketException;
|
||||
|
||||
import network.Socket;
|
||||
import network.protocol.packets.*;
|
||||
|
||||
public class Client {
|
||||
|
||||
private final ClientConnexion connexion;
|
||||
private ClientListener callback;
|
||||
|
||||
public Client(InetSocketAddress serverAddress, ClientListener callback, String pseudo) throws SocketException {
|
||||
this.connexion = new ClientConnexion(new Socket(), serverAddress, callback);
|
||||
this.callback = callback;
|
||||
login(pseudo);
|
||||
}
|
||||
|
||||
private void login(String pseudo) {
|
||||
this.connexion.sendPacket(new LoginPacket(pseudo));
|
||||
}
|
||||
|
||||
public void close() {
|
||||
this.connexion.sendPacket(new DisconnectPacket("Leaving"));
|
||||
this.connexion.close();
|
||||
this.callback.handleDisconnect();
|
||||
}
|
||||
|
||||
public void SendChatMessage(String message) {
|
||||
this.connexion.sendPacket(new SendChatMessagePacket(message));
|
||||
}
|
||||
|
||||
public void SendCreateRoom(String roomName) {
|
||||
this.connexion.sendPacket(new CreateRoomPacket(roomName));
|
||||
}
|
||||
|
||||
public void SendJoinRoom(String roomName) {
|
||||
this.connexion.sendPacket(new JoinRoomPacket(roomName));
|
||||
}
|
||||
|
||||
public void SendLeaveRoom() {
|
||||
this.connexion.sendPacket(new LeaveRoomPacket());
|
||||
}
|
||||
|
||||
public void RequestRoomList() {
|
||||
this.connexion.sendPacket(new RequestRoomListPacket());
|
||||
}
|
||||
|
||||
public void RequestActualRoom() {
|
||||
this.connexion.sendPacket(new RequestActualRoomPacket());
|
||||
}
|
||||
|
||||
public void changeCallback(ClientListener callback) {
|
||||
this.callback = callback;
|
||||
this.connexion.changeCallback(callback);
|
||||
}
|
||||
}
|
||||
117
ChatApp/app/src/main/java/client/ClientConnexion.java
Normal file
117
ChatApp/app/src/main/java/client/ClientConnexion.java
Normal file
@@ -0,0 +1,117 @@
|
||||
package client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
import network.PacketHandler;
|
||||
import network.Socket;
|
||||
import network.protocol.Packet;
|
||||
import network.protocol.PacketVisitor;
|
||||
import network.protocol.packets.*;
|
||||
import network.protocol.packets.ServerResponsePacket.Response;
|
||||
|
||||
public class ClientConnexion implements PacketVisitor, PacketHandler {
|
||||
|
||||
private final InetSocketAddress serverAddress;
|
||||
private final Socket socket;
|
||||
private ClientListener callback;
|
||||
|
||||
public ClientConnexion(Socket socket, InetSocketAddress serverAddress, ClientListener callback) {
|
||||
this.serverAddress = serverAddress;
|
||||
this.socket = socket;
|
||||
this.callback = callback;
|
||||
this.socket.onClose.connect((args) -> onSocketClose());
|
||||
this.socket.addHandler(this);
|
||||
}
|
||||
|
||||
private void onSocketClose() {
|
||||
this.callback.handleConnexionError();
|
||||
}
|
||||
|
||||
public void close() {
|
||||
this.socket.close();
|
||||
}
|
||||
|
||||
public void sendPacket(Packet packet) {
|
||||
try {
|
||||
this.socket.sendPacket(packet, serverAddress);
|
||||
} catch (IOException e) {
|
||||
this.close();
|
||||
this.callback.handleConnexionError();
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePacket(Packet packet, InetSocketAddress address) {
|
||||
// we assume that the packet comes from the server
|
||||
visit(packet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(ChatMessagePacket packet) {
|
||||
this.callback.handleChatMessage(packet.getTime(), packet.getChatter(), packet.getContent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(RoomListPacket packet) {
|
||||
this.callback.handleRoomList(packet.getRoomNames());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(ServerResponsePacket packet) {
|
||||
this.callback.handleServerResponse(packet.getResponse());
|
||||
if (packet.getResponse() == Response.AuthSuccess)
|
||||
this.callback.handleConnect();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(ActualRoomPacket packet) {
|
||||
this.callback.handleActualRoom(packet.getRoomName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(DisconnectPacket packet) {
|
||||
this.close();
|
||||
this.callback.handleDisconnect();
|
||||
}
|
||||
|
||||
public void changeCallback(ClientListener callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(RequestActualRoomPacket packet) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(CreateRoomPacket packet) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(JoinRoomPacket packet) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(LeaveRoomPacket packet) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(LoginPacket packet) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(RequestRoomListPacket packet) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(SendChatMessagePacket packet) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
|
||||
}
|
||||
}
|
||||
187
ChatApp/app/src/main/java/client/ClientConsole.java
Normal file
187
ChatApp/app/src/main/java/client/ClientConsole.java
Normal file
@@ -0,0 +1,187 @@
|
||||
package client;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketException;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Scanner;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import network.protocol.ANSIColor;
|
||||
import network.protocol.packets.ServerResponsePacket;
|
||||
import network.protocol.packets.ServerResponsePacket.Response;
|
||||
import utilities.Signal;
|
||||
|
||||
public class ClientConsole implements ClientListener {
|
||||
|
||||
private Client client;
|
||||
private final Thread inputThread;
|
||||
private final Scanner scanner;
|
||||
|
||||
public final Signal onConnect = new Signal();
|
||||
public final Signal onDisconnect = new Signal();
|
||||
|
||||
public ClientConsole(InetSocketAddress address) {
|
||||
this.inputThread = new Thread(this::inputLoop);
|
||||
this.scanner = new Scanner(System.in);
|
||||
String pseudo = inputPseudo();
|
||||
try {
|
||||
this.client = new Client(address, this, pseudo);
|
||||
this.inputThread.start();
|
||||
} catch (SocketException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private String inputPseudo() {
|
||||
System.out.println("Enter your pseudo:");
|
||||
String pseudo = "chatter";
|
||||
pseudo = this.scanner.nextLine();
|
||||
return pseudo;
|
||||
}
|
||||
|
||||
public Client getClientInterface() {
|
||||
return this.client;
|
||||
}
|
||||
|
||||
private void inputLoop() {
|
||||
while (!Thread.interrupted()) {
|
||||
String message = this.scanner.nextLine();
|
||||
if (Thread.interrupted())
|
||||
break;
|
||||
visitMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
public void joinThread() throws InterruptedException {
|
||||
this.inputThread.join();
|
||||
}
|
||||
|
||||
private void printHelp(Map<List<String>, Consumer<String>> commands) {
|
||||
System.out.println("Available commands:");
|
||||
for (var entry : commands.entrySet()) {
|
||||
List<String> commandNames = entry.getKey();
|
||||
System.out.println("\t" + commandNames.get(0) + (commandNames.size() == 2 ? " or " + commandNames.get(1) : ""));
|
||||
}
|
||||
// the clearLine eats the last line if a new line is not skipped
|
||||
System.out.println("\n");
|
||||
}
|
||||
|
||||
private void visitMessage(String message) {
|
||||
if (!message.startsWith("/")) {
|
||||
this.client.SendChatMessage(message);
|
||||
clearLine();
|
||||
return;
|
||||
}
|
||||
|
||||
final Map<List<String>, Consumer<String>> commands = Map.of(
|
||||
List.of("/createRoom <roomName>", "/create <roomName>"), (args) -> this.client.SendCreateRoom(args),
|
||||
List.of("/listRooms", "/list"), (args) -> this.client.RequestRoomList(),
|
||||
List.of("/joinRoom <roomName>", "/join <roomName>"), (args) -> this.client.SendJoinRoom(args),
|
||||
List.of("/leaveRoom", "/leave"), (args) -> this.client.SendLeaveRoom(),
|
||||
List.of("/actualRoom", "/room"), (args) -> this.client.RequestActualRoom(),
|
||||
List.of("/goodbye", "/bye"), (args) -> this.client.close(),
|
||||
List.of("/help"), (args) -> {});
|
||||
|
||||
boolean commandFound = false;
|
||||
|
||||
for (var entry : commands.entrySet()) {
|
||||
List<String> commandNames = entry.getKey();
|
||||
Consumer<String> runnable = entry.getValue();
|
||||
for (String cmd : commandNames) {
|
||||
String[] cmdParts = cmd.split(" ");
|
||||
if (cmdParts.length > 1) {
|
||||
// the command expects arguments
|
||||
if (message.startsWith(cmdParts[0] + " ")) {
|
||||
runnable.accept(message.substring(cmdParts[0].length() + 1).trim());
|
||||
commandFound = true;
|
||||
}
|
||||
} else {
|
||||
// the command does not expect arguments
|
||||
if (message.equals(cmd)) {
|
||||
runnable.accept(message.substring(cmd.length()).trim());
|
||||
commandFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (commandFound)
|
||||
break;
|
||||
}
|
||||
|
||||
if (commandFound) {
|
||||
if (message.equals("/help"))
|
||||
printHelp(commands);
|
||||
clearLine();
|
||||
return;
|
||||
}
|
||||
|
||||
System.out.println(ANSIColor.formatString("&rUnknown command&n"));
|
||||
printHelp(commands);
|
||||
clearLine();
|
||||
}
|
||||
|
||||
private void clearLine() {
|
||||
System.out.print("\033[1A\r\033[2K"); // weird sequence to clear the line (but it works !)
|
||||
System.out.flush();
|
||||
}
|
||||
|
||||
private void stop() {
|
||||
this.inputThread.interrupt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleDisconnect() {
|
||||
System.out.println("Disconnected !");
|
||||
stop();
|
||||
this.onDisconnect.emit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleConnexionError() {
|
||||
System.out.println("An error occured during the connexion !");
|
||||
stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleChatMessage(Instant time, String chatter, String content) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String strTime = time.toString();
|
||||
sb.append("&y[")
|
||||
.append(strTime, 11, 19) // We only take the HH:MM:SS part
|
||||
.append("]&n")
|
||||
.append(" ")
|
||||
.append(chatter)
|
||||
.append(" : ")
|
||||
.append(content).append("&n"); // make the color back to normal at the end of every message
|
||||
System.out.println(ANSIColor.formatString(sb.toString()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRoomList(List<String> roomNames) {
|
||||
System.out.println(roomNames.isEmpty()
|
||||
? "No rooms available"
|
||||
: "Rooms : \n\t" + String.join("\n\t", roomNames));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleActualRoom(String roomName) {
|
||||
System.out.println(roomName != null ? ANSIColor.formatString("You are now in room &b" + roomName + "&n") : "You are not in a room");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleServerResponse(Response response) {
|
||||
if (response == ServerResponsePacket.Response.MessageSent
|
||||
|| response == ServerResponsePacket.Response.MessageNotSent) {
|
||||
return;
|
||||
}
|
||||
System.out.println(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleConnect() {
|
||||
System.out.println("Connected to server !");
|
||||
this.onConnect.emit();
|
||||
}
|
||||
|
||||
}
|
||||
36
ChatApp/app/src/main/java/client/ClientGui.java
Normal file
36
ChatApp/app/src/main/java/client/ClientGui.java
Normal file
@@ -0,0 +1,36 @@
|
||||
package client;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.stage.Screen;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class ClientGui extends Application {
|
||||
public static void main(String[] args) {
|
||||
launch(args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Stage stage) throws Exception {
|
||||
FXMLLoader loader = new FXMLLoader(getClass().getResource("/client/clientLogin.fxml"));
|
||||
Scene scene = new Scene(loader.load(), 400, 240);
|
||||
|
||||
double screenWidth = Screen.getPrimary().getVisualBounds().getWidth();
|
||||
double screenHeight = Screen.getPrimary().getVisualBounds().getHeight();
|
||||
|
||||
double xPos = screenWidth / 2 - scene.getWidth() / 2;
|
||||
double yPos = screenHeight / 2 - scene.getHeight() / 2;
|
||||
|
||||
scene.getStylesheets().add(getClass().getResource("clientStyle.css").toExternalForm());
|
||||
stage.getIcons().add(new Image(Objects.requireNonNull(getClass().getResourceAsStream("/liscord.png"))));
|
||||
stage.setTitle("Liscord");
|
||||
stage.setScene(scene);
|
||||
stage.setX(xPos);
|
||||
stage.setY(yPos);
|
||||
stage.show();
|
||||
}
|
||||
}
|
||||
283
ChatApp/app/src/main/java/client/ClientGuiController.java
Normal file
283
ChatApp/app/src/main/java/client/ClientGuiController.java
Normal file
@@ -0,0 +1,283 @@
|
||||
package client;
|
||||
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Duration;
|
||||
import network.protocol.packets.ServerResponsePacket;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketException;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class ClientGuiController implements ClientListener {
|
||||
|
||||
private Client client;
|
||||
|
||||
private boolean connected = true;
|
||||
|
||||
@FXML
|
||||
private BorderPane vueContainer;
|
||||
|
||||
@FXML
|
||||
private VBox roomList;
|
||||
|
||||
@FXML
|
||||
private VBox chatList;
|
||||
|
||||
@FXML
|
||||
private HBox chatInput;
|
||||
|
||||
@FXML
|
||||
private ScrollPane chatPane;
|
||||
|
||||
@FXML
|
||||
private Label roomName;
|
||||
|
||||
public void setClient(Client client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void initialize() throws SocketException {
|
||||
client = new Client(new InetSocketAddress("192.168.163.131", 6665), this, UsernameSingleton.getInstance().getUsername());
|
||||
Platform.runLater(() -> {
|
||||
Stage stage = (Stage) vueContainer.getScene().getWindow();
|
||||
stage.setResizable(true); // Fixed a bug that made the close button disappear under my Wayland setup (don't know if it's the same for x11)
|
||||
stage.setOnCloseRequest(event -> {
|
||||
connected = false;
|
||||
client.close();
|
||||
});
|
||||
chatList.heightProperty().addListener((obs, oldVal, newVal) -> chatPane.setVvalue(1.0));
|
||||
});
|
||||
|
||||
requestRoomsRegularly();
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the list of rooms from the server every second.
|
||||
*/
|
||||
private void requestRoomsRegularly() {
|
||||
Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(10), event -> client.RequestRoomList()));
|
||||
timeline.setCycleCount(Timeline.INDEFINITE);
|
||||
timeline.play();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleDisconnect() {
|
||||
System.out.println("Disconnected");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an alert dialog when the connection to the server fails.
|
||||
*/
|
||||
@Override
|
||||
public void handleConnexionError() {
|
||||
if(connected) {
|
||||
Platform.runLater(() -> {
|
||||
Alert alert = new Alert(Alert.AlertType.ERROR);
|
||||
alert.setTitle("Connection");
|
||||
alert.setHeaderText("Connection failed");
|
||||
Optional<ButtonType> res = alert.showAndWait();
|
||||
if (res.isPresent() && res.get() == ButtonType.OK) {
|
||||
((Stage) vueContainer.getScene().getWindow()).close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an alert dialog when the connection to the server is successful.
|
||||
*/
|
||||
@Override
|
||||
public void handleConnect() {
|
||||
Platform.runLater(() -> {
|
||||
Alert alert = new Alert(Alert.AlertType.INFORMATION);
|
||||
alert.setTitle("Connection");
|
||||
alert.setHeaderText("Connection to the server successful");
|
||||
alert.showAndWait();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format and insert the received chat message into the chat list.
|
||||
* @param time the time the message was sent
|
||||
* @param chatter the name of the person who sent the message
|
||||
* @param content the content of the message
|
||||
*/
|
||||
@Override
|
||||
public void handleChatMessage(Instant time, String chatter, String content) {
|
||||
String untagged = untag(content);
|
||||
String messageColor = Objects.equals(content, untagged) ? "lightblue" : "lightgreen";
|
||||
Platform.runLater(() -> {
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm")
|
||||
.withZone(ZoneId.systemDefault());
|
||||
String timeString = formatter.format(time);
|
||||
Text chatterText = new Text(chatter + ": ");
|
||||
chatterText.setStyle("-fx-fill: black; -fx-font-weight: bold;");
|
||||
TextFlow messageText = formatMessage(untagged);
|
||||
TextFlow wholeMessage = new TextFlow(chatterText, messageText);
|
||||
Text timeText = new Text(" " + timeString);
|
||||
timeText.setStyle("-fx-fill: gray; -fx-font-size: 10px;");
|
||||
HBox messageContainer = new HBox(5, wholeMessage, timeText);
|
||||
messageContainer.setStyle("-fx-background-color: " + messageColor + "; -fx-padding: 8; -fx-background-radius: 5;");
|
||||
messageContainer.setMaxWidth(Double.MAX_VALUE);
|
||||
HBox.setHgrow(messageText, Priority.ALWAYS);
|
||||
chatList.getChildren().add(messageContainer);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the tag from the message (handle ANSI characters encoded by the server)
|
||||
* @param message the message to untag
|
||||
* @return the untagged message
|
||||
*/
|
||||
public String untag(String message) {
|
||||
Pattern pattern = Pattern.compile("\u001B\\[44;30m(.*?)\u001B\\[49;39m");
|
||||
Matcher matcher = pattern.matcher(message);
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
while (matcher.find()) {
|
||||
String taggedName = matcher.group(1);
|
||||
matcher.appendReplacement(result, taggedName);
|
||||
}
|
||||
matcher.appendTail(result);
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the list of rooms in the room list.
|
||||
* @param roomNames the list of room names
|
||||
*/
|
||||
@Override
|
||||
public void handleRoomList(List<String> roomNames) {
|
||||
Platform.runLater(() -> {
|
||||
roomList.getChildren().clear();
|
||||
for (String roomName : roomNames) {
|
||||
Button button = new Button(roomName);
|
||||
button.setOnAction(event -> {
|
||||
client.SendJoinRoom(roomName);
|
||||
createChatEnv(roomName);
|
||||
});
|
||||
roomList.getChildren().add(button);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the chat (message wall + input) environment.
|
||||
* @param roomName the name of the room
|
||||
*/
|
||||
private void createChatEnv(String roomName) {
|
||||
Platform.runLater(() -> {
|
||||
this.roomName.setText("Room: " + roomName);
|
||||
chatList.getChildren().clear();
|
||||
chatInput.getChildren().clear();
|
||||
Button leaveButton = new Button("Leave room");
|
||||
leaveButton.setOnAction(event -> {
|
||||
client.SendLeaveRoom();
|
||||
this.roomName.setText("");
|
||||
chatList.getChildren().clear();
|
||||
chatInput.getChildren().clear();
|
||||
});
|
||||
chatList.getChildren().add(leaveButton);
|
||||
|
||||
TextField messageInput = new TextField();
|
||||
messageInput.setPromptText("Type a message...");
|
||||
messageInput.getStyleClass().add("message-input");
|
||||
HBox.setHgrow(messageInput, Priority.ALWAYS);
|
||||
messageInput.onActionProperty().set(event -> {
|
||||
client.SendChatMessage(messageInput.getText());
|
||||
messageInput.clear();
|
||||
});
|
||||
chatInput.getChildren().add(messageInput);
|
||||
|
||||
Button sendButton = new Button("Send");
|
||||
sendButton.getStyleClass().add("send-button");
|
||||
sendButton.setOnAction(event -> {
|
||||
client.SendChatMessage(messageInput.getText());
|
||||
messageInput.clear();
|
||||
});
|
||||
chatInput.getChildren().add(sendButton);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Colorize the message according to the color codes.
|
||||
* @param content the content of the message
|
||||
* @return the formatted message
|
||||
*/
|
||||
private TextFlow formatMessage(String content) {
|
||||
TextFlow textFlow = new TextFlow();
|
||||
// Evil regex : match every sequence starting with a color code ending with the next color code
|
||||
Pattern pattern = Pattern.compile("&([rbgyn])([^&]*)");
|
||||
Matcher matcher = pattern.matcher(content);
|
||||
int lastIndex = 0;
|
||||
|
||||
while (matcher.find()) {
|
||||
if (matcher.start() > lastIndex) {
|
||||
textFlow.getChildren().add(new Text(content.substring(lastIndex, matcher.start())));
|
||||
}
|
||||
|
||||
String colorCode = matcher.group(1);
|
||||
String textPart = matcher.group(2);
|
||||
|
||||
String color = switch (colorCode) {
|
||||
case "r" -> "red";
|
||||
case "b" -> "blue";
|
||||
case "g" -> "green";
|
||||
case "y" -> "gray";
|
||||
default -> "black";
|
||||
};
|
||||
|
||||
Text coloredText = new Text(textPart);
|
||||
coloredText.setStyle("-fx-fill: " + color + ";");
|
||||
textFlow.getChildren().add(coloredText);
|
||||
|
||||
lastIndex = matcher.end();
|
||||
}
|
||||
|
||||
// Add the remaining text (works if there's no color code at all)
|
||||
if (lastIndex < content.length()) {
|
||||
textFlow.getChildren().add(new Text(content.substring(lastIndex)));
|
||||
}
|
||||
|
||||
return textFlow;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleServerResponse(ServerResponsePacket.Response response) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleActualRoom(String roomName) {
|
||||
|
||||
}
|
||||
|
||||
public void createRoom() {
|
||||
TextInputDialog dialog = new TextInputDialog();
|
||||
dialog.setTitle("Create a room");
|
||||
dialog.setHeaderText("Enter the name of the room");
|
||||
dialog.setContentText("Room name:");
|
||||
Optional<String> result = dialog.showAndWait();
|
||||
result.ifPresent(name -> {
|
||||
client.SendCreateRoom(name);
|
||||
createChatEnv(name);
|
||||
});
|
||||
}
|
||||
}
|
||||
18
ChatApp/app/src/main/java/client/ClientListener.java
Normal file
18
ChatApp/app/src/main/java/client/ClientListener.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package client;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import network.protocol.packets.ServerResponsePacket;
|
||||
|
||||
public interface ClientListener {
|
||||
|
||||
void handleDisconnect();
|
||||
void handleConnexionError();
|
||||
void handleConnect();
|
||||
void handleChatMessage(Instant time, String chatter, String content);
|
||||
void handleRoomList(List<String> roomNames);
|
||||
void handleServerResponse(ServerResponsePacket.Response response);
|
||||
void handleActualRoom(String roomName);
|
||||
|
||||
}
|
||||
55
ChatApp/app/src/main/java/client/ClientLoading.java
Normal file
55
ChatApp/app/src/main/java/client/ClientLoading.java
Normal file
@@ -0,0 +1,55 @@
|
||||
package client;
|
||||
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.animation.RotateTransition;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.shape.Arc;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static utilities.FxUtilities.centerStage;
|
||||
|
||||
public class ClientLoading {
|
||||
@FXML
|
||||
private Arc spinnerArc;
|
||||
|
||||
/**
|
||||
* Initialize the loading screen.
|
||||
*/
|
||||
public void initialize() {
|
||||
// Rotation animation cycle
|
||||
RotateTransition rotate = new RotateTransition(Duration.seconds(1), spinnerArc);
|
||||
rotate.setByAngle(360);
|
||||
rotate.setCycleCount(RotateTransition.INDEFINITE);
|
||||
rotate.play();
|
||||
|
||||
// Yes, this loading screen is just for show. But it's fun and pretty.
|
||||
// PauseTransition allows no to block the rotation animation.
|
||||
PauseTransition pause = new PauseTransition(Duration.millis(1000));
|
||||
pause.setOnFinished(event -> {
|
||||
try {
|
||||
login();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
pause.play();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the client GUI and switch to it.
|
||||
* @throws IOException if the FXML file is not found.
|
||||
*/
|
||||
public void login() throws IOException {
|
||||
var loader = new FXMLLoader(getClass().getResource("/client/clientVue.fxml"));
|
||||
loader.load();
|
||||
Stage stage = (Stage) spinnerArc.getScene().getWindow();
|
||||
stage.setScene(new Scene(loader.getRoot(), 800, 600));
|
||||
centerStage(stage);
|
||||
stage.getScene().getStylesheets().add(getClass().getResource("clientStyle.css").toExternalForm());
|
||||
}
|
||||
}
|
||||
33
ChatApp/app/src/main/java/client/ClientLogin.java
Normal file
33
ChatApp/app/src/main/java/client/ClientLogin.java
Normal file
@@ -0,0 +1,33 @@
|
||||
package client;
|
||||
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.stage.Stage;
|
||||
import java.io.IOException;
|
||||
|
||||
import static utilities.FxUtilities.centerStage;
|
||||
|
||||
public class ClientLogin {
|
||||
@FXML
|
||||
public TextField usernameField;
|
||||
|
||||
/**
|
||||
* Sets the username and switches to the loading screen.
|
||||
* @throws IOException if the FXML file is not found.
|
||||
*/
|
||||
@FXML
|
||||
private void login() throws IOException {
|
||||
String username = usernameField.getText();
|
||||
if(username.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
UsernameSingleton.getInstance().setUsername(username);
|
||||
var loader = new FXMLLoader(getClass().getResource("/client/clientLoading.fxml"));
|
||||
loader.load();
|
||||
Stage stage = (Stage) usernameField.getScene().getWindow();
|
||||
stage.setScene(new Scene(loader.getRoot(), 800, 600));
|
||||
centerStage(stage);
|
||||
}
|
||||
}
|
||||
31
ChatApp/app/src/main/java/client/UsernameSingleton.java
Normal file
31
ChatApp/app/src/main/java/client/UsernameSingleton.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package client;
|
||||
|
||||
/**
|
||||
* This class aims to make the username available to all controllers. (Recommended by JavaFX, because otherwise, FXMLLoader bugs and doesn't switch scenes properly.)
|
||||
*/
|
||||
public class UsernameSingleton {
|
||||
private static UsernameSingleton instance;
|
||||
private String username;
|
||||
|
||||
private UsernameSingleton() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the instance of the singleton.
|
||||
* @return the instance
|
||||
*/
|
||||
public static UsernameSingleton getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new UsernameSingleton();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
}
|
||||
35
ChatApp/app/src/main/java/network/IPAddressFinder.java
Normal file
35
ChatApp/app/src/main/java/network/IPAddressFinder.java
Normal file
@@ -0,0 +1,35 @@
|
||||
package network;
|
||||
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.SocketException;
|
||||
import java.util.Enumeration;
|
||||
import java.net.InetAddress;
|
||||
|
||||
public class IPAddressFinder {
|
||||
public static String findIPAddress(){
|
||||
try {
|
||||
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
|
||||
|
||||
while (networkInterfaces.hasMoreElements()) {
|
||||
NetworkInterface networkInterface = networkInterfaces.nextElement();
|
||||
|
||||
Enumeration<InetAddress> inetAddresses = networkInterface.getInetAddresses();
|
||||
|
||||
while (inetAddresses.hasMoreElements()) {
|
||||
InetAddress inetAddress = inetAddresses.nextElement();
|
||||
|
||||
if (!inetAddress.isLoopbackAddress()) {
|
||||
String ip = inetAddress.getHostAddress();
|
||||
|
||||
if (ip.startsWith("192.168")) {
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SocketException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return "Not Found";
|
||||
}
|
||||
}
|
||||
356
ChatApp/app/src/main/java/network/PacketPool.java
Normal file
356
ChatApp/app/src/main/java/network/PacketPool.java
Normal file
@@ -0,0 +1,356 @@
|
||||
package network;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Objects;
|
||||
import java.util.Stack;
|
||||
|
||||
import network.protocol.Packet;
|
||||
import network.protocol.packets.AcknowlegdementPacket;
|
||||
import network.protocol.packets.DisconnectPacket;
|
||||
|
||||
public class PacketPool {
|
||||
|
||||
private final Stack<ReliablePacketAddress> packetQueue;
|
||||
private final Map<InetSocketAddress, AdressContext> addressContexts;
|
||||
|
||||
private final Socket socket;
|
||||
|
||||
private static int MAX_SEND_TRY = 50;
|
||||
private static long SEND_DELAY = 10;
|
||||
private static long RETRY_INTERVAL = 100;
|
||||
private static float PACKET_LOSS_PROBABILITY = 0.1f;
|
||||
|
||||
private static record ReliablePacketAddress(ReliablePacket packet, InetSocketAddress address) {
|
||||
@Override
|
||||
public final int hashCode() {
|
||||
return Objects.hash(packet, address);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean equals(Object arg0) {
|
||||
if (arg0 instanceof ReliablePacketAddress packetAddress)
|
||||
return packetAddress.address().equals(this.address())
|
||||
&& packetAddress.packet().getSeq() == this.packet().getSeq();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data specific to one connexion
|
||||
*/
|
||||
private static class AdressContext {
|
||||
public int currentSeqRecv = 0;
|
||||
public int currentSeqSend = 0;
|
||||
public final List<Thread> sendThreads = new ArrayList<>();
|
||||
public final List<ReliablePacket> packetRecvBuffer = new ArrayList<>();
|
||||
public final Map<ReliablePacket, Integer> packetsSentTries = new HashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param address the address to send to
|
||||
* @return The next send sequence
|
||||
*/
|
||||
private int getNextSeqSend(InetSocketAddress address) {
|
||||
return this.addressContexts.get(address).currentSeqSend++;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param address the address which a packet was recieved
|
||||
* @return the current recieve sequence number
|
||||
*/
|
||||
private int getCurrentSeqRecv(InetSocketAddress address) {
|
||||
return this.addressContexts.get(address).currentSeqRecv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the recieve sequence number
|
||||
*
|
||||
* @param address
|
||||
* @param newValue
|
||||
*/
|
||||
private void setSeqRecv(InetSocketAddress address, int newValue) {
|
||||
this.addressContexts.get(address).currentSeqRecv = newValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to add the address into memory
|
||||
*
|
||||
* @param address
|
||||
*/
|
||||
private void tryAddContext(InetSocketAddress address) {
|
||||
this.addressContexts.putIfAbsent(address, new AdressContext());
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a PacketPool
|
||||
*
|
||||
* @param socket
|
||||
*/
|
||||
public PacketPool(Socket socket) {
|
||||
this.socket = socket;
|
||||
this.packetQueue = new Stack<>();
|
||||
this.addressContexts = new HashMap<>();
|
||||
}
|
||||
|
||||
private void debugPrint(String msg) {
|
||||
// System.out.println(msg);
|
||||
}
|
||||
|
||||
private void debugSend(ReliablePacket packet, InetSocketAddress address) {
|
||||
boolean client = address.getPort() == 6665;
|
||||
debugPrint((client ? "[Client]" : "[Server]") + " Sent " + packet.getPacket().getClass().getName()
|
||||
+ " with seq : " + packet.getSeq());
|
||||
}
|
||||
|
||||
private void debugRecv(ReliablePacket packet, InetSocketAddress address) {
|
||||
boolean client = address.getPort() == 6665;
|
||||
debugPrint((client ? "[Client]" : "[Server]") + " Received " + packet.getPacket().getClass().getName()
|
||||
+ " with seq : " + packet.getSeq());
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a packet to the socket
|
||||
*
|
||||
* @param packet
|
||||
* @param address
|
||||
* @throws IOException
|
||||
*/
|
||||
private void sendPacketToSocket(ReliablePacket packet, InetSocketAddress address) throws IOException {
|
||||
var packetsSentTries = this.addressContexts.get(address).packetsSentTries;
|
||||
|
||||
new Thread(() -> {
|
||||
// try {
|
||||
// Thread.sleep(SEND_DELAY);
|
||||
// if (Math.random() > PACKET_LOSS_PROBABILITY)
|
||||
try {
|
||||
this.socket.sendPacket(packet, address);
|
||||
} catch (IOException e) {
|
||||
// e.printStackTrace();
|
||||
}
|
||||
// } catch (InterruptedException | IOException e) {
|
||||
// // e.printStackTrace();
|
||||
// }
|
||||
}).start();
|
||||
|
||||
if (packet.getPacket() instanceof AcknowlegdementPacket)
|
||||
return;
|
||||
|
||||
Integer count = packetsSentTries.get(packet);
|
||||
if (count == null) {
|
||||
packetsSentTries.put(packet, 1);
|
||||
} else {
|
||||
packetsSentTries.put(packet, count + 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a packet to socket and try to resend if an acknowlegment was not
|
||||
* recieved
|
||||
*
|
||||
* @param packet
|
||||
* @param address
|
||||
* @throws IOException
|
||||
*/
|
||||
private synchronized void sendPacket(ReliablePacket packet, InetSocketAddress address) throws IOException {
|
||||
sendPacketToSocket(packet, address);
|
||||
debugSend(packet, address);
|
||||
|
||||
ReliablePacketAddress reliablePacketAddress = new ReliablePacketAddress(packet, address);
|
||||
|
||||
if (!(packet.getPacket() instanceof AcknowlegdementPacket)) {
|
||||
Thread newThread = new Thread(() -> tryResend(reliablePacketAddress));
|
||||
this.addressContexts.get(address).sendThreads.add(newThread);
|
||||
newThread.start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a packet (and encapsulate it)
|
||||
*
|
||||
* @param packet
|
||||
* @param address
|
||||
* @throws IOException
|
||||
*/
|
||||
public synchronized void sendPacket(Packet packet, InetSocketAddress address) throws IOException {
|
||||
tryAddContext(address);
|
||||
ReliablePacket reliablePacket = new ReliablePacket(packet, getNextSeqSend(address));
|
||||
sendPacket(reliablePacket, address);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to resend a packet
|
||||
*
|
||||
* @param reliablePacketAddress
|
||||
*/
|
||||
private void tryResend(ReliablePacketAddress reliablePacketAddress) {
|
||||
try {
|
||||
while (!Thread.interrupted()) {
|
||||
Thread.sleep(RETRY_INTERVAL);
|
||||
var packetsSentTries = this.addressContexts.get(reliablePacketAddress.address()).packetsSentTries;
|
||||
// the packet has been received
|
||||
if (!packetsSentTries.containsKey(reliablePacketAddress.packet()))
|
||||
break;
|
||||
|
||||
Integer sendCount = packetsSentTries.get(reliablePacketAddress.packet());
|
||||
if (sendCount > MAX_SEND_TRY) {
|
||||
close(reliablePacketAddress.address());
|
||||
debugPrint(
|
||||
"Packet" + reliablePacketAddress.packet() + " not send after " + MAX_SEND_TRY + " tries");
|
||||
// simulating a fake disconnect packet
|
||||
this.socket.handlePacket(new DisconnectPacket("Timed out"), reliablePacketAddress.address());
|
||||
break;
|
||||
}
|
||||
|
||||
boolean client = reliablePacketAddress.address().getPort() == 6665;
|
||||
debugPrint((client ? "[Client]" : "[Server]") + " Trying to resend the packet "
|
||||
+ reliablePacketAddress.packet().getSeq() + " ...");
|
||||
sendPacketToSocket(reliablePacketAddress.packet(), reliablePacketAddress.address());
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
AdressContext ctx = this.addressContexts.get(reliablePacketAddress.address());
|
||||
if (ctx != null)
|
||||
ctx.sendThreads.remove(Thread.currentThread());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param address
|
||||
* @return The smallest sequence number recieved
|
||||
*/
|
||||
private ReliablePacket getMinimumSeqReceived(InetSocketAddress address) {
|
||||
List<ReliablePacket> packetRecvBuffer = this.addressContexts.get(address).packetRecvBuffer;
|
||||
if (packetRecvBuffer.isEmpty())
|
||||
return null;
|
||||
return Collections.min(packetRecvBuffer, (rel1, rel2) -> {
|
||||
return Integer.compare(rel1.getSeq(), rel2.getSeq());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Move packets in buffer to the packet recieved queue to be processed by the
|
||||
* app
|
||||
*
|
||||
* @param address
|
||||
* @return the sequence number of the last packet that was added to the queue
|
||||
*/
|
||||
private int fillPacketQueue(InetSocketAddress address) {
|
||||
List<ReliablePacket> packetRecvBuffer = this.addressContexts.get(address).packetRecvBuffer;
|
||||
ReliablePacket minimum = getMinimumSeqReceived(address);
|
||||
int lastSeqProcessed = -1;
|
||||
while (true) {
|
||||
this.packetQueue.add(new ReliablePacketAddress(minimum, address));
|
||||
packetRecvBuffer.remove(minimum);
|
||||
lastSeqProcessed = minimum.getSeq();
|
||||
|
||||
ReliablePacket nextMinimum = getMinimumSeqReceived(address);
|
||||
if (nextMinimum == null || nextMinimum.getSeq() != minimum.getSeq() + 1)
|
||||
break;
|
||||
minimum = nextMinimum;
|
||||
}
|
||||
Collections.reverse(this.packetQueue);
|
||||
return lastSeqProcessed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process packet when recieved
|
||||
*
|
||||
* @param packet
|
||||
* @param address
|
||||
* @throws IOException
|
||||
*/
|
||||
public void onPacketReceived(ReliablePacket packet, InetSocketAddress address) throws IOException {
|
||||
tryAddContext(address);
|
||||
|
||||
var packetsSentTries = this.addressContexts.get(address).packetsSentTries;
|
||||
|
||||
if (packet.getPacket() instanceof AcknowlegdementPacket ackPacket) {
|
||||
assert (ackPacket.getAck() != -1);
|
||||
for (var entry : packetsSentTries.entrySet()) {
|
||||
ReliablePacket reliablePacket = entry.getKey();
|
||||
if (entry.getKey().getSeq() == ackPacket.getAck()) {
|
||||
packetsSentTries.remove(reliablePacket);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.addressContexts.get(address).packetRecvBuffer.contains(packet)) {
|
||||
debugPrint("The packet has already been received !");
|
||||
sendPacketToSocket(
|
||||
new ReliablePacket(new AcknowlegdementPacket(packet.getSeq()), -1), address);
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet.getSeq() < getCurrentSeqRecv(address)) {
|
||||
debugPrint("Packet too old, current : " + getCurrentSeqRecv(address));
|
||||
sendPacketToSocket(
|
||||
new ReliablePacket(new AcknowlegdementPacket(packet.getSeq()), -1), address);
|
||||
return;
|
||||
}
|
||||
|
||||
this.addressContexts.get(address).packetRecvBuffer.add(packet);
|
||||
debugRecv(packet, address);
|
||||
sendPacketToSocket(
|
||||
new ReliablePacket(new AcknowlegdementPacket(packet.getSeq()), -1), address);
|
||||
|
||||
// got the packet in the right order
|
||||
if (packet.getSeq() == getCurrentSeqRecv(address)) {
|
||||
setSeqRecv(address, fillPacketQueue(address) + 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The next packet in the queue
|
||||
*/
|
||||
public Entry<Packet, InetSocketAddress> getNextPacket() {
|
||||
if (this.packetQueue.isEmpty())
|
||||
return null;
|
||||
ReliablePacketAddress last = this.packetQueue.pop();
|
||||
var entry = Map.entry(last.packet().getPacket(), last.address);
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the connexion
|
||||
*
|
||||
* @param adress
|
||||
*/
|
||||
private void close(InetSocketAddress adress) {
|
||||
var ctx = this.addressContexts.get(adress);
|
||||
if (ctx != null)
|
||||
close(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the threads of the connexion
|
||||
*
|
||||
* @param adressContext
|
||||
*/
|
||||
private void close(AdressContext adressContext) {
|
||||
for (Thread thread : adressContext.sendThreads) {
|
||||
thread.interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the PacketPool
|
||||
*/
|
||||
public void close() {
|
||||
for (AdressContext adressContext : this.addressContexts.values()) {
|
||||
close(adressContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
38
ChatApp/app/src/main/java/network/ReliablePacket.java
Normal file
38
ChatApp/app/src/main/java/network/ReliablePacket.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package network;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import network.protocol.Packet;
|
||||
|
||||
public class ReliablePacket implements Serializable {
|
||||
|
||||
private final Packet packet;
|
||||
private final int seq;
|
||||
|
||||
public ReliablePacket(Packet packet, int seq) {
|
||||
this.packet = packet;
|
||||
this.seq = seq;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The encapsulated packet
|
||||
*/
|
||||
public Packet getPacket() {
|
||||
return packet;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The sequence number of this packet
|
||||
*/
|
||||
public int getSeq() {
|
||||
return seq;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof ReliablePacket packet)
|
||||
return packet.getSeq() == this.getSeq();
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
158
ChatApp/app/src/main/java/network/Socket.java
Normal file
158
ChatApp/app/src/main/java/network/Socket.java
Normal file
@@ -0,0 +1,158 @@
|
||||
package network;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import network.protocol.Packet;
|
||||
import utilities.Signal;
|
||||
|
||||
public class Socket {
|
||||
private final DatagramSocket socket;
|
||||
private final List<PacketHandler> handlers;
|
||||
private final Thread readThread;
|
||||
private final PacketPool packetPool;
|
||||
|
||||
public final Signal onClose = new Signal();
|
||||
|
||||
/**
|
||||
* Construct a UDP Socket to connect to a server
|
||||
* @throws SocketException
|
||||
*/
|
||||
public Socket() throws SocketException {
|
||||
this.socket = new DatagramSocket();
|
||||
this.handlers = new ArrayList<>();
|
||||
this.packetPool = new PacketPool(this);
|
||||
this.readThread = new Thread(this::readLoop);
|
||||
this.readThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a UDP Socket to listen to connexion at the specified port
|
||||
* @param listeningPort the port to listen to
|
||||
* @throws SocketException
|
||||
*/
|
||||
public Socket(int listeningPort) throws SocketException {
|
||||
this.socket = new DatagramSocket(listeningPort);
|
||||
this.handlers = new ArrayList<>();
|
||||
this.packetPool = new PacketPool(this);
|
||||
this.readThread = new Thread(this::readLoop);
|
||||
this.readThread.start();
|
||||
}
|
||||
|
||||
public void addHandler(PacketHandler handler) {
|
||||
this.handlers.add(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return this.socket.getLocalPort()
|
||||
*/
|
||||
public int getLocalPort() {
|
||||
return this.socket.getLocalPort();
|
||||
}
|
||||
|
||||
// needs to be accessible by PacketPool
|
||||
/**
|
||||
* Send a packet to the specified address
|
||||
* @param packet the packet to send
|
||||
* @param address the address to send to
|
||||
* @throws IOException
|
||||
*/
|
||||
void sendPacket(ReliablePacket packet, InetSocketAddress address) throws IOException {
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
ObjectOutputStream oos = new ObjectOutputStream(stream);
|
||||
oos.writeObject(packet);
|
||||
oos.flush();
|
||||
byte[] data = stream.toByteArray();
|
||||
DatagramPacket dataPacket = new DatagramPacket(data, data.length, address.getAddress(),
|
||||
address.getPort());
|
||||
this.socket.send(dataPacket);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to recieve packets and send them to the PacketPool.
|
||||
* This method blocks until something has been read
|
||||
* @throws IOException
|
||||
* @throws ClassNotFoundException
|
||||
*/
|
||||
private void recievePacketReliable()
|
||||
throws IOException, ClassNotFoundException {
|
||||
byte[] buffer = new byte[65535];
|
||||
DatagramPacket dataPacket = new DatagramPacket(buffer, buffer.length);
|
||||
socket.receive(dataPacket);
|
||||
|
||||
InetSocketAddress address = new InetSocketAddress(dataPacket.getAddress(), dataPacket.getPort());
|
||||
|
||||
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(dataPacket.getData()));
|
||||
ReliablePacket packet = (ReliablePacket) ois.readObject();
|
||||
|
||||
this.packetPool.onPacketReceived(packet, address);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a packet to the specified address
|
||||
* @param packet the packet to send
|
||||
* @param address the address to send to
|
||||
* @throws IOException
|
||||
*/
|
||||
public void sendPacket(Packet packet, InetSocketAddress address) throws IOException {
|
||||
this.packetPool.sendPacket(packet, address);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recieve packet in a reliable way (packets are in the right order).
|
||||
* This method is blocking
|
||||
* @throws IOException
|
||||
* @throws ClassNotFoundException
|
||||
*/
|
||||
private void recievePacket() throws IOException, ClassNotFoundException {
|
||||
this.recievePacketReliable();
|
||||
var entry = this.packetPool.getNextPacket();
|
||||
while (entry != null) {
|
||||
this.handlePacket(entry.getKey(), entry.getValue());
|
||||
entry = this.packetPool.getNextPacket();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the socket
|
||||
*/
|
||||
public void close() {
|
||||
this.socket.close();
|
||||
this.packetPool.close();
|
||||
this.readThread.interrupt();
|
||||
this.onClose.emit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the packet
|
||||
* @param packet the packet to dispatch
|
||||
* @param address the incoming address
|
||||
*/
|
||||
void handlePacket(Packet packet, InetSocketAddress address) {
|
||||
for (PacketHandler handler : this.handlers) {
|
||||
handler.handlePacket(packet, address);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loop to read all packets
|
||||
*/
|
||||
private void readLoop() {
|
||||
try {
|
||||
while (!Thread.interrupted()) {
|
||||
recievePacket();
|
||||
}
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
// e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package network.protocol;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class ANSIColor {
|
||||
public static final String RESET = "\u001B[0m";
|
||||
public static final String BLACK = "\u001B[30m";
|
||||
@@ -15,4 +17,8 @@ public class ANSIColor {
|
||||
.replace("&y", GREY)
|
||||
.replace("&n", RESET);
|
||||
}
|
||||
|
||||
public static String tag(String message, String chatter){
|
||||
return message.replaceAll("(@" + Pattern.quote(chatter) + ")", "\u001B[44;30m$1\u001B[49;39m" );
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,10 @@ import java.io.Serializable;
|
||||
|
||||
public abstract class Packet implements Serializable {
|
||||
|
||||
/**
|
||||
* Used for double dispatch
|
||||
* @param packetVisitor the visitor to dispatch to
|
||||
*/
|
||||
public abstract void accept(PacketVisitor packetVisitor);
|
||||
|
||||
}
|
||||
@@ -1,23 +1,20 @@
|
||||
package network.protocol;
|
||||
|
||||
import network.protocol.packets.ChatMessagePacket;
|
||||
import network.protocol.packets.CreateRoomPacket;
|
||||
import network.protocol.packets.JoinRoomPacket;
|
||||
import network.protocol.packets.LeaveRoomPacket;
|
||||
import network.protocol.packets.LoginPacket;
|
||||
import network.protocol.packets.RequestRoomListPacket;
|
||||
import network.protocol.packets.RoomListPacket;
|
||||
import network.protocol.packets.SendChatMessagePacket;
|
||||
import network.protocol.packets.ServerResponsePacket;
|
||||
import network.protocol.packets.*;
|
||||
|
||||
public interface PacketVisitor {
|
||||
|
||||
/**
|
||||
* Use double dispatch to process a packet
|
||||
* @param packet the packet to process
|
||||
*/
|
||||
default void visit(Packet packet) {
|
||||
packet.accept(this);
|
||||
}
|
||||
|
||||
void visitPacket(ChatMessagePacket packet);
|
||||
void visitPacket(CreateRoomPacket packet);
|
||||
void visitPacket(DisconnectPacket packet);
|
||||
void visitPacket(JoinRoomPacket packet);
|
||||
void visitPacket(LeaveRoomPacket packet);
|
||||
void visitPacket(LoginPacket packet);
|
||||
@@ -25,5 +22,7 @@ public interface PacketVisitor {
|
||||
void visitPacket(RoomListPacket packet);
|
||||
void visitPacket(SendChatMessagePacket packet);
|
||||
void visitPacket(ServerResponsePacket packet);
|
||||
void visitPacket(RequestActualRoomPacket packet);
|
||||
void visitPacket(ActualRoomPacket packet);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package network.protocol.packets;
|
||||
|
||||
import network.protocol.Packet;
|
||||
import network.protocol.PacketVisitor;
|
||||
|
||||
public class AcknowlegdementPacket extends Packet {
|
||||
|
||||
private final int ack;
|
||||
|
||||
public AcknowlegdementPacket(int ack) {
|
||||
this.ack = ack;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(PacketVisitor packetVisitor) {
|
||||
// this packet should not be handled by the app
|
||||
}
|
||||
|
||||
public int getAck() {
|
||||
return ack;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package network.protocol.packets;
|
||||
|
||||
import network.protocol.Packet;
|
||||
import network.protocol.PacketVisitor;
|
||||
|
||||
public class ActualRoomPacket extends Packet {
|
||||
private final String roomName;
|
||||
|
||||
public ActualRoomPacket(String roomName) {
|
||||
this.roomName = roomName;
|
||||
}
|
||||
|
||||
public String getRoomName() {
|
||||
return roomName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(PacketVisitor packetVisitor) {
|
||||
packetVisitor.visitPacket(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package network.protocol.packets;
|
||||
|
||||
import network.protocol.Packet;
|
||||
import network.protocol.PacketVisitor;
|
||||
|
||||
public class DisconnectPacket extends Packet {
|
||||
|
||||
private final String reason;
|
||||
|
||||
public DisconnectPacket(String reason) {
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
public String getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(PacketVisitor packetVisitor) {
|
||||
packetVisitor.visitPacket(this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package network.protocol.packets;
|
||||
|
||||
import network.protocol.Packet;
|
||||
import network.protocol.PacketVisitor;
|
||||
|
||||
public class RequestActualRoomPacket extends Packet {
|
||||
public RequestActualRoomPacket() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(PacketVisitor packetVisitor) {
|
||||
packetVisitor.visitPacket(this);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,19 @@
|
||||
package network.protocol.packets;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import network.protocol.Packet;
|
||||
import network.protocol.PacketVisitor;
|
||||
|
||||
public class RoomListPacket extends Packet {
|
||||
|
||||
private final ArrayList<String> roomNames;
|
||||
private final List<String> roomNames;
|
||||
|
||||
public RoomListPacket(ArrayList<String> roomNames) {
|
||||
public RoomListPacket(List<String> roomNames) {
|
||||
this.roomNames = roomNames;
|
||||
}
|
||||
|
||||
public ArrayList<String> getRoomNames() {
|
||||
public List<String> getRoomNames() {
|
||||
return roomNames;
|
||||
}
|
||||
|
||||
153
ChatApp/app/src/main/java/server/Server.java
Normal file
153
ChatApp/app/src/main/java/server/Server.java
Normal file
@@ -0,0 +1,153 @@
|
||||
package server;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketException;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import network.PacketHandler;
|
||||
import network.Socket;
|
||||
import network.protocol.ANSIColor;
|
||||
import network.protocol.Packet;
|
||||
import network.protocol.packets.ChatMessagePacket;
|
||||
import network.protocol.packets.SendChatMessagePacket;
|
||||
|
||||
public class Server implements PacketHandler {
|
||||
|
||||
private final Socket serverSocket;
|
||||
private final Map<InetSocketAddress, ServerConnexion> connexions;
|
||||
private final Map<InetSocketAddress, Instant> connexionTimes;
|
||||
private final Map<String, ArrayList<ServerConnexion>> rooms;
|
||||
|
||||
public Server(int port) throws SocketException {
|
||||
this.serverSocket = new Socket(port);
|
||||
this.serverSocket.addHandler(this);
|
||||
this.connexions = new HashMap<>();
|
||||
this.connexionTimes = new HashMap<>();
|
||||
this.rooms = new HashMap<>();
|
||||
}
|
||||
|
||||
public ArrayList<String> getRoomNames() {
|
||||
return rooms.keySet().stream().collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
|
||||
}
|
||||
|
||||
public boolean isInRoom(ServerConnexion connexion) {
|
||||
return getRoomName(connexion) != null;
|
||||
}
|
||||
|
||||
public String getRoomName(ServerConnexion connexion) {
|
||||
for (Map.Entry<String, ArrayList<ServerConnexion>> entry : rooms.entrySet()) {
|
||||
if (entry.getValue().contains(connexion)) {
|
||||
return entry.getKey();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean createRoom(String roomName, ServerConnexion connexion) {
|
||||
if (rooms.containsKey(roomName)) {
|
||||
return false;
|
||||
}
|
||||
rooms.put(roomName, new ArrayList<>());
|
||||
leaveRoom(connexion); // Leave the current room (auto handle if not in a room)
|
||||
rooms.get(roomName).add(connexion); // Add the creator to the room
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean leaveRoom(ServerConnexion connexion) {
|
||||
String roomName = getRoomName(connexion);
|
||||
if (roomName != null) {
|
||||
rooms.get(roomName).remove(connexion);
|
||||
// Remove the room if it is empty
|
||||
if (rooms.get(roomName).isEmpty()) {
|
||||
rooms.remove(roomName);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean joinRoom(String roomName, ServerConnexion connexion) {
|
||||
leaveRoom(connexion); // Leave the current room (auto handle if not in a room)
|
||||
if (rooms.containsKey(roomName) && !rooms.get(roomName).contains(connexion)) {
|
||||
rooms.get(roomName).add(connexion);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean sendToRoom(String roomName, ChatMessagePacket packet) {
|
||||
if (roomName != null && rooms.containsKey(roomName)) {
|
||||
rooms.get(roomName).forEach(con -> con.sendPacket(
|
||||
new ChatMessagePacket(packet.getTime(), packet.getChatter(), ANSIColor.tag(packet.getContent(), con.getChatterName()))
|
||||
));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean sendToRoom(ServerConnexion connexion, ChatMessagePacket packet) {
|
||||
String roomName = getRoomName(connexion);
|
||||
return sendToRoom(roomName, packet);
|
||||
}
|
||||
|
||||
public boolean sendToRoom(ServerConnexion connexion, SendChatMessagePacket packet) {
|
||||
return sendToRoom(connexion,
|
||||
new ChatMessagePacket(Instant.now(), connexion.getChatterName(), packet.getContent()));
|
||||
}
|
||||
|
||||
public void close() {
|
||||
this.serverSocket.close();
|
||||
}
|
||||
|
||||
public boolean hasChatterName(String pseudo) {
|
||||
return this.connexions.values().stream()
|
||||
.anyMatch(connexion -> pseudo.equals(connexion.getChatterName()));
|
||||
}
|
||||
|
||||
void removeConnexion(ServerConnexion connexion) {
|
||||
for (var it = this.connexions.entrySet().iterator(); it.hasNext();) {
|
||||
var entry = it.next();
|
||||
if (entry.getValue() == connexion) {
|
||||
it.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (var entry : this.rooms.entrySet()) {
|
||||
if (entry.getValue().remove(connexion))
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePacket(Packet packet, InetSocketAddress address) {
|
||||
if (!connexions.containsKey(address)) {
|
||||
this.connexions.put(address, new ServerConnexion(this, serverSocket, address));
|
||||
}
|
||||
this.connexions.get(address).visit(packet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Avoid the server to spam the chat
|
||||
* @param address the address of the connexion
|
||||
* @return true if the connexion is the first received or older than 5 seconds
|
||||
*/
|
||||
public boolean handleConnexionTime(InetSocketAddress address) {
|
||||
if (!connexionTimes.containsKey(address)) {
|
||||
connexionTimes.put(address, Instant.now());
|
||||
return true;
|
||||
}
|
||||
Instant lastConnexion = connexionTimes.get(address);
|
||||
if (Instant.now().isAfter(lastConnexion.plusSeconds(5))) {
|
||||
connexionTimes.put(address, Instant.now());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public int getRunningPort() {
|
||||
return this.serverSocket.getLocalPort();
|
||||
}
|
||||
}
|
||||
206
ChatApp/app/src/main/java/server/ServerConnexion.java
Normal file
206
ChatApp/app/src/main/java/server/ServerConnexion.java
Normal file
@@ -0,0 +1,206 @@
|
||||
package server;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.time.Instant;
|
||||
|
||||
import network.Socket;
|
||||
import network.protocol.Packet;
|
||||
import network.protocol.PacketVisitor;
|
||||
import network.protocol.packets.*;
|
||||
import network.protocol.packets.ServerResponsePacket.Response;
|
||||
|
||||
public class ServerConnexion implements PacketVisitor {
|
||||
|
||||
private final Server server;
|
||||
private final InetSocketAddress clientAddress;
|
||||
private final Socket socket;
|
||||
private String chatterName;
|
||||
|
||||
public ServerConnexion(Server server, Socket socket, InetSocketAddress clientAddress) {
|
||||
this.clientAddress = clientAddress;
|
||||
this.server = server;
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
public String getChatterName() {
|
||||
return this.chatterName;
|
||||
}
|
||||
|
||||
public void sendPacket(Packet packet) {
|
||||
try {
|
||||
this.socket.sendPacket(packet, clientAddress);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
this.server.removeConnexion(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the client is logged in
|
||||
* @return true if the client is logged in
|
||||
*/
|
||||
private boolean checkLogin() {
|
||||
if (this.chatterName != null && this.chatterName.isEmpty()) {
|
||||
sendPacket(new ServerResponsePacket(Response.AuthError));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Make the server create a room & enter it
|
||||
* @param packet the packet containing the room name
|
||||
*/
|
||||
@Override
|
||||
public void visitPacket(CreateRoomPacket packet) {
|
||||
if (!checkLogin())
|
||||
return;
|
||||
boolean created = server.createRoom(packet.getRoomName(), this);
|
||||
sendPacket(new ServerResponsePacket(created ? Response.RoomCreated : Response.RoomNotCreated));
|
||||
if (created)
|
||||
onRoomJoin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the server join a room
|
||||
* @param packet the packet containing the room name
|
||||
*/
|
||||
@Override
|
||||
public void visitPacket(JoinRoomPacket packet) {
|
||||
if (!checkLogin())
|
||||
return;
|
||||
if (server.getRoomName(this) != null) {
|
||||
server.leaveRoom(this);
|
||||
return;
|
||||
}
|
||||
boolean joined = server.joinRoom(packet.getRoomName(), this);
|
||||
sendPacket(new ServerResponsePacket(joined ? Response.RoomJoined : Response.RoomNotJoined));
|
||||
if (joined)
|
||||
onRoomJoin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the server leave a room
|
||||
* @param packet the packet containing the room name
|
||||
*/
|
||||
@Override
|
||||
public void visitPacket(LeaveRoomPacket packet) {
|
||||
if (!checkLogin())
|
||||
return;
|
||||
String roomName = this.server.getRoomName(this);
|
||||
boolean left = server.leaveRoom(this);
|
||||
sendPacket(new ServerResponsePacket(left ? Response.RoomLeft : Response.RoomNotLeft));
|
||||
if (left)
|
||||
onRoomLeave(roomName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the pseudo is available (nobody connected with the same pseudo)
|
||||
* @param packet the packet containing the pseudo
|
||||
*/
|
||||
@Override
|
||||
public void visitPacket(LoginPacket packet) {
|
||||
if (packet.getPseudo().isEmpty() || server.hasChatterName(packet.getPseudo())) {
|
||||
sendPacket(new ServerResponsePacket(Response.AuthError));
|
||||
return;
|
||||
}
|
||||
|
||||
this.chatterName = packet.getPseudo();
|
||||
sendPacket(new ServerResponsePacket(Response.AuthSuccess));
|
||||
sendPacket(new RoomListPacket(server.getRoomNames()));
|
||||
System.out.println("[Server] Chatter " + packet.getPseudo() + " connected !");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of rooms
|
||||
* @param packet the packet containing the request
|
||||
*/
|
||||
@Override
|
||||
public void visitPacket(RequestRoomListPacket packet) {
|
||||
if (!checkLogin())
|
||||
return;
|
||||
sendPacket(new RoomListPacket(server.getRoomNames()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to the room
|
||||
* @param packet the packet containing the message
|
||||
*/
|
||||
@Override
|
||||
public void visitPacket(SendChatMessagePacket packet) {
|
||||
if (!checkLogin())
|
||||
return;
|
||||
boolean messageSent = server.sendToRoom(this, packet);
|
||||
sendPacket(new ServerResponsePacket(messageSent ? Response.MessageSent : Response.MessageNotSent));
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect the client
|
||||
* @param packet the packet containing the request
|
||||
*/
|
||||
@Override
|
||||
public void visitPacket(DisconnectPacket packet) {
|
||||
this.onDisconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all room connections while disconnecting the client
|
||||
*/
|
||||
private void onDisconnect() {
|
||||
if (this.server.isInRoom(this)) {
|
||||
this.onRoomLeave(this.server.getRoomName(this));
|
||||
}
|
||||
this.server.removeConnexion(this);
|
||||
if (chatterName != null)
|
||||
System.out.println("[Server] Chatter " + chatterName + " disconnected !");
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to the room when a client joins
|
||||
*/
|
||||
private void onRoomJoin() {
|
||||
String joinMessage = "Chatter " + this.chatterName + " joined the room !";
|
||||
this.server.sendToRoom(this, new ChatMessagePacket(Instant.now(), "", joinMessage));
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to the room when a client leaves
|
||||
* @param roomName the name of the room
|
||||
*/
|
||||
private void onRoomLeave(String roomName) {
|
||||
String joinMessage = "Chatter " + this.chatterName + " left the room !";
|
||||
this.server.sendToRoom(roomName, new ChatMessagePacket(Instant.now(), "", joinMessage));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(RoomListPacket packet) {
|
||||
// I'm never supposed to receive this from the client
|
||||
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(ChatMessagePacket packet) {
|
||||
// I'm never supposed to receive this from the client
|
||||
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(ServerResponsePacket packet) {
|
||||
// I'm never supposed to receive this from the client
|
||||
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(RequestActualRoomPacket packet) {
|
||||
sendPacket(new ActualRoomPacket(server.getRoomName(this)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(ActualRoomPacket packet) {
|
||||
// I'm never supposed to receive this from the client
|
||||
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
|
||||
}
|
||||
|
||||
}
|
||||
37
ChatApp/app/src/main/java/server/ServerGui.java
Normal file
37
ChatApp/app/src/main/java/server/ServerGui.java
Normal file
@@ -0,0 +1,37 @@
|
||||
package server;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.stage.Screen;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
||||
public class ServerGui extends Application {
|
||||
@Override
|
||||
public void start(Stage stage) throws IOException {
|
||||
FXMLLoader loader = new FXMLLoader(getClass().getResource("/server/serverVue.fxml"));
|
||||
Scene scene = new Scene(loader.load(), 400, 240);
|
||||
|
||||
double screenWidth = Screen.getPrimary().getVisualBounds().getWidth();
|
||||
double screenHeight = Screen.getPrimary().getVisualBounds().getHeight();
|
||||
|
||||
double xPos = screenWidth / 2 - scene.getWidth() / 2;
|
||||
double yPos = screenHeight / 2 - scene.getHeight() / 2;
|
||||
|
||||
scene.getStylesheets().add(getClass().getResource("serverStyle.css").toExternalForm());
|
||||
stage.getIcons().add(new Image(Objects.requireNonNull(getClass().getResourceAsStream("/liscord.png"))));
|
||||
stage.setTitle("Liscord Server");
|
||||
stage.setScene(scene);
|
||||
stage.setX(xPos);
|
||||
stage.setY(yPos);
|
||||
stage.show();
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
launch();
|
||||
}
|
||||
}
|
||||
42
ChatApp/app/src/main/java/server/ServerGuiController.java
Normal file
42
ChatApp/app/src/main/java/server/ServerGuiController.java
Normal file
@@ -0,0 +1,42 @@
|
||||
package server;
|
||||
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import network.IPAddressFinder;
|
||||
|
||||
import java.net.SocketException;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
public class ServerGuiController {
|
||||
@FXML
|
||||
private Label IPAddress;
|
||||
|
||||
@FXML
|
||||
private Button toggleServerButton;
|
||||
|
||||
private Server server;
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
IPAddress.setText("IP Address: " + IPAddressFinder.findIPAddress());
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void toggleServer() throws SocketException {
|
||||
if(server == null){
|
||||
toggleServerButton.setDisable(true);
|
||||
server = new Server(6665);
|
||||
toggleServerButton.setDisable(false);
|
||||
toggleServerButton.setText("Stop Server");
|
||||
System.out.println("Server started on port 6665");
|
||||
} else {
|
||||
toggleServerButton.setDisable(true);
|
||||
server.close();
|
||||
server = null;
|
||||
toggleServerButton.setDisable(false);
|
||||
toggleServerButton.setText("Start Server");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
24
ChatApp/app/src/main/java/utilities/ANSIColor.java
Normal file
24
ChatApp/app/src/main/java/utilities/ANSIColor.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package utilities;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class ANSIColor {
|
||||
public static final String RESET = "\u001B[0m";
|
||||
public static final String BLACK = "\u001B[30m";
|
||||
public static final String RED = "\u001B[31m";
|
||||
public static final String GREEN = "\u001B[32m";
|
||||
public static final String BLUE = "\u001B[34m";
|
||||
public static final String GREY = "\u001B[37m";
|
||||
|
||||
public static String formatString(String message){
|
||||
return message.replace("&r", RED)
|
||||
.replace("&g", GREEN)
|
||||
.replace("&b", BLUE)
|
||||
.replace("&y", GREY)
|
||||
.replace("&n", RESET);
|
||||
}
|
||||
|
||||
public static String tag(String message, String chatter){
|
||||
return message.replaceAll("(@" + Pattern.quote(chatter) + ")", "\u001B[44;30m$1\u001B[49;39m" );
|
||||
}
|
||||
}
|
||||
17
ChatApp/app/src/main/java/utilities/FxUtilities.java
Normal file
17
ChatApp/app/src/main/java/utilities/FxUtilities.java
Normal file
@@ -0,0 +1,17 @@
|
||||
package utilities;
|
||||
|
||||
import javafx.stage.Screen;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
public class FxUtilities {
|
||||
public static void centerStage(Stage stage) {
|
||||
double screenWidth = Screen.getPrimary().getVisualBounds().getWidth();
|
||||
double screenHeight = Screen.getPrimary().getVisualBounds().getHeight();
|
||||
|
||||
double xPos = screenWidth / 2 - stage.getWidth() / 2;
|
||||
double yPos = screenHeight / 2 - stage.getHeight() / 2;
|
||||
|
||||
stage.setX(xPos);
|
||||
stage.setY(yPos);
|
||||
}
|
||||
}
|
||||
38
ChatApp/app/src/main/java/utilities/Signal.java
Normal file
38
ChatApp/app/src/main/java/utilities/Signal.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package utilities;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class Signal {
|
||||
|
||||
private final Class<?>[] types;
|
||||
private final Set<Slot> listeners;
|
||||
|
||||
public Signal(Class<?>... types) {
|
||||
this.types = types;
|
||||
this.listeners = new HashSet<>();
|
||||
}
|
||||
|
||||
public void connect(Slot listener) {
|
||||
this.listeners.add(listener);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
this.listeners.clear();
|
||||
}
|
||||
|
||||
public void emit(Object... values) {
|
||||
for (Slot listener : this.listeners) {
|
||||
if (values.length != types.length) {
|
||||
throw new UnsupportedOperationException("The number of provided arguments is not right");
|
||||
}
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
if (!types[i].isInstance(values[i])) {
|
||||
throw new UnsupportedOperationException("Incorrect value at index " + i);
|
||||
}
|
||||
}
|
||||
listener.run(values);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
5
ChatApp/app/src/main/java/utilities/Slot.java
Normal file
5
ChatApp/app/src/main/java/utilities/Slot.java
Normal file
@@ -0,0 +1,5 @@
|
||||
package utilities;
|
||||
|
||||
public interface Slot {
|
||||
void run(Object ... args);
|
||||
}
|
||||
12
ChatApp/app/src/main/resources/client/clientLoading.fxml
Normal file
12
ChatApp/app/src/main/resources/client/clientLoading.fxml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.layout.StackPane?>
|
||||
<?import javafx.scene.shape.Arc?>
|
||||
|
||||
<?import javafx.scene.control.Label?>
|
||||
<StackPane xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"
|
||||
fx:controller="client.ClientLoading" style="-fx-background-color: lightgrey;">
|
||||
<Arc fx:id="spinnerArc" centerX="50" centerY="50" radiusX="30" radiusY="30"
|
||||
startAngle="30" length="330" fill="transparent" stroke="blue" strokeWidth="5" onMouseClicked="#login"
|
||||
/>
|
||||
</StackPane>
|
||||
23
ChatApp/app/src/main/resources/client/clientLogin.fxml
Normal file
23
ChatApp/app/src/main/resources/client/clientLogin.fxml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import java.lang.*?>
|
||||
<?import java.util.*?>
|
||||
<?import javafx.scene.*?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
|
||||
<VBox alignment="CENTER" spacing="10.0"
|
||||
xmlns="http://javafx.com/javafx"
|
||||
xmlns:fx="http://javafx.com/fxml"
|
||||
fx:controller="client.ClientLogin"
|
||||
prefHeight="400.0" prefWidth="600.0"
|
||||
styleClass="login-box">
|
||||
<padding>
|
||||
<Insets bottom="10.0" left="100.0" right="100.0" top="10.0"/>
|
||||
</padding>
|
||||
<Label text="Enter your username"/>
|
||||
<TextField fx:id="usernameField" onAction="#login" styleClass="login"/>
|
||||
<Button text="Login" onAction="#login"/>
|
||||
</VBox>
|
||||
17
ChatApp/app/src/main/resources/client/clientStyle.css
Normal file
17
ChatApp/app/src/main/resources/client/clientStyle.css
Normal file
@@ -0,0 +1,17 @@
|
||||
.login-box, .logo-box, .rooms-list, .chat-list {
|
||||
-fx-background-color: lightgray;
|
||||
}
|
||||
|
||||
.login {
|
||||
-fx-border-color: transparent;
|
||||
}
|
||||
|
||||
.login:focused {
|
||||
-fx-border-color: lightgrey;
|
||||
}
|
||||
|
||||
.rooms-label {
|
||||
-fx-font-size: 18px;
|
||||
-fx-padding: 10px;
|
||||
-fx-background-color: lightgray;
|
||||
}
|
||||
64
ChatApp/app/src/main/resources/client/clientVue.fxml
Normal file
64
ChatApp/app/src/main/resources/client/clientVue.fxml
Normal file
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import java.lang.*?>
|
||||
<?import java.util.*?>
|
||||
<?import javafx.scene.*?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.image.ImageView?>
|
||||
<?import javafx.scene.image.Image?>
|
||||
<BorderPane xmlns="http://javafx.com/javafx"
|
||||
xmlns:fx="http://javafx.com/fxml"
|
||||
fx:controller="client.ClientGuiController"
|
||||
prefHeight="400.0" prefWidth="600.0" fx:id="vueContainer">
|
||||
<top>
|
||||
<HBox alignment="CENTER" styleClass="logo-box">
|
||||
<padding>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
|
||||
</padding>
|
||||
<ImageView fitHeight="50.0" fitWidth="50.0">
|
||||
<Image url="@/liscord.png" />
|
||||
</ImageView>
|
||||
<Label text="Liscord" style="-fx-font-size: 24px; -fx-font-weight: bold; -fx-padding: 0 0 0 10px"/>
|
||||
</HBox>
|
||||
</top>
|
||||
<left>
|
||||
<ScrollPane prefWidth="200.0" fitToWidth="true">
|
||||
<BorderPane>
|
||||
<center>
|
||||
<VBox spacing="10.0" alignment="CENTER" VBox.vgrow="ALWAYS">
|
||||
<Label text="Rooms" styleClass="rooms-label"/>
|
||||
<VBox fx:id="roomList" styleClass="rooms-list" spacing="5.0" VBox.vgrow="ALWAYS" alignment="CENTER"/>
|
||||
</VBox>
|
||||
</center>
|
||||
<bottom>
|
||||
<Button text="Create room" onAction="#createRoom" />
|
||||
</bottom>
|
||||
</BorderPane>
|
||||
</ScrollPane>
|
||||
</left>
|
||||
<center>
|
||||
<BorderPane>
|
||||
<top>
|
||||
<HBox alignment="CENTER">
|
||||
<padding>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
|
||||
</padding>
|
||||
<Label fx:id="roomName" style="-fx-font-size: 18px; -fx-font-weight: bold;"/>
|
||||
</HBox>
|
||||
</top>
|
||||
<center>
|
||||
<ScrollPane fx:id="chatPane" fitToWidth="true">
|
||||
<VBox spacing="10.0">
|
||||
<VBox fx:id="chatList" styleClass="chat-list" spacing="5.0" VBox.vgrow="ALWAYS"/>
|
||||
</VBox>
|
||||
</ScrollPane>
|
||||
</center>
|
||||
<bottom>
|
||||
<HBox fx:id="chatInput" spacing="5.0" style="-fx-padding: 10;" />
|
||||
</bottom>
|
||||
</BorderPane>
|
||||
</center>
|
||||
</BorderPane>
|
||||
BIN
ChatApp/app/src/main/resources/liscord.png
Normal file
BIN
ChatApp/app/src/main/resources/liscord.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 248 KiB |
27
ChatApp/app/src/main/resources/server/serverStyle.css
Normal file
27
ChatApp/app/src/main/resources/server/serverStyle.css
Normal file
@@ -0,0 +1,27 @@
|
||||
.root{
|
||||
-fx-background-color: darkblue;
|
||||
-fx-text-fill: blue;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.address {
|
||||
-fx-text-fill: white;
|
||||
-fx-font-size: 25px;
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
-fx-background-color: darkblue;
|
||||
-fx-text-fill: white;
|
||||
-fx-font-size: 20px;
|
||||
-fx-padding: 10px;
|
||||
-fx-border-width: 2px;
|
||||
-fx-border-color: white;
|
||||
-fx-cursor: hand;
|
||||
-fx-border-radius: 5;
|
||||
}
|
||||
|
||||
.toggle-button:hover {
|
||||
-fx-background-color: white;
|
||||
-fx-text-fill: darkblue;
|
||||
transition: -fx-background-color 0.3s, -fx-text-fill 0.3s;
|
||||
}
|
||||
14
ChatApp/app/src/main/resources/server/serverVue.fxml
Normal file
14
ChatApp/app/src/main/resources/server/serverVue.fxml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.*?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
|
||||
<VBox alignment="CENTER" spacing="20.0" xmlns="http://javafx.com/javafx/11.0.14-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="server.ServerGuiController">
|
||||
<padding>
|
||||
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
|
||||
</padding>
|
||||
|
||||
<Label fx:id="IPAddress" styleClass="address" />
|
||||
<Button text="Start Server" onAction="#toggleServer" fx:id="toggleServerButton" styleClass="toggle-button"/>
|
||||
</VBox>
|
||||
6
ChatApp/gradle.properties
Normal file
6
ChatApp/gradle.properties
Normal file
@@ -0,0 +1,6 @@
|
||||
# This file was generated by the Gradle 'init' task.
|
||||
# https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties
|
||||
|
||||
org.gradle.configuration-cache=false
|
||||
org.gradle.console=plain
|
||||
org.gradle.logging.level=quiet
|
||||
10
ChatApp/gradle/libs.versions.toml
Normal file
10
ChatApp/gradle/libs.versions.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
# This file was generated by the Gradle 'init' task.
|
||||
# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format
|
||||
|
||||
[versions]
|
||||
guava = "33.3.1-jre"
|
||||
junit-jupiter = "5.11.3"
|
||||
|
||||
[libraries]
|
||||
guava = { module = "com.google.guava:guava", version.ref = "guava" }
|
||||
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" }
|
||||
BIN
ChatApp/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
ChatApp/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
ChatApp/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
ChatApp/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
251
ChatApp/gradlew
vendored
Executable file
251
ChatApp/gradlew
vendored
Executable file
@@ -0,0 +1,251 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
94
ChatApp/gradlew.bat
vendored
Normal file
94
ChatApp/gradlew.bat
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
14
ChatApp/settings.gradle
Normal file
14
ChatApp/settings.gradle
Normal file
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* This file was generated by the Gradle 'init' task.
|
||||
*
|
||||
* The settings file is used to specify which projects to include in your build.
|
||||
* For more detailed information on multi-project builds, please refer to https://docs.gradle.org/8.13/userguide/multi_project_builds.html in the Gradle documentation.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
// Apply the foojay-resolver plugin to allow automatic download of JDKs
|
||||
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.9.0'
|
||||
}
|
||||
|
||||
rootProject.name = 'ChatApp'
|
||||
include('app')
|
||||
@@ -1,23 +0,0 @@
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Scanner;
|
||||
|
||||
import client.Client;
|
||||
import server.Server;
|
||||
|
||||
public class ChatApp {
|
||||
public static void main(String[] args) throws Exception {
|
||||
Server server = new Server(6665);
|
||||
Client client = new Client(new InetSocketAddress("localhost", 6665));
|
||||
|
||||
client.SendCreateRoom("101");
|
||||
|
||||
Scanner scanner = new Scanner(System.in);
|
||||
while (true) {
|
||||
String message = scanner.nextLine();
|
||||
System.out.print("\033[1A");
|
||||
System.out.print("\r\033[2K");
|
||||
System.out.flush();
|
||||
client.visitMessage(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
package client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketException;
|
||||
import java.util.Objects;
|
||||
import java.util.Scanner;
|
||||
|
||||
import network.protocol.packets.*;
|
||||
|
||||
public class Client {
|
||||
|
||||
private final ClientConnexion connexion;
|
||||
|
||||
public static void main(String[] args) {
|
||||
String host = "localhost";
|
||||
int port = 6665;
|
||||
try {
|
||||
Client client = new Client(new InetSocketAddress(host, port));
|
||||
Scanner scanner = new Scanner(System.in);
|
||||
while(true) {
|
||||
String message = scanner.nextLine();
|
||||
client.visitMessage(message);
|
||||
}
|
||||
} catch (SocketException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public Client(InetSocketAddress serverAddress) throws SocketException {
|
||||
this.connexion = new ClientConnexion(new DatagramSocket(), serverAddress);
|
||||
Scanner scanner = new Scanner(System.in);
|
||||
System.out.println("Enter your pseudo:");
|
||||
String pseudo = scanner.nextLine();
|
||||
login(pseudo);
|
||||
}
|
||||
|
||||
public void visitMessage(String message){
|
||||
try {
|
||||
if(message.startsWith("/")){
|
||||
if(message.startsWith("/createRoom")) {
|
||||
String roomName = message.substring(12).trim();
|
||||
SendCreateRoom(roomName);
|
||||
} else if(message.startsWith("/listRooms")) {
|
||||
RequestRoomList();
|
||||
} else if(message.startsWith("/joinRoom")) {
|
||||
String roomName = message.substring(10).trim();
|
||||
SendJoinRoom(roomName);
|
||||
} else if(message.startsWith("/leaveRoom")) {
|
||||
SendLeaveRoom();
|
||||
} else if(message.startsWith("/help")) {
|
||||
System.out.println("Available commands:");
|
||||
System.out.println("\t/createRoom <roomName>");
|
||||
System.out.println("\t/listRooms");
|
||||
System.out.println("\t/joinRoom <roomName>");
|
||||
System.out.println("\t/leaveRoom");
|
||||
System.out.println("\t/help");
|
||||
}
|
||||
else {
|
||||
System.out.println("Unknown command");
|
||||
}
|
||||
} else {
|
||||
SendChatMessage(message);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void login(String pseudo) {
|
||||
try {
|
||||
this.connexion.sendPacket(new LoginPacket(pseudo));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void SendChatMessage(String message) {
|
||||
try {
|
||||
this.connexion.sendPacket(new SendChatMessagePacket(message));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void SendCreateRoom(String roomName) {
|
||||
try {
|
||||
this.connexion.sendPacket(new CreateRoomPacket(roomName));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void SendJoinRoom(String roomName) {
|
||||
try {
|
||||
this.connexion.sendPacket(new JoinRoomPacket(roomName));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void SendLeaveRoom() {
|
||||
try {
|
||||
this.connexion.sendPacket(new LeaveRoomPacket());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void RequestRoomList() {
|
||||
try {
|
||||
this.connexion.sendPacket(new RequestRoomListPacket());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
package client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
import network.PacketHandler;
|
||||
import network.SocketReader;
|
||||
import network.SocketWriter;
|
||||
import network.protocol.ANSIColor;
|
||||
import network.protocol.Packet;
|
||||
import network.protocol.PacketVisitor;
|
||||
import network.protocol.packets.ChatMessagePacket;
|
||||
import network.protocol.packets.CreateRoomPacket;
|
||||
import network.protocol.packets.JoinRoomPacket;
|
||||
import network.protocol.packets.LeaveRoomPacket;
|
||||
import network.protocol.packets.LoginPacket;
|
||||
import network.protocol.packets.RequestRoomListPacket;
|
||||
import network.protocol.packets.RoomListPacket;
|
||||
import network.protocol.packets.SendChatMessagePacket;
|
||||
import network.protocol.packets.ServerResponsePacket;
|
||||
|
||||
public class ClientConnexion implements PacketVisitor, PacketHandler{
|
||||
|
||||
private final InetSocketAddress serverAddress;
|
||||
private final SocketWriter writer;
|
||||
private final SocketReader reader;
|
||||
|
||||
public ClientConnexion(DatagramSocket socket, InetSocketAddress serverAddress) {
|
||||
this.serverAddress = serverAddress;
|
||||
this.writer = new SocketWriter(socket);
|
||||
this.reader = new SocketReader(socket, this);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
this.reader.stop();
|
||||
}
|
||||
|
||||
public void sendPacket(Packet packet) throws IOException {
|
||||
this.writer.sendPacket(packet, serverAddress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePacket(Packet packet, InetSocketAddress address) {
|
||||
// we assume that the packet comes from the server
|
||||
visit(packet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(ChatMessagePacket packet) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String time = packet.getTime().toString();
|
||||
sb.append("&y[");
|
||||
sb.append(time, 11, 19); // We only take the HH:MM:SS part
|
||||
sb.append("]&n");
|
||||
sb.append(" ");
|
||||
sb.append(packet.getChatter());
|
||||
sb.append(" : ");
|
||||
sb.append(packet.getContent()).append("&n"); // make the color back to normal at the end of every message
|
||||
System.out.println(ANSIColor.formatString(sb.toString()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(CreateRoomPacket packet) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(JoinRoomPacket packet) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(LeaveRoomPacket packet) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(LoginPacket packet) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(RequestRoomListPacket packet) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(RoomListPacket packet) {
|
||||
System.out.println("Rooms :");
|
||||
for (String room : packet.getRoomNames()) {
|
||||
System.out.println("\t" + room);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(SendChatMessagePacket packet) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(ServerResponsePacket packet) {
|
||||
if(packet.getResponse() == ServerResponsePacket.Response.MessageSent || packet.getResponse() == ServerResponsePacket.Response.MessageNotSent) {
|
||||
return;
|
||||
}
|
||||
System.out.println(packet.getResponse());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package network;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
import network.protocol.Packet;
|
||||
|
||||
public class SocketReader {
|
||||
|
||||
private final DatagramSocket socket;
|
||||
private final Thread readThread;
|
||||
private final PacketHandler handler;
|
||||
|
||||
public SocketReader(DatagramSocket socket, PacketHandler handler) {
|
||||
this.socket = socket;
|
||||
this.handler = handler;
|
||||
this.readThread = new Thread(this::readLoop);
|
||||
this.readThread.start();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
this.readThread.interrupt();
|
||||
}
|
||||
|
||||
private void readLoop() {
|
||||
while (!Thread.interrupted()) {
|
||||
try {
|
||||
byte[] buffer = new byte[65535];
|
||||
DatagramPacket dataPacket = new DatagramPacket(buffer, buffer.length);
|
||||
socket.receive(dataPacket);
|
||||
|
||||
InetSocketAddress address = new InetSocketAddress(dataPacket.getAddress(), dataPacket.getPort());
|
||||
|
||||
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(dataPacket.getData()));
|
||||
Packet packet = (Packet) ois.readObject();
|
||||
|
||||
this.handler.handlePacket(packet, address);
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package network;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
import network.protocol.Packet;
|
||||
|
||||
public class SocketWriter {
|
||||
|
||||
private final DatagramSocket socket;
|
||||
|
||||
public SocketWriter(DatagramSocket socket) {
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
public void sendPacket(Packet packet, InetSocketAddress address) throws IOException {
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
ObjectOutputStream oos = new ObjectOutputStream(stream);
|
||||
oos.writeObject(packet);
|
||||
oos.flush();
|
||||
byte[] data = stream.toByteArray();
|
||||
DatagramPacket dataPacket = new DatagramPacket(data, data.length, address.getAddress(),
|
||||
address.getPort());
|
||||
this.socket.send(dataPacket);
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
package server;
|
||||
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketException;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import network.PacketHandler;
|
||||
import network.SocketReader;
|
||||
import network.protocol.Packet;
|
||||
import network.protocol.packets.ChatMessagePacket;
|
||||
import network.protocol.packets.SendChatMessagePacket;
|
||||
|
||||
public class Server implements PacketHandler {
|
||||
|
||||
private final DatagramSocket serverSocket;
|
||||
private final Map<InetSocketAddress, ServerConnexion> connexions;
|
||||
private final SocketReader reader;
|
||||
private final Map<String, ArrayList<ServerConnexion>> roomNames;
|
||||
|
||||
public Server(int port) throws SocketException {
|
||||
this.serverSocket = new DatagramSocket(port);
|
||||
this.connexions = new HashMap<>();
|
||||
this.reader = new SocketReader(serverSocket, this);
|
||||
this.roomNames = new HashMap<>();
|
||||
}
|
||||
|
||||
public ArrayList<String> getRoomNames() {
|
||||
return roomNames.keySet().stream().collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
|
||||
}
|
||||
|
||||
public String getRoomName(ServerConnexion connexion) {
|
||||
for (Map.Entry<String, ArrayList<ServerConnexion>> entry : roomNames.entrySet()) {
|
||||
if(entry.getValue().contains(connexion)) {
|
||||
return entry.getKey();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void createRoom(String roomName, ServerConnexion connexion) throws SocketException {
|
||||
if(roomNames.containsKey(roomName)) {
|
||||
throw new SocketException("Room already exists");
|
||||
}
|
||||
roomNames.put(roomName, new ArrayList<>());
|
||||
roomNames.get(roomName).add(connexion);
|
||||
}
|
||||
|
||||
public void leaveRoom(ServerConnexion connexion) throws SocketException {
|
||||
String roomName = getRoomName(connexion);
|
||||
if(roomName != null) {
|
||||
roomNames.get(roomName).remove(connexion);
|
||||
// Remove the room if it is empty
|
||||
if(roomNames.get(roomName).isEmpty()) {
|
||||
roomNames.remove(roomName);
|
||||
}
|
||||
return;
|
||||
}
|
||||
throw new SocketException("Room does not exist");
|
||||
}
|
||||
|
||||
public void joinRoom(String roomName, ServerConnexion connexion) throws SocketException {
|
||||
if(roomNames.containsKey(roomName) && !roomNames.get(roomName).contains(connexion)) {
|
||||
roomNames.get(roomName).add(connexion);
|
||||
return;
|
||||
}
|
||||
throw new SocketException("Room does not exist");
|
||||
}
|
||||
|
||||
public void sendToRoom(ServerConnexion connexion, SendChatMessagePacket packet) throws SocketException {
|
||||
String roomName = getRoomName(connexion);
|
||||
ChatMessagePacket chatPacket = new ChatMessagePacket(Instant.now(), connexion.getChatterName(), packet.getContent());
|
||||
if(roomName != null && roomNames.containsKey(roomName)) {
|
||||
roomNames.get(roomName).forEach(con -> con.sendPacket(chatPacket));
|
||||
return;
|
||||
}
|
||||
throw new SocketException("You are not in a room or the room does not exist");
|
||||
}
|
||||
|
||||
public void close() {
|
||||
this.reader.stop();
|
||||
}
|
||||
|
||||
public boolean hasChatterName(String pseudo) {
|
||||
return this.connexions.values().stream()
|
||||
.anyMatch(connexion -> pseudo.equals(connexion.getChatterName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePacket(Packet packet, InetSocketAddress address) {
|
||||
if (!connexions.containsKey(address)) {
|
||||
this.connexions.put(address, new ServerConnexion(this, serverSocket, address));
|
||||
}
|
||||
this.connexions.get(address).visit(packet);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
package server;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketException;
|
||||
|
||||
import network.SocketWriter;
|
||||
import network.protocol.Packet;
|
||||
import network.protocol.PacketVisitor;
|
||||
import network.protocol.packets.ChatMessagePacket;
|
||||
import network.protocol.packets.CreateRoomPacket;
|
||||
import network.protocol.packets.JoinRoomPacket;
|
||||
import network.protocol.packets.LeaveRoomPacket;
|
||||
import network.protocol.packets.LoginPacket;
|
||||
import network.protocol.packets.RequestRoomListPacket;
|
||||
import network.protocol.packets.RoomListPacket;
|
||||
import network.protocol.packets.SendChatMessagePacket;
|
||||
import network.protocol.packets.ServerResponsePacket;
|
||||
import network.protocol.packets.ServerResponsePacket.Response;
|
||||
|
||||
public class ServerConnexion implements PacketVisitor {
|
||||
|
||||
private final Server server;
|
||||
private final InetSocketAddress clientAddress;
|
||||
private final SocketWriter writer;
|
||||
private String chatterName;
|
||||
|
||||
public ServerConnexion(Server server, DatagramSocket socket, InetSocketAddress clientAddress) {
|
||||
this.clientAddress = clientAddress;
|
||||
this.server = server;
|
||||
this.writer = new SocketWriter(socket);
|
||||
}
|
||||
|
||||
public String getChatterName() {
|
||||
return this.chatterName;
|
||||
}
|
||||
|
||||
public void sendPacket(Packet packet) {
|
||||
try {
|
||||
this.writer.sendPacket(packet, clientAddress);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(ChatMessagePacket packet) {
|
||||
// I'm never supposed to receive this from the client
|
||||
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(CreateRoomPacket packet) {
|
||||
try {
|
||||
server.createRoom(packet.getRoomName(), this);
|
||||
sendPacket(new ServerResponsePacket(Response.RoomCreated));
|
||||
} catch (SocketException e) {
|
||||
sendPacket(new ServerResponsePacket(Response.RoomNotCreated));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(JoinRoomPacket packet) {
|
||||
try {
|
||||
server.joinRoom(packet.getRoomName(), this);
|
||||
sendPacket(new ServerResponsePacket(Response.RoomJoined));
|
||||
} catch (SocketException e) {
|
||||
sendPacket(new ServerResponsePacket(Response.RoomNotJoined));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(LeaveRoomPacket packet) {
|
||||
try {
|
||||
server.leaveRoom(this);
|
||||
sendPacket(new ServerResponsePacket(Response.RoomLeft));
|
||||
} catch (SocketException e) {
|
||||
sendPacket(new ServerResponsePacket(Response.RoomNotLeft));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(LoginPacket packet) {
|
||||
if (server.hasChatterName(packet.getPseudo())) {
|
||||
sendPacket(new ServerResponsePacket(Response.AuthError));
|
||||
return;
|
||||
}
|
||||
|
||||
this.chatterName = packet.getPseudo();
|
||||
sendPacket(new RoomListPacket(server.getRoomNames()));
|
||||
System.out.println("[Server] Chatter " + packet.getPseudo() + " connected !");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(RequestRoomListPacket packet) {
|
||||
sendPacket(new RoomListPacket(server.getRoomNames()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(RoomListPacket packet) {
|
||||
// I'm never supposed to receive this from the client
|
||||
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(SendChatMessagePacket packet) {
|
||||
try {
|
||||
server.sendToRoom(this, packet);
|
||||
} catch (SocketException e) {
|
||||
sendPacket(new ServerResponsePacket(Response.MessageNotSent));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPacket(ServerResponsePacket packet) {
|
||||
// I'm never supposed to receive this from the client
|
||||
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
|
||||
}
|
||||
|
||||
}
|
||||
2
Sudoku
2
Sudoku
Submodule Sudoku updated: af0ac0ff77...05df8a56a7
Reference in New Issue
Block a user