feat: client/server working for adding lines, can undo locally, added a colorpicker and drawing in color is now a thing

This commit is contained in:
Emi Boucly 2025-03-24 16:43:16 +01:00
parent a8b2a0ced6
commit de645a8b1e
11 changed files with 939 additions and 40 deletions

View file

@ -0,0 +1,175 @@
package fr.emiko.net;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
/**
* Client TCP : envoie des chaines de caractères à un serveur et lit les chaines en retour.
*/
public class ClientTCP {
/** Couleur rouge */
public static final String RED = "\u001b[31m";
/** Couleur bleue */
public static final String BLUE = "\u001b[34m";
/** Couleur standard */
public static final String RST = "\u001b[0m";
/** Fin de message */
public static final String END_MESSAGE = "fin";
/**
* Socket connecté au serveur
*/
protected Socket sock;
/**
* Flux de caractères UTF-8 en sortie
*/
protected PrintStream out;
/**
* Flux de caractères UTF-8 en entrée
*/
protected BufferedReader in;
/**
* Chaine de caractères "ip:port" du client
*/
protected String ipPort;
/**
* Le client est-il connecté ?
*/
protected boolean connected;
/**
* Programme principal [Déprécié]
* @param args Arguments
* @throws Exception Si la connexion échoue
*/
public static void main(String[] args) throws Exception {
ClientTCP client = new ClientTCP("localhost", 2024);
Thread envoi = new Thread(client::sendLoop);
Thread reception = new Thread(client::receiveLoop);
envoi.start();
reception.start();
envoi.join();
client.close();
}
/**
* Le constructeur ouvre la connexion TCP au serveur <code>host:port</code>
* et récupère les flux de caractères en entrée {@link #in} et sortie {@link #out}
*import static rtgre.chat.ChatApplication.LOGGER;
* @param host IP ou nom de domaine du serveur
* @param port port d'écoute du serveur
* @throws IOException si la connexion échoue ou si les flux ne sont pas récupérables
*/
public ClientTCP(String host, int port) throws IOException {
sock = new Socket(host, port);
ipPort = "%s:%d".formatted(sock.getLocalAddress().getHostAddress(), sock.getLocalPort());
this.connected = true;
OutputStream os = sock.getOutputStream();
InputStream is = sock.getInputStream();
out = new PrintStream(os, true, StandardCharsets.UTF_8);
in = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8), 2048);
Thread rcLoop = new Thread(this::receiveLoop);
rcLoop.setDaemon(true);
rcLoop.start();
}
/**
* Envoie une chaine de caractères
*
* @param message chaine de caractères à transmettre
* @throws IOException lorsqu'une erreur sur le flux de sortie est détectée
*/
public void send(String message) throws IOException {
out.println(message);
if (out.checkError()) {
throw new IOException("Output stream error");
}
}
/**
* Getter de connected
* @return L'état de connected
*/
public boolean isConnected() {
return connected;
}
/**
* Setter de connected
* @param connected L'utilisateur est-il connecté ?
*/
public void setConnected(boolean connected) {
this.connected = connected;
}
/**
* Attente d'une chaine de caractères en entrée.
*
* @return chaine de caractères reçue
* @throws IOException lorsque la fin du flux est atteinte
*/
public String receive() throws IOException {
String message = in.readLine();
if (message == null) {
throw new IOException("End of the stream has been reached");
}
return message;
}
/**
* Fermeture de la connexion TCP
*/
public void close() {
try {
sock.close();
this.connected = false;
} catch (IOException e) {
}
}
/**
* Boucle d'envoi de messages
*/
public void sendLoop() {
BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in));
connected = true;
try {
while (connected) {
String message = stdIn.readLine();
if (message == null) { // fin du flux stdIn
message = END_MESSAGE;
}
this.send(message);
if (END_MESSAGE.equals(message)) {
connected = false;
}
}
} catch (IOException e) {
connected = false;
}
}
/**
* Boucle de réception de messages
*/
public void receiveLoop() {
connected = true;
try {
while (connected) {
String message = this.receive();
System.out.println("Message received: " + message);
}
} catch (IOException e) {
connected = false;
}
}
}

View file

@ -0,0 +1,68 @@
package fr.emiko.net;
import fr.emiko.graphicalapp.HelloController;
import javafx.application.Platform;
import org.json.JSONObject;
import java.io.IOException;
public class DrawClient extends ClientTCP{
private final HelloController listener;
public DrawClient(String host, int port, HelloController listener) throws IOException {
super(host, port);
this.listener = listener;
}
/**
* Envoi d'un évènement, sérialisé dans sa représentation JSON, au serveur.
* @param event L'évènement à envoyer
*/
public void sendEvent(Event event) {
connected = true;
try {
String message = event.toJSON();
if (message == null) { // fin du flux stdIn
message = END_MESSAGE;
}
this.send(message);
if (END_MESSAGE.equals(message)) {
connected = false;
}
} catch (IOException e) {
connected = false;
}
}
/**
* Boucle de réception des messages : chaque message est un évènement sérialisé en JSON, qui est transféré à ChatController.handleEvent(rtgre.modeles.Event) pour traitement.
* Si le message n'est pas conforme (format JSON), la connection est stoppée.
*/
@Override
public void receiveLoop() {
try {
while (connected) {
String message = this.receive();
if (listener != null) {
Platform.runLater(() -> listener.handleEvent(Event.fromJSON(message)));
}
}
} catch (IOException e) {
connected = false;
} finally {
close();
}
}
public void sendAuthEvent(String login) {
try {
this.send(new Event(Event.AUTH, new JSONObject().put("username", login)).toJSON());
} catch (IOException e) {
e.printStackTrace();
}
}
}

View file

@ -0,0 +1,234 @@
package fr.emiko.net;
import java.awt.*;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Vector;
import fr.emiko.graphicsElement.Line;
import org.json.JSONException;
import org.json.JSONObject;
public class DrawServer {
private ServerSocket passiveSocket;
private Vector<DrawClientHandler> clientList = new Vector<DrawClientHandler>();
private Vector<Line> lines;
public DrawServer(int port) throws IOException {
passiveSocket = new ServerSocket(port);
}
public static void main(String[] args) throws IOException {
DrawServer server = new DrawServer(8090);
server.acceptClients();
}
public void acceptClients() {
while (true) {
try {
Socket sock = passiveSocket.accept();
handleNewClient(sock);
} catch (IOException e) {
System.out.println(e);
}
}
}
public void removeClient(DrawClientHandler client) {
clientList.remove(client);
}
private void handleNewClient(Socket sock) throws IOException {
DrawClientHandler client = new DrawClientHandler(sock);
clientList.add(client);
Thread clientLoop = new Thread(client::eventReceiveLoop);
clientLoop.start();
}
/**
* Ferme la connexion du serveur, en fermant la connexion auprès de tous ses clients, puis en fermant son socket en écoute passive.
* @throws IOException si la connexion
*/
public void close() throws IOException {
for (DrawClientHandler client : clientList) {
client.close();
}
passiveSocket.close();
}
private class DrawClientHandler {
/** Message de fin d'une connexion */
public static final String END_MESSAGE = "fin";
/**
* Socket connecté au client
*/
private Socket sock;
/**
* Flux de caractères en sortie
*/
private PrintStream out;
/**
* Flux de caractères en entrée
*/
private BufferedReader in;
/**
* Chaine de caractères "ip:port" du client
*/
private String ipPort;
private User user;
/**
* Initialise les attributs {@link #sock} (socket connecté au client),
* {@link #out} (flux de caractères UTF-8 en sortie) et
* {@link #in} (flux de caractères UTF-8 en entrée).
*
* @param sock socket connecté au client
* @throws IOException si la connexion ne peut être établie ou si les flux ne peuvent être récupérés
*/
public DrawClientHandler(Socket sock) throws IOException {
this.sock = sock;
this.ipPort = "%s:%d".formatted(sock.getInetAddress().getHostAddress(), sock.getPort());
OutputStream os = sock.getOutputStream();
InputStream is = sock.getInputStream();
this.out = new PrintStream(os, true, StandardCharsets.UTF_8);
this.in = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
}
/**
* Boucle de réception d'évènement : réceptionne les messages reçus et les délèguent à `handleEvent(java.lang.String)` pour les interpréter
*/
public void eventReceiveLoop() {
try {
String message = null;
while (!END_MESSAGE.equals(message)) {
message = in.readLine();
if (message == null) {
break;
}
System.out.println("Réception de message : " + message);
try {
if (!handleEvent(message)) {
break;
}
} catch (Exception e) {
System.out.println(e.getMessage());
break;
}
}
} catch (IOException e) {
System.out.println(e.getMessage());
}
close();
}
/**
* Traitement d'un évènement. Ventile vers les méthodes traitant chaque type d'évènement.
* @param message objet évènement sous la forme d'une chaine JSON brute de réception
* @return `false` si l'évènement est de type Event.QUIT , `true` pour tous les autres types.
* @throws JSONException si l'objet JSON n'est pas conforme
* @throws IllegalStateException si l'authentification n'est pas effectuée
*/
private boolean handleEvent(String message) throws JSONException, IllegalStateException {
Event event = Event.fromJSON(message);
switch (event.getType()) {
case Event.AUTH -> {
doLogin(event.getContent());
return true;
}
case Event.ADDLINE -> {
doAddLine(event.getContent());
return true;
}
case Event.DELLINE -> {
doDelLine(event.getContent());
return true;
}
case Event.LSTLINE -> {
doSendLines();
return true;
}
default -> {
return false;
}
}
}
private void doDelLine(JSONObject content) {
Line line = Line.fromJSONArray(content.getJSONArray("line"));
this.user.getLines().remove(line);
sendAllOtherUsers(new Event("DELLINE", line.toJSONObject()));
}
private void doAddLine(JSONObject content) {
try {
System.out.println(Line.fromJSONArray(content.getJSONArray("line")));
} catch (Exception e) {
System.out.println(e);
e.printStackTrace();
}
Line line = Line.fromJSONArray(content.getJSONArray("line"));
this.user.getLines().add(line);
sendAllOtherUsers(new Event("LINE", line.toJSONObject()));
}
private void sendAllOtherUsers(Event event) {
for (DrawClientHandler client : clientList) {
if (client.user != this.user) {
System.out.println(client.user.getUsername());
sendEvent(client, event);
}
}
}
private void sendEvent(DrawClientHandler client, Event event) {
String jsonEvent = event.toJSON();
client.out.println(jsonEvent);
}
private void doSendLines() {
Vector<Line> lines = new Vector<>();
for (DrawClientHandler client: clientList) {
for (Line line: client.user.getLines()) {
lines.add(line);
}
}
for (Line line: lines) {
out.println(new Event("LINELST", line.toJSONObject()));
}
}
private void doLogin(JSONObject content) {
this.user = new User(content.getString("username"));
}
public void close() {
try {
sock.close();
removeClient(this);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}

View file

@ -0,0 +1,50 @@
package fr.emiko.net;
import org.json.JSONObject;
public class Event {
public static final String AUTH = "AUTH";
public static final String LSTLINE = "LSTLINE";
public static final String ADDLINE = "ADDLINE";
public static final String DELLINE = "DELLINE";
public static final String LINE = "LINE";
public static final String LINELST = "LINELST";
private String type;
private JSONObject content;
public Event(String type, JSONObject content) {
this.type = type;
this.content = content;
}
public String getType() {
return type;
}
public JSONObject getContent() {
return content;
}
public JSONObject toJSONObject() {
return new JSONObject()
.put("type", type)
.put("content", content);
}
public String toJSON() {
return toJSONObject().toString();
}
public static Event fromJSON(String obj) {
JSONObject jobj = new JSONObject(obj);
String type = jobj.getString("type");
JSONObject content = jobj.getJSONObject("content");
return new Event(type, content);
}
@Override
public String toString() {
return this.toJSON();
}
}

View file

@ -0,0 +1,32 @@
package fr.emiko.net;
import fr.emiko.graphicsElement.Line;
import java.util.Vector;
public class User {
private String username;
private String hashedPassword = "";
private Vector<Line> lines = new Vector<Line>();
private boolean connected;
public User(String username) {
this.username = username;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Vector<Line> getLines() {
return lines;
}
public void setLines(Vector<Line> lines) {
this.lines = lines;
}
}