diff --git a/chat/src/main/java/rtgre/chat/net/ClientTCP.java b/chat/src/main/java/rtgre/chat/net/ClientTCP.java
new file mode 100644
index 0000000..7cc9ed4
--- /dev/null
+++ b/chat/src/main/java/rtgre/chat/net/ClientTCP.java
@@ -0,0 +1,186 @@
+package rtgre.chat.net;
+
+import java.io.*;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.logging.Level;
+import java.util.logging.LogManager;
+import static rtgre.chat.ChatApplication.LOGGER;
+
+/**
+ * Client TCP: envoie des chaines de caractères à un serveur et lit les chaines en retour.
+ *
+ * Serveur netcat à lancer en face : nc -k -l -p 2024 -v
+ */
+public class ClientTCP {
+
+ public static final String RED = "\u001b[31m";
+ public static final String BLUE = "\u001b[34m";
+ public static final String RST = "\u001b[0m";
+ public static final String END_MESSAGE = "fin";
+
+ static {
+ try {
+ InputStream is = ClientTCP.class.getClassLoader()
+ .getResource("logging.properties").openStream();
+ LogManager.getLogManager().readConfiguration(is);
+ } catch (Exception e) {
+ LOGGER.log(Level.INFO, "Cannot read configuration file", e);
+ }
+ }
+
+ public static void main(String[] args) throws Exception {
+ /*
+ ClientTCP client = new ClientTCP("localhost", 2024);
+
+ // Essai simple d'émission / réception d'une chaine de caractères
+ String message = "Hello World!";
+ System.out.println(BLUE + "Envoi :" + message + RST);
+ client.send(message);
+ message = client.receive();
+ System.out.println(RED + "Réception: " + message + RST);
+
+ client.close();
+ */
+
+ 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();
+ }
+
+ /**
+ * 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;
+
+ protected boolean connected;
+
+ /**
+ * Le constructeur ouvre la connexion TCP au serveur host:port
+ * 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
+ */
+ public ClientTCP(String host, int port) throws IOException {
+ System.out.printf("Connexion à [%s:%d]%n", host, port);
+ sock = new Socket(host, port);
+ ipPort = "%s:%d".formatted(sock.getLocalAddress().getHostAddress(), sock.getLocalPort());
+ LOGGER.info("[%s] Connexion établie vers [%s:%d]".formatted(ipPort, host, port));
+ this.connected = true;
+ LOGGER.fine("[%s] Recuperation des flux d'octets en entree et sortie".formatted(ipPort));
+ OutputStream os = sock.getOutputStream();
+ InputStream is = sock.getInputStream();
+ LOGGER.fine("[%s] Conversion flux d'octets en flux de caractères UTF-8".formatted(ipPort));
+ out = new PrintStream(os, true, StandardCharsets.UTF_8);
+ in = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8), 2048);
+ }
+
+ /**
+ * 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 {
+ LOGGER.finest("send: %s".formatted(message));
+ out.println(message);
+ if (out.checkError()) {
+ throw new IOException("Output stream error");
+ }
+ }
+
+ public boolean isConnected() {
+ return connected;
+ }
+
+ 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();
+ LOGGER.finest("receive: %s".formatted(message));
+ if (message == null) {
+ throw new IOException("End of the stream has been reached");
+ }
+ return message;
+ }
+
+ /**
+ * Fermeture de la connexion TCP
+ */
+ public void close() {
+ LOGGER.info("[%s] Fermeture de la connexion".formatted(ipPort));
+ try {
+ sock.close();
+ this.connected = false;
+ } catch (IOException e) {
+ LOGGER.finest("[%s] %s".formatted(ipPort, e));
+ }
+ }
+
+
+ public void sendLoop() {
+ System.out.println(BLUE + "Boucle d'envoi de messages..." + RST);
+ BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in));
+ connected = true;
+ try {
+ while (connected) {
+ System.out.println(BLUE + "Votre message (\"fin\" pour terminer) : " + RST);
+ String message = stdIn.readLine();
+ if (message == null) { // fin du flux stdIn
+ message = END_MESSAGE;
+ }
+ System.out.println(BLUE + "Envoi: " + message + RST);
+ this.send(message);
+ if (END_MESSAGE.equals(message)) {
+ connected = false;
+ }
+ }
+ } catch (IOException e) {
+ LOGGER.severe(e.toString());
+ connected = false;
+ }
+ }
+
+ public void receiveLoop() {
+ System.out.println(RED + "Boucle de réception de messages..." + RST);
+ connected = true;
+ try {
+ while (connected) {
+ String message = this.receive();
+ System.out.println(RED + "Réception: " + message + RST);
+ }
+ } catch (IOException e) {
+ LOGGER.severe("[%s] %s".formatted(ipPort, e));
+ connected = false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/chat/src/main/java/rtgre/server/ChatServer.java b/chat/src/main/java/rtgre/server/ChatServer.java
new file mode 100644
index 0000000..3665663
--- /dev/null
+++ b/chat/src/main/java/rtgre/server/ChatServer.java
@@ -0,0 +1,210 @@
+package rtgre.server;
+
+import java.io.*;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.Vector;
+import java.util.logging.Level;
+import java.util.logging.LogManager;
+import java.util.logging.Logger;
+
+/**
+ * Programme serveur qui renvoie les chaines de caractères lues jusqu'à recevoir le message "fin"
+ */
+public class ChatServer {
+
+ private static final Logger LOGGER = Logger.getLogger(ChatServer.class.getCanonicalName());
+
+ private Vector clientList;
+
+ static {
+ try {
+ InputStream is = ChatServer.class.getClassLoader()
+ .getResource("logging.properties").openStream();
+ LogManager.getLogManager().readConfiguration(is);
+ } catch (Exception e) {
+ LOGGER.log(Level.INFO, "Cannot read configuration file", e);
+ }
+ }
+
+
+ public static void main(String[] args) throws IOException {
+ ChatServer server = new ChatServer(2024);
+ server.acceptClients();
+ }
+
+ /**
+ * Socket passif en écoute
+ */
+ private ServerSocket passiveSock;
+
+ public ChatServer(int port) throws IOException {
+ passiveSock = new ServerSocket(port);
+ LOGGER.info("Serveur en écoute " + passiveSock);
+ clientList = new Vector<>();
+ }
+
+ public void close() throws IOException {
+ for (ChatClientHandler client : clientList) {
+ client.close();
+ }
+ passiveSock.close();
+ }
+
+ /**
+ * Boucle d'attente des clients
+ */
+ public void acceptClients() {
+ int clientCounter = 1;
+ while (true) {
+ try {
+ LOGGER.info("Attente du client n°%02d".formatted(clientCounter));
+ Socket sock = passiveSock.accept();
+ LOGGER.info("[%s:%d] Connexion établie (client n°%02d)"
+ .formatted(sock.getInetAddress().getHostAddress(), sock.getPort(), clientCounter));
+ handleNewClient(sock);
+ } catch (IOException e) {
+ LOGGER.severe(e.toString());
+ }
+ clientCounter++;
+ }
+ }
+
+ public void removeClient(ChatClientHandler client) {
+ clientList.remove(client);
+ LOGGER.fine("Client [%s] retiré de la liste (%d clients connectés)"
+ .formatted(client.getIpPort(), clientList.size()));
+ }
+
+ public Vector getClientList() {
+ return clientList;
+ }
+
+ public ServerSocket getPassiveSocket() {
+ return passiveSock;
+ }
+
+ /**
+ * Prise en charge d'un nouveau client
+ *
+ * @param sock socket connecté au client
+ * @throws IOException
+ */
+ private void handleNewClient(Socket sock) throws IOException {
+ ChatClientHandler client = new ChatClientHandler(sock);
+ Thread clientLoop = new Thread(client::echoLoop);
+ clientLoop.start();
+ clientList.add(client);
+ LOGGER.fine("Ajout du client [%s] dans la liste (%d clients connectés)"
+ .formatted(client.getIpPort(), clientList.size()));
+ //client.echoLoop();
+ }
+
+ private class ChatClientHandler {
+ 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;
+
+ /**
+ * 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
+ */
+ public ChatClientHandler(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 écho : renvoie tous les messages reçus.
+ */
+ public void echoLoop() {
+ try {
+ String message = null;
+ while (!END_MESSAGE.equals(message)) {
+ message = in.readLine();
+ if (message == null) {
+ break;
+ }
+ LOGGER.info("[%s] Réception de : %s".formatted(ipPort, message));
+ LOGGER.info("[%s] Envoi de : %s".formatted(ipPort, message));
+ //out.println(message);
+ sendAllOtherClients(this, message);
+
+ }
+ } catch (IOException e) {
+ LOGGER.severe("[%s] %s".formatted(ipPort, e));
+ }
+ close();
+ }
+
+ public void send(String message) throws IOException {
+ LOGGER.finest("send: %s".formatted(message));
+ out.println(message);
+ if (out.checkError()) {
+ throw new IOException("Output stream error");
+ }
+ }
+
+ public String getIpPort() {
+ return ipPort;
+ }
+
+ public void sendAllOtherClients(ChatClientHandler fromClient, String message) {
+ for (ChatClientHandler client : clientList) {
+ if (!client.equals(this)) {
+ LOGGER.fine(clientList.toString());
+ LOGGER.fine("Envoi vers [%s] : %s".formatted(client.getIpPort(), message));
+ try {
+ client.send("[%s] %s".formatted(fromClient.getIpPort(), message));
+ } catch (Exception e) {
+ LOGGER.severe("[%s] %s".formatted(client.getIpPort(), e));
+ client.close();
+ }
+ }
+ }
+ }
+
+ public String receive() throws IOException {
+ String message = in.readLine();
+ LOGGER.finest("receive: %s".formatted(message));
+ if (message == null) {
+ throw new IOException("End of the stream has been reached");
+ }
+ return message;
+ }
+
+ public void close() {
+ LOGGER.info("[%s] Fermeture de la connexion".formatted(ipPort));
+ try {
+ sock.close();
+ removeClient(this);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/chat/src/test/java/rtgre/chat/net/ClientTCPTest.java b/chat/src/test/java/rtgre/chat/net/ClientTCPTest.java
new file mode 100644
index 0000000..0b33469
--- /dev/null
+++ b/chat/src/test/java/rtgre/chat/net/ClientTCPTest.java
@@ -0,0 +1,126 @@
+package rtgre.chat.net;
+
+import org.junit.jupiter.api.*;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import rtgre.chat.net.ClientTCP;
+
+
+import java.io.IOException;
+import java.lang.reflect.*;
+import java.net.ServerSocket;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+
+class ClientTCPTest {
+
+ static Class classe = ClientTCP.class;
+ static String module = "rtgre.chat.net";
+
+
+ @DisplayName("01-Structure de ClientTCP")
+ @Nested
+ class StructureTest {
+ static Method[] methodes;
+ static List methodesSignatures;
+ static List constructeursSignatures;
+
+ @BeforeAll
+ static void init() {
+ // Les méthodes
+ methodes = classe.getDeclaredMethods();
+ methodesSignatures = Arrays.asList(methodes).stream().map(e -> e.toString()).collect(Collectors.toList());
+ Constructor>[] constructeurs = classe.getConstructors();
+ constructeursSignatures = Arrays.asList(constructeurs).stream().map(e -> e.toString()).collect(Collectors.toList());
+ }
+
+
+ static Stream attributsProvider() {
+ return Stream.of(
+ arguments("sock", "java.net.Socket", Modifier.PROTECTED),
+ arguments("out", "java.io.PrintStream", Modifier.PROTECTED),
+ arguments("in", "java.io.BufferedReader", Modifier.PROTECTED),
+ arguments("ipPort", "java.lang.String", Modifier.PROTECTED),
+ arguments("connected", "boolean", Modifier.PROTECTED)
+ );
+ }
+ @DisplayName("Déclaration des attributs")
+ @ParameterizedTest
+ @MethodSource("attributsProvider")
+ void testDeclarationAttributs(String nom, String type, int modifier) throws NoSuchFieldException {
+ Field field = classe.getDeclaredField(nom);
+ Assertions.assertEquals(type, field.getType().getName(),
+ "Type " + nom + " erroné : doit être " + type);
+ Assertions.assertEquals(modifier, field.getModifiers(),
+ "Visibilité " + nom + " erronée : doit être " + modifier);
+ }
+
+
+ static Stream constructeursProvider() {
+ return Stream.of(
+ arguments("public %s.ClientTCP(java.lang.String,int) throws java.io.IOException")
+ );
+ }
+ @DisplayName("Déclaration des constructeurs")
+ @ParameterizedTest
+ @MethodSource("constructeursProvider")
+ void testConstructeurs1(String signature) {
+ Assertions.assertTrue(constructeursSignatures.contains(String.format(signature, module)),
+ String.format("Constructeur non déclaré : doit être %s\nalors que sont déclarés %s",
+ signature, constructeursSignatures));
+
+ }
+
+ static Stream methodesProvider() {
+ return Stream.of(
+ arguments("isConnected", "public boolean %s.ClientTCP.isConnected()"),
+ arguments("send", "public void %s.ClientTCP.send(java.lang.String) throws java.io.IOException"),
+ arguments("receive", "public java.lang.String %s.ClientTCP.receive() throws java.io.IOException"),
+ arguments("close", "public void %s.ClientTCP.close()")
+ );
+ }
+ @DisplayName("Déclaration des méthodes")
+ @ParameterizedTest
+ @MethodSource("methodesProvider")
+ void testDeclarationMethodes1(String nom, String signature) {
+ Assertions.assertTrue(methodesSignatures.contains(String.format(signature, module, module)),
+ String.format("Méthode non déclarée : doit être %s\nalors que sont déclarés %s",
+ signature, methodesSignatures));
+ }
+ }
+
+ @DisplayName("02-Connexion/déconnexion (port=1800)")
+ @Nested
+ class ConnexionTest {
+ static int port = 1800;
+ static ServerSocket passiveSocket;
+
+ @BeforeAll
+ static void init() throws IOException {
+ passiveSocket = new ServerSocket(1800);
+ }
+
+ @AfterAll
+ static void close() throws IOException {
+ passiveSocket.close();
+ }
+
+ @DisplayName("Connexion+deconnexion")
+ @Test
+ void testConnexion() throws IOException {
+ ClientTCP client = new ClientTCP("localhost", port);
+ Assertions.assertNotNull(client, "Connexion impossible");
+ Assertions.assertTrue(client.isConnected(), "Etat de connexion erroné");
+ client.close();
+ Assertions.assertFalse(client.isConnected(), "Etat de connexion erroné");
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/chat/src/test/java/rtgre/server/ChatServerTest.java b/chat/src/test/java/rtgre/server/ChatServerTest.java
new file mode 100644
index 0000000..da1a615
--- /dev/null
+++ b/chat/src/test/java/rtgre/server/ChatServerTest.java
@@ -0,0 +1,7 @@
+package rtgre.server;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class ChatServerTest {
+
+}
\ No newline at end of file