diff --git a/pom.xml b/pom.xml index c8f2162..6d17e05 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 fr.univ-lyon1 - Java TP + java-tp 1.0-SNAPSHOT @@ -65,7 +65,7 @@ default-cli - fr.watteau.javafx_demo/fr.watteau.javafx_demo.HelloApplication + fr.univ.lyon1.gui.MainGui diff --git a/src/fr/univ/lyon1/client/Client.java b/src/fr/univ/lyon1/client/Client.java new file mode 100644 index 0000000..19718cc --- /dev/null +++ b/src/fr/univ/lyon1/client/Client.java @@ -0,0 +1,69 @@ +package fr.univ.lyon1.client; + +import fr.univ.lyon1.common.Message; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.net.Socket; + +public class Client { + private final int port; + private final String address; + protected final Socket socket; + protected final ObjectOutputStream out; + private ObjectInputStream in; + protected boolean started = false; + + public Client(String address, int port) throws IOException, InterruptedException { + this.address = address; + this.port = port; + socket = new Socket(address, port); + out = new ObjectOutputStream(socket.getOutputStream()); + } + + public void disconnectedServer() throws IOException { + socket.close(); + out.close(); + if (in != null) + in.close(); + + System.exit(0); + } + + public String sendMessage(String content) { + Message msg = new Message(null, content); + + try { + out.writeObject(msg); + out.flush(); + } catch (IOException e) { + System.err.println("Fail to send message !"); + e.printStackTrace(); + } + + return content; + } + + public Message messageReceived(Message msg) { + System.out.println(); + System.out.println(msg); + return msg; + } + + public void run() throws InterruptedException, IOException { + if (started) + return; + Thread clientSendThread = new Thread(new ClientSend(this, out, socket)); + clientSendThread.start(); + + Thread clientReceiveThread = new Thread(new ClientReceive(this, socket)); + clientReceiveThread.start(); + + started = true; + + clientSendThread.join(); + socket.close(); + clientReceiveThread.interrupt(); + } +} diff --git a/src/fr/univ/lyon1/client/ClientReceive.java b/src/fr/univ/lyon1/client/ClientReceive.java new file mode 100644 index 0000000..ddccb12 --- /dev/null +++ b/src/fr/univ/lyon1/client/ClientReceive.java @@ -0,0 +1,61 @@ +package fr.univ.lyon1.client; + +import fr.univ.lyon1.common.Message; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.net.Socket; +import java.net.SocketException; + +public class ClientReceive implements Runnable { + private final Client client; + private ObjectInputStream in; + private final Socket socket; + + public ClientReceive(Client client, Socket socket) { + this.client = client; + this.socket = socket; + } + + @Override + public void run() { + try { + in = new ObjectInputStream(socket.getInputStream()); + } catch (IOException e) { + e.printStackTrace(); + return; + } + + while(true) { + Message msg; + try { + msg = (Message) in.readObject(); + } catch (ClassNotFoundException|IOException e) { + if (e instanceof SocketException) { + System.err.println("Connexion closed"); + break; + } + System.err.println("Fail to read message object !"); + e.printStackTrace(); + try { + Thread.sleep(1000); + } catch (InterruptedException ex) { + break; + } + continue; + } + + if (msg == null) + break; + + this.client.messageReceived(msg); + } + + try { + client.disconnectedServer(); + } catch (IOException e) { + System.err.println("Fail to disconnect !"); + e.printStackTrace(); + } + } +} diff --git a/src/fr/univ/lyon1/client/ClientSend.java b/src/fr/univ/lyon1/client/ClientSend.java new file mode 100644 index 0000000..d3d82fe --- /dev/null +++ b/src/fr/univ/lyon1/client/ClientSend.java @@ -0,0 +1,31 @@ +package fr.univ.lyon1.client; + +import java.io.ObjectOutputStream; +import java.net.Socket; +import java.util.Scanner; + +public class ClientSend implements Runnable { + private final Client client; + private final ObjectOutputStream out; + private final Socket socket; + + public ClientSend(Client client, ObjectOutputStream out, Socket socket) { + this.client = client; + this.out = out; + this.socket = socket; + } + + public void run() { + Scanner sc = new Scanner(System.in); + + while (true) { + System.out.print(">> "); + String m = sc.nextLine(); + + if (m.equals("exit")) + break; + + client.sendMessage(m); + } + } +} diff --git a/src/fr/univ/lyon1/client/MainClient.java b/src/fr/univ/lyon1/client/MainClient.java new file mode 100644 index 0000000..82c088b --- /dev/null +++ b/src/fr/univ/lyon1/client/MainClient.java @@ -0,0 +1,26 @@ +package fr.univ.lyon1.client; + +import java.io.IOException; + +public class MainClient { + public static void main(String[] args) { + try { + if (args.length != 2) { + printUsage(); + } else { + String address = args[0]; + int port = Integer.parseInt(args[1]); + Client c = new Client(address, port); + c.run(); + } + } catch (IOException|InterruptedException e) { + e.printStackTrace(); + } + } + + private static void printUsage() { + System.out.println("java client.Client
"); + System.out.println("\t
: server's ip address"); + System.out.println("\t: server's port"); + } +} diff --git a/src/fr/univ/lyon1/common/Message.java b/src/fr/univ/lyon1/common/Message.java new file mode 100644 index 0000000..15a8638 --- /dev/null +++ b/src/fr/univ/lyon1/common/Message.java @@ -0,0 +1,31 @@ +package fr.univ.lyon1.common; + +import java.io.Serializable; + +public class Message implements Serializable { + private String sender; + private final String content; + + + public Message(String sender, String content) { + this.sender = sender; + this.content = content; + } + + public void setSender(String sender) { + this.sender = sender; + } + + public String getSender() { + return sender; + } + + public String getContent() { + return content; + } + + @Override + public String toString() { + return sender + ": " + content; + } +} diff --git a/src/fr/univ/lyon1/gui/ClientGUI.java b/src/fr/univ/lyon1/gui/ClientGUI.java new file mode 100644 index 0000000..634aefc --- /dev/null +++ b/src/fr/univ/lyon1/gui/ClientGUI.java @@ -0,0 +1,33 @@ +package fr.univ.lyon1.gui; + +import fr.univ.lyon1.client.Client; +import fr.univ.lyon1.client.ClientReceive; +import fr.univ.lyon1.common.Message; + +import java.io.IOException; + +public class ClientGUI extends Client { + private final MainGui gui; + + public ClientGUI(MainGui gui, String address, int port) throws IOException, InterruptedException { + super(address, port); + this.gui = gui; + } + + @Override + public Message messageReceived(Message msg) { + gui.receiveMessage(msg.toString()); + return msg; + } + + @Override + public void run() { + if (started) + return; + + Thread clientReceiveThread = new Thread(new ClientReceive(this, super.socket)); + clientReceiveThread.start(); + + started = true; + } +} diff --git a/src/fr/univ/lyon1/gui/ClientPanel.java b/src/fr/univ/lyon1/gui/ClientPanel.java new file mode 100644 index 0000000..e47bd98 --- /dev/null +++ b/src/fr/univ/lyon1/gui/ClientPanel.java @@ -0,0 +1,85 @@ +package fr.univ.lyon1.gui; + +import javafx.application.Platform; +import javafx.event.ActionEvent; +import javafx.scene.Parent; +import javafx.scene.control.Button; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextArea; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; + +public class ClientPanel extends Parent { + private TextArea textToSend = new TextArea(); + private ScrollPane scrollReceivedText = new ScrollPane(); + private TextFlow receivedText = new TextFlow(); + private Button sendBtn = new Button(); + private Button clearBtn = new Button(); + private final MainGui gui; + + ClientPanel(MainGui gui) { + this.gui = gui; + scrollReceivedText.setLayoutX(20); + scrollReceivedText.setLayoutY(20); + scrollReceivedText.setPrefWidth(400); + scrollReceivedText.setPrefHeight(350); + scrollReceivedText.setContent(receivedText); + scrollReceivedText.vvalueProperty().bind(receivedText.heightProperty()); + this.getChildren().add(scrollReceivedText); + + sendBtn.setPrefWidth(80); + + int btnMargin = 20; + + textToSend.setLayoutX(scrollReceivedText.getLayoutX()); + textToSend.setLayoutY(scrollReceivedText.getLayoutY() + scrollReceivedText.getPrefHeight() + 20); + textToSend.setPrefWidth(400-sendBtn.getPrefWidth()-btnMargin); + textToSend.setPrefHeight(100); + this.getChildren().add(textToSend); + textToSend.setOnKeyPressed(this::textToSendKeyPressed); + + sendBtn.setText("Send"); + sendBtn.setLayoutX(textToSend.getLayoutX() + textToSend.getPrefWidth() + btnMargin); + sendBtn.setLayoutY(textToSend.getLayoutY()); + sendBtn.setPrefWidth(80); + sendBtn.setPrefHeight(40); + sendBtn.setVisible(true); + this.getChildren().add(sendBtn); + sendBtn.setOnAction(this::sendBtnAction); + + clearBtn.setText("Clear"); + clearBtn.setLayoutX(sendBtn.getLayoutX()); + clearBtn.setPrefWidth(sendBtn.getPrefWidth()); + clearBtn.setPrefHeight(sendBtn.getPrefHeight()); + clearBtn.setLayoutY(textToSend.getLayoutY() + textToSend.getPrefHeight() - clearBtn.getPrefHeight()); + clearBtn.setVisible(true); + this.getChildren().add(clearBtn); + clearBtn.setOnAction(this::clearBtnAction); + } + + private void send() { + String msg = textToSend.getText(); + gui.sendMessage(msg); + textToSend.clear(); + addMessage(msg); + } + + private void sendBtnAction(ActionEvent e) { + send(); + } + + private void textToSendKeyPressed(KeyEvent e) { + if (e.getCode() == KeyCode.ENTER) + send(); + } + + private void clearBtnAction(ActionEvent e) { + textToSend.clear(); + } + + public void addMessage(String message) { + Platform.runLater(() -> receivedText.getChildren().add(new Text(message+"\n"))); + } +} diff --git a/src/fr/univ/lyon1/gui/MainGui.java b/src/fr/univ/lyon1/gui/MainGui.java new file mode 100644 index 0000000..2b1c691 --- /dev/null +++ b/src/fr/univ/lyon1/gui/MainGui.java @@ -0,0 +1,45 @@ +package fr.univ.lyon1.gui; + +import javafx.application.Application; +import javafx.scene.Group; +import javafx.scene.Scene; +import javafx.stage.Stage; + +import java.util.List; + +public class MainGui extends Application { + private ClientPanel clientPanel; + private ClientGUI client; + + @Override + public void start(Stage stage) throws Exception { + List parameters = this.getParameters().getUnnamed(); + client = new ClientGUI(this, parameters.get(0), Integer.parseInt(parameters.get(1))); + //ToDo: error management especially for bad server IP/port + //ToDo: Server IP/port enter by user on the GUI + + stage.setTitle("Chat client"); + stage.setWidth(440); + + clientPanel = new ClientPanel(this); + Group root = new Group(); + root.getChildren().add(clientPanel); + Scene scene = new Scene(root, 600, 500); + + stage.setScene(scene); + stage.show(); + client.run(); + } + + public static void main(String[] args) { + Application.launch(MainGui.class, args); + } + + public void sendMessage(String msg) { + client.sendMessage(msg); + } + + public void receiveMessage(String msg) { + clientPanel.addMessage(msg); + } +} diff --git a/src/fr/univ/lyon1/server/ConnectedClient.java b/src/fr/univ/lyon1/server/ConnectedClient.java new file mode 100644 index 0000000..39d8d0b --- /dev/null +++ b/src/fr/univ/lyon1/server/ConnectedClient.java @@ -0,0 +1,64 @@ +package fr.univ.lyon1.server; + +import fr.univ.lyon1.common.Message; + +import java.io.EOFException; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.net.Socket; + +public class ConnectedClient implements Runnable { + private static int idCounter = 0; + private final int id = idCounter++; + private final Server server; + private final Socket socket; + private final ObjectOutputStream out; + private ObjectInputStream in; + + ConnectedClient(Server server, Socket socket) throws IOException { + this.server = server; + this.socket = socket; + this.out = new ObjectOutputStream(socket.getOutputStream()); + } + + public Message sendMessage(Message message) throws IOException { + out.writeObject(message); + out.flush(); + return message; + } + + public void run() { + try { + in = new ObjectInputStream(socket.getInputStream()); + + while (true) { + Message msg = (Message) in.readObject(); + + if (msg == null) + break; + + msg.setSender(String.valueOf(id)); + server.broadcastMessage(msg, id); + } + } catch (IOException | ClassNotFoundException e) { + if (!(e instanceof EOFException)) { + System.err.println("Client connection error"); + e.printStackTrace(); + } + } finally { + server.disconnectedClient(this); + } + } + + public void closeClient() throws IOException { + if (in != null) + in.close(); + out.close(); + socket.close(); + } + + public int getId() { + return id; + } +} diff --git a/src/fr/univ/lyon1/server/Connection.java b/src/fr/univ/lyon1/server/Connection.java new file mode 100644 index 0000000..7ab3b09 --- /dev/null +++ b/src/fr/univ/lyon1/server/Connection.java @@ -0,0 +1,31 @@ +package fr.univ.lyon1.server; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; + +public class Connection implements Runnable { + private final Server server; + private final ServerSocket serverSocket; + + Connection(Server server) throws IOException { + this.server = server; + this.serverSocket = new ServerSocket(server.getPort()); + } + + public void run() { + while (true) { + Socket clientSocket; + try { + clientSocket = serverSocket.accept(); + ConnectedClient client = new ConnectedClient(server, clientSocket); + server.addClient(client); + Thread clientThread = new Thread(client); + clientThread.start(); + } catch (IOException e) { + System.err.println("Fail to connect the new client !"); + e.printStackTrace(); + } + } + } +} diff --git a/src/fr/univ/lyon1/server/MainServer.java b/src/fr/univ/lyon1/server/MainServer.java new file mode 100644 index 0000000..5809841 --- /dev/null +++ b/src/fr/univ/lyon1/server/MainServer.java @@ -0,0 +1,23 @@ +package fr.univ.lyon1.server; + +import java.io.IOException; + +public class MainServer { + public static void main(String[] args) { + try { + if (args.length != 1) { + printUsage(); + } else { + int port = Integer.parseInt(args[0]); + new Server(port); + } + } catch (IOException e1) { + e1.printStackTrace(); + } + } + + private static void printUsage() { + System.out.println("java server.Server "); + System.out.println("\t: server's port"); + } +} diff --git a/src/fr/univ/lyon1/server/Server.java b/src/fr/univ/lyon1/server/Server.java new file mode 100644 index 0000000..ca711d6 --- /dev/null +++ b/src/fr/univ/lyon1/server/Server.java @@ -0,0 +1,65 @@ +package fr.univ.lyon1.server; + +import fr.univ.lyon1.common.Message; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class Server { + private final int port; + private List clients = new ArrayList<>(); + + Server(int port) throws IOException { + this.port = port; + Thread connection = new Thread(new Connection(this)); + connection.start(); + } + + public ConnectedClient addClient(ConnectedClient newClient) { + Message msg = new Message( "Server", newClient.getId() + " is connected !"); + + clients.add(newClient); + + broadcastMessage(msg, -1); + + System.out.println("Client "+newClient.getId()+" is connected !"); + + return newClient; + } + + public int broadcastMessage(Message message, int id) { + for (ConnectedClient client : clients) { + if (id == -1 || client.getId() != id) + try { + client.sendMessage(message); + } catch (IOException e) { + System.err.println("Fail to send message to " + client.getId()); + e.printStackTrace(); + } + } + return id; + } + + public ConnectedClient disconnectedClient(ConnectedClient client) { + try { + client.closeClient(); + } catch (IOException e) { + System.err.println("Fail to close client "+client.getId()); + e.printStackTrace(); + } + + clients.remove(client); + + Message msg = new Message("Server", "Client "+client.getId()+" is disconnected"); + + broadcastMessage(msg, -1); + + System.out.println("Client "+client.getId()+" disconnected"); + return client; + } + + public int getPort() { + return port; + } +}