Salta el contingut
 

Examen 2a avaluació

Joan Puigcerver Ibáñez

j.puigcerveribanez@edu.gva.es

Llicència: CC BY-NC-SA 4.0

(Reconeixement - NoComercial - CompartirIgual) 🅭

Entrega

Perill

Si l'entrega no compleix aquests criteris, no és qualificarà.

L'entrega ha de complir els següents requisits:

  • El codi ha d'estar en el paquet corresponent: exam2
  • El format de la eixida del programa ha de ser consistent amb el format demanat.
  • S'ha d'entregar un fitxer .zip amb el contingut del paquet.
  • El codi ha d'estar pujat a GitHub en el vostre repositori de l'assignatura.
  • Tag GitHub: Exam2 (StackOverflow: Create a tag in a GitHub repository)

Exercici: Messaging application

exam2

Es demana implementar una apliació de missatgeria instantània, la qual permitisca enviar missatges a diferents usuaris.

L'apliació es connectarà mitjançant JSSE, utilitzant SSLSockets.

Certificats

Per poder utilitzar els SSLSockets cal generar un certificat per al servidor, que després serà importat en la TrustStore del client:

Important

Cal adjuntar els fitxers generats a GitHub.

Perill

NO UTILITZEU RUTES ABSOLUTES EN EL CODI

  • Server KeyStore: files/exam2/messaging-keystore.jks
  • Client TrustStore: files/exam2/messaging-truststore.jks

  • Certificat:

    Quan inicie el servidor, ha de mostrar la informació del certificat per pantalla.

    Camp Valor
    Àlies messaging-server
    CN MessagingServer
    OU PSP-DAM2S
    O CIPFP Mislata
    L Mislata
    ST València
    C ES

MessagingServer

El servidor s'encarrega de processar les peticions que li arriben des del client. Aquestes peticions poden ser del tipus:

  • LIST: El client dessitja saber els clients connectats.

    • El servidor retornarà un SUCCESS amb els clients connectats separats per comes.
  • SEND: El client dessitja enviar un missatge a un altre client connectat:

    • Si hi ha un client connectat amb aquest alias, s'enviarà el missatge i contestarà amb un SUCESS indicant que s'ha enviat correctament el missatge.
    • Si no hi ha cap client connectat, el servidor contestarà amb un USER_NOT_FOUND_ERROR.
    • Si el missatge està buit, el servidor contestarà amb un INVALID_MESSAGE_ERROR.
  • CHANGE_NAME: El client dessitja canviar el seu alies.

    • Si el client ha indicat un alies (en el camp del alies), es canviarà i contestarà amb un missatge de tipus SUCCESS.
    • Si ja hi ha un altre client amb aquest alies o no s'ha especificat, contestarà amb USER_ALREADY_EXISTS_ERROR.

MessagingClient

El client es connecta amb el servidor i permet enviar missatges a altres clients.

El client té les següents commandes:

  • \exit: Ix del xat.
  • \msg alias message: Envia el missatge message a el client identificat per alias.
  • \change alias: Canvia el seu alias a alias.
  • \list: Li demana al servidor els cliens connectats i els mostra per pantalla.

El client té la classe MessagingClientListener, que s'encarrega de rebre els missatges del servidor. Depen dels tipus de missatge, ha de mostrar els missatges rebuts en el següent format:

  • SEND:
    <alias>: <Message>
    
  • SUCCESS:
    <Message>
    
  • USER_NOT_FOUND_ERROR:
    ERROR: User '<alias>' not found
    
  • INVALID_MESSAGE_ERROR:
    ERROR: Invalid message
    
  • USER_ALREADY_EXISTS_ERROR:
    ERROR: User '<alias>' already exists
    

Diagrames de seqüència

  • LIST:
sequenceDiagram
    actor Usuari
    Usuari->>Client: \list
    Client->>Servidor: LIST
    Servidor-->>Client: SUCCESS [llista]
    Client-->>Usuari: [message]
  • SEND:

    sequenceDiagram
        actor Usuari
        Usuari->>Client: \msg [alias_destinatari] [message]
        Client->>Servidor: SEND [alias_destinatari] [message]
        alt no existeix usuari
        Servidor-->>Client: USER_NOT_FOUND_ERROR
        Client-->>Usuari: Error: User '[alias]' not found
        else missatge buit
        Servidor-->>Client: INVALID_MESSAGE_ERROR
        Client-->>Usuari: Error: Invalid message
        else
        Servidor-->>Client: SUCCESS
        actor Desinatari
        Servidor->>Desinatari: SEND [alias_emisor] [message]
        Client-->>Usuari: [alias_emisor]: [message]
        end

  • CHANGE_NAME:

    sequenceDiagram
        actor Usuari
        Usuari->>Client: \change [alias]
        Client->>Servidor: CHANGE_NAME
        alt no existeix usuari
        Servidor-->>Client: SUCCESS
        Client-->>Usuari: [message]
        else
        Servidor-->>Client: USER_ALREADY_EXISTS_ERROR
        Client-->>Usuari: Error: User '[alias]' already exists
        end

Codi font

Servidor

MessagingServer.java
package exam2.server;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Scanner;
import java.util.stream.Collectors;
import java.util.List;

public class MessagingServer extends Thread {
    ServerSocket server;

    List<MessagingServerHandler> clients;
    boolean running;

    public MessagingServer(int port) throws IOException {
        // TODO: Crea un SocketServer mitjançant JSSE
        this.server = null;
        clients = new ArrayList<>();
        running = true;
    }

    public void close(){
        running = false;
        this.interrupt();
    }

    public synchronized void removeClient(MessagingServerHandler hc){
        clients.remove(hc);
    }

    /**
     * Retorna el client amb el alies especificat o null si no existeix
     * @param alias Alias de la persona a la que li envies el missatge
     * @return Retorna el client amb el alies especificat o null si no existeix
     */
    public MessagingServerHandler getClientByAlias(String alias){
        return clients.stream().filter(c -> c.getAlias().equals(alias)).findAny().orElse(null);
    }
    /**
     * Retorna els alies dels clients connectats separats per comes
     * @return Alies dels clients connectats separats per comes
     */
    public String connectedClients(){
        return clients.stream().map(MessagingServerHandler::getAlias).collect(Collectors.joining(","));
    }


    @Override
    public void run() {
        while (running){
            try {
                Socket client = server.accept();
                MessagingServerHandler messagingServerHandler = new MessagingServerHandler(client, this);
                clients.add(messagingServerHandler);
                messagingServerHandler.start();
                System.out.println("Nova connexió acceptada.");
            } catch (IOException e) {
                System.err.println("Error while accepting new connection");
                System.err.println(e.getMessage());
            }
        }
    }

    public static void main(String[] args) {
        try {
            Scanner scanner = new Scanner(System.in);
            MessagingServer server = new MessagingServer(1234);
            server.start();

            scanner.nextLine();

            server.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }
}
MessagingServerHandler.java
package exam2.server;

import exam2.models.MessagingMessage;
import exam2.models.MessagingMessageType;

import java.io.IOException;
import java.net.Socket;

public class MessagingServerHandler extends Thread {
    private final MessagingServer server;
    private final Socket client;

    // TODO: ObjectStream related object

    private String alias;

    public MessagingServerHandler(Socket client, MessagingServer server) throws IOException {
        this.server = server;
        this.client = client;
        // TODO: ObjectStream related object
        alias = "";
    }

    public String getAlias() {
        return alias;
    }

    public void setAlias(String alias) {
        this.alias = alias;
    }

    /**
     * TODO: Envia una petició
     * @param messagingMessage Petició a enviar
     * @throws IOException
     */
    public void sendMessage(MessagingMessage messagingMessage) throws IOException {
    }

    /**
     * TODO: Llegeix una petició
     * @return Petició llegida
     * @throws IOException
     * @throws ClassNotFoundException
     */
    public MessagingMessage readMessage() throws IOException, ClassNotFoundException {
        // TODO
        return null;
    }

    /**
     * TODO: Acció del servidor a les respostes del tipus SEND
     * @param messagingMessage Petició a processar
     */
    private void processSend(MessagingMessage messagingMessage) throws IOException {
    }

    /**
     * TODO: Acció del servidor a les respostes del tipus CHANGE_NAME
     * @param messagingMessage Petició a processar
     */
    private void processChangeName(MessagingMessage messagingMessage) throws IOException {
    }

    /**
     * TODO: Acció del servidor a les respostes del tipus LIST
     * @param messagingMessage Petició a processar
     */
    private void processList(MessagingMessage messagingMessage) throws IOException {
    }

    @Override
    public void run() {
        try {
            MessagingMessage messagingMessage;
            while((messagingMessage = readMessage()) != null){
                System.out.println(messagingMessage);
                switch (messagingMessage.type()) {
                    case SEND -> {
                        processSend(messagingMessage);
                    }
                    case CHANGE_NAME -> {
                        processChangeName(messagingMessage);
                    }
                    case LIST -> {
                        processList(messagingMessage);
                    }
                }
            }
        } catch (IOException | ClassNotFoundException e) {
            System.err.println(e.getMessage());
        }
        server.removeClient(this);
    }
}

Models

MessagingMessage.java
package exam2.models;

import java.io.Serializable;

/**
 * Classe que representa una petició o resposta entre el servidor i el client.
 * <p>
 * Aquesta classe implementa Serialitzable per poder ser convertida a
 * bytes i poder ser enviada mitjançant sockets.
 *
 * @see MessagingMessageType
 * @param type Tipus de la petició
 * @param alias Destinatari
 * @param message Missatge adjuntat
 */
public record MessagingMessage (
        MessagingMessageType type,
        String alias,
        String message
) implements Serializable {
    @Override
    public String toString() {
        return "Request{" +
                "type=" + type +
                ", alias='" + alias + '\'' +
                ", message='" + message + '\'' +
                '}';
    }
}
MessagingMessageType.java
package exam2.models;

import java.io.Serializable;

/**
 * Enumeració amb els diferents tipus de peticions que podem trobar
 * <p>
 * Aquesta classe implementa Serialitzable per poder ser convertida a
 * bytes i poder ser enviada mitjançant sockets.
 */
public enum MessagingMessageType implements Serializable {
    /**
     * El tipus LIST s'utilitza per indicar al servidor que es vol recuperar la llista de clients connectats
     */
    LIST,
    /**
     * El tipus CHANGE_NAME s'utilitza per canviar el àlias del client
     */
    CHANGE_NAME,
    /**
     * El tipus USER_ALREADY_EXISTS_ERROR s'utilitza per indicar que ja existeix un usuari amb aquest àlias
     */
    USER_ALREADY_EXISTS_ERROR,
    /**
     * El tipus SEND s'utilitza per enviar un missatge
     */
    SEND,
    /**
     * El tipus USER_NOT_FOUND_ERROR s'utilitza per indicar que l'usuari no existeix
     */
    USER_NOT_FOUND_ERROR,
    /**
     * El tipus INVALID_MESSAGE_ERROR s'utilitza per indicar que el missatge no és vàlid
     */
    INVALID_MESSAGE_ERROR,
    /**
     * El tipus SUCCESS s'utilitza per indicar que l'acció s'ha dut a terme correctament
     */
    SUCCESS,
}

Client

MessagingClient.java
package exam2.client;

import exam2.models.MessagingMessage;

import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;

public class MessagingClient {
    private final Socket socket;
    private final MessagingClientListener listener;
    private final Scanner scanner;

    // TODO: ObjectStream related object

    public MessagingClient(String host, int port) throws IOException {
        this.scanner = new Scanner(System.in);
        // TODO: Connectar-se mitjançant JSSE
        this.socket = null;
        // TODO: Object stream related objects

        this.listener = new MessagingClientListener(socket, this);
        this.listener.start();
    }

    /**
     * TODO: Envia una petició
     * @param messagingMessage Petició a enviar
     * @throws IOException
     */
    public void sendMessage(MessagingMessage messagingMessage) throws IOException {
    }

    /**
     * TODO: Demana a l'usuari el seu àlies i s'identifica amb el servidor
     * @throws IOException
     */
    public void identify() throws IOException {
        System.out.print("Introdueix el teu nom: ");
        String line = scanner.nextLine();
    }

    public void chat() throws IOException {
        System.out.println("Acabes d'entrar al chat.");
        System.out.println("Per exir, escriu \"/exit\".");
        String action;
        while((action = scanner.next()) != null && this.socket.isConnected()){
            switch (action){
                case "/exit" -> {
                    this.close();
                    return;
                }
                case "/list" -> {
                    list();
                }
                case "/change" -> {
                    String alias = scanner.nextLine();
                    change(alias);
                }
                case "/msg" -> {
                    String alias = scanner.next();
                    String message = scanner.nextLine();
                    msg(alias, message);
                }
                default -> {
                    System.out.printf("Ordre \"%s\" no trobada\n", action);
                    scanner.nextLine();
                }
            }
        }
    }

    private void list() throws IOException {
        // TODO: /list
    }

    private void change(String alias) throws IOException {
        // TODO: /change alias
    }

    private void msg(String alias, String message) throws IOException {
        // TODO: /msg alias message
    }

    public void close(){
        try {
            socket.close();
            listener.interrupt();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        System.out.println("Connectant-se amb el servidor...");
        try {
            MessagingClient chat = new MessagingClient("localhost", 1234);
            chat.identify();
            chat.chat();
        } catch (IOException e){
            System.err.println("Error connectant-se amb el servidor.");
        }
    }
}
MessagingClientListener.java
package exam2.client;

import exam2.models.MessagingMessage;

import java.io.IOException;
import java.net.Socket;

public class MessagingClientListener extends Thread {

    private final MessagingClient client;
    private final Socket socket;

    // TODO: ObjectStream related object

    public MessagingClientListener(Socket socket, MessagingClient client) throws IOException {
        this.client = client;
        this.socket = socket;
        // TODO: ObjectStream related object
    }

    /**
     * TODO: Llegeix una petició
     * @return Petició llegida
     * @throws IOException
     * @throws ClassNotFoundException
     */
    public MessagingMessage readMessage() throws IOException, ClassNotFoundException {
        return null;
    }

    // TODO: Reb missatges
    @Override
    public void run() {
        try {
            MessagingMessage messagingMessage;
            while((messagingMessage = readMessage()) != null){
                System.out.println(messagingMessage);
                switch (messagingMessage.type()) {
                    case SEND -> {
                        processSend(messagingMessage);
                    }
                    case SUCCESS -> {
                        processSuccess(messagingMessage);
                    }
                    case USER_NOT_FOUND_ERROR -> {
                        processUserNotFoundError(messagingMessage);
                    }
                    case INVALID_MESSAGE_ERROR -> {
                        processInvalidMessageError(messagingMessage);
                    }
                    case USER_ALREADY_EXISTS_ERROR -> {
                        processUserAlreadyExistsError(messagingMessage);
                    }
                }
            }
        } catch (IOException | ClassNotFoundException e) {
            System.err.println(e.getMessage());
        }
    }

    private void processSend(MessagingMessage messagingMessage) {
        // TODO: Acció del client a les respostes del tipus SEND
    }

    private void processSuccess(MessagingMessage messagingMessage) {
        // TODO: Acció del client a les respostes del tipus SUCCESS
    }

    private void processUserNotFoundError(MessagingMessage messagingMessage) {
        // TODO: Acció del client a les respostes del tipus USER_NOT_FOUND_ERROR
    }

    private void processInvalidMessageError(MessagingMessage messagingMessage) {
        // TODO: Acció del client a les respostes del tipus INVALID_MESSAGE_ERROR
    }

    private void processUserAlreadyExistsError(MessagingMessage messagingMessage) {
        // TODO: Acció del client a les respostes del tipus USER_ALREADY_EXISTS_ERROR
    }
}

Rúbrica

  • (1 punt) Generar el certificat i mostrar-lo per pantalla quan inicie el servidor.
  • (1 punt) Connectar el programa mitjançant JSSE.
  • (1 punt) Creació del objectes necessaris per l'enviament i recepció d'objectes.
  • (1 punt) Implementar els mètodes sendMessage(MessagingMessage m) i readMessage()
  • Servidor MessagingServerHandler:
    • (0.5 punts) LIST
    • CHANGE_NAME
      • (0.5 punts) SUCCESS
      • (0.5 punts) USER_ALREADY_EXISTS_ERROR
    • SEND
      • (0.5 punts) SUCCESS
      • (0.25 punts) USER_NOT_FOUND_ERROR
      • (0.25 punts) INVALID_MESSAGE_ERROR
  • Client MessagingClientListener:
    • (0.3 punts) SUCCESS
    • (0.3 punts) SEND
    • (0.3 punts) USER_NOT_FOUND_ERROR
    • (0.3 punts) INVALID_MESSAGE_ERROR
    • (0.3 punts) USER_ALREADY_EXISTS_ERROR
  • Client MessagingClient:
    • (0.5 punts) identify()
    • (0.5 punts) /list
    • (0.5 punts) /msg
    • (0.5 punts) /change
📌 Aquest document pot quedar desactualitzat després d’imprimir-lo. Pots consultar la versió més recent a la pàgina web.
🌿 Abans d’imprimir aquest document, considera si és realment necessari. Redueix el consum de paper i ajuda a protegir el nostre entorn.