Compare commits
49 Commits
handshake
...
dfdaae163b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
7
ChatApp/.gitignore
vendored
7
ChatApp/.gitignore
vendored
@@ -1,3 +1,8 @@
|
|||||||
.vscode
|
.vscode
|
||||||
bin
|
bin
|
||||||
lib
|
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
|
## How to run
|
||||||
|
|
||||||
> [!WARNING]
|
### Console
|
||||||
> As of now, the app rely on ANSI escape character, thus not available on Windows.
|
|
||||||
> However, this feature is under development.
|
|
||||||
|
|
||||||
You can run the app by launching the `ChatApp` class. This will create the server (port `6665`) and a first client,
|
#### Server + Client
|
||||||
(creating by the way a room (`101`)).
|
|
||||||
|
|
||||||
You can then create more clients by launching the `Client` class. It will connect to the server
|
You can create a server and an "admin" client by using:
|
||||||
(be sure you launched it first) on the default port `6665`.
|
|
||||||
|
```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
|
## 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
|
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
|
- /listRooms
|
||||||
- /joinRoom *roomName*
|
- /joinRoom *roomName*
|
||||||
- /leaveRoom
|
- /leaveRoom
|
||||||
- /help
|
- /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();
|
||||||
|
}
|
||||||
|
}
|
||||||
253
ChatApp/app/src/main/java/client/ClientGuiController.java
Normal file
253
ChatApp/app/src/main/java/client/ClientGuiController.java
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
package client;
|
||||||
|
|
||||||
|
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 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;
|
||||||
|
|
||||||
|
public void setClient(Client client) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void initialize() throws SocketException {
|
||||||
|
client = new Client(new InetSocketAddress("localhost", 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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@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(chatter, 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);
|
||||||
|
|
||||||
|
StringBuffer result = new StringBuffer();
|
||||||
|
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(() -> {
|
||||||
|
chatList.getChildren().clear();
|
||||||
|
chatInput.getChildren().clear();
|
||||||
|
Button leaveButton = new Button("Leave room");
|
||||||
|
leaveButton.setOnAction(event -> {
|
||||||
|
client.SendLeaveRoom();
|
||||||
|
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 chatter the name of the person who sent the message
|
||||||
|
* @param content the content of the message
|
||||||
|
* @return the formatted message
|
||||||
|
*/
|
||||||
|
private TextFlow formatMessage(String chatter, 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) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
340
ChatApp/app/src/main/java/network/PacketPool.java
Normal file
340
ChatApp/app/src/main/java/network/PacketPool.java
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
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 = SEND_DELAY * 2;
|
||||||
|
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)
|
||||||
|
this.socket.sendPacket(packet, address);
|
||||||
|
} 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;
|
package network.protocol;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class ANSIColor {
|
public class ANSIColor {
|
||||||
public static final String RESET = "\u001B[0m";
|
public static final String RESET = "\u001B[0m";
|
||||||
public static final String BLACK = "\u001B[30m";
|
public static final String BLACK = "\u001B[30m";
|
||||||
@@ -15,4 +17,8 @@ public class ANSIColor {
|
|||||||
.replace("&y", GREY)
|
.replace("&y", GREY)
|
||||||
.replace("&n", RESET);
|
.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 {
|
public abstract class Packet implements Serializable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for double dispatch
|
||||||
|
* @param packetVisitor the visitor to dispatch to
|
||||||
|
*/
|
||||||
public abstract void accept(PacketVisitor packetVisitor);
|
public abstract void accept(PacketVisitor packetVisitor);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,23 +1,20 @@
|
|||||||
package network.protocol;
|
package network.protocol;
|
||||||
|
|
||||||
import network.protocol.packets.ChatMessagePacket;
|
import network.protocol.packets.*;
|
||||||
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 interface PacketVisitor {
|
public interface PacketVisitor {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use double dispatch to process a packet
|
||||||
|
* @param packet the packet to process
|
||||||
|
*/
|
||||||
default void visit(Packet packet) {
|
default void visit(Packet packet) {
|
||||||
packet.accept(this);
|
packet.accept(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
void visitPacket(ChatMessagePacket packet);
|
void visitPacket(ChatMessagePacket packet);
|
||||||
void visitPacket(CreateRoomPacket packet);
|
void visitPacket(CreateRoomPacket packet);
|
||||||
|
void visitPacket(DisconnectPacket packet);
|
||||||
void visitPacket(JoinRoomPacket packet);
|
void visitPacket(JoinRoomPacket packet);
|
||||||
void visitPacket(LeaveRoomPacket packet);
|
void visitPacket(LeaveRoomPacket packet);
|
||||||
void visitPacket(LoginPacket packet);
|
void visitPacket(LoginPacket packet);
|
||||||
@@ -25,5 +22,7 @@ public interface PacketVisitor {
|
|||||||
void visitPacket(RoomListPacket packet);
|
void visitPacket(RoomListPacket packet);
|
||||||
void visitPacket(SendChatMessagePacket packet);
|
void visitPacket(SendChatMessagePacket packet);
|
||||||
void visitPacket(ServerResponsePacket 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;
|
package network.protocol.packets;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.List;
|
||||||
|
|
||||||
import network.protocol.Packet;
|
import network.protocol.Packet;
|
||||||
import network.protocol.PacketVisitor;
|
import network.protocol.PacketVisitor;
|
||||||
|
|
||||||
public class RoomListPacket extends Packet {
|
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;
|
this.roomNames = roomNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ArrayList<String> getRoomNames() {
|
public List<String> getRoomNames() {
|
||||||
return roomNames;
|
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;
|
||||||
|
}
|
||||||
49
ChatApp/app/src/main/resources/client/clientVue.fxml
Normal file
49
ChatApp/app/src/main/resources/client/clientVue.fxml
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?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" styleClass="vue-container" 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" styleClass="rooms-pane" fx:id="roomsPane" fitToWidth="true">
|
||||||
|
<VBox fx:id="rooms" 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>
|
||||||
|
</ScrollPane>
|
||||||
|
</left>
|
||||||
|
<center>
|
||||||
|
<BorderPane>
|
||||||
|
<center>
|
||||||
|
<ScrollPane styleClass="chat-pane" fx:id="chatPane" fitToWidth="true">
|
||||||
|
<VBox fx:id="chat" spacing="10.0">
|
||||||
|
<VBox fx:id="chatList" styleClass="chat-list" spacing="5.0" VBox.vgrow="ALWAYS"/>
|
||||||
|
</VBox>
|
||||||
|
</ScrollPane>
|
||||||
|
</center>
|
||||||
|
<bottom>
|
||||||
|
<HBox fx:id="chatInput" styleClass="chat-input" 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