Search code examples
javamultithreadingswingclient-serverserversocket

How to handle an input duplicate name error using swing in a multi-threaded chat


I create a client-server chat without graphical interface. And it works in console. now I want to create swing interface. But I get stuck trying to handle error duplicate name.

When Server accepts connection, it creates new thread for this connection and inside of this new thread it receives messages from the client. First message which is expected - is the user name. Server checks the list with already registered user's name. If the name is already in use, server sends error message to the client and continues to expect the name from the client. And it's easy to handle it in console - the client just receives the message from the server and continues to input the name (the client does not need to enter again ip-address, for the connection is already established)

Another situation with swing. (I would like to separate logic from representation for the further modification). So, when the user registers, he enters the ip-address, chooses a port from ComboBox and enters his name. After pressing the button connection with the server is established.

My question is: how can I handle error "name already in use" and repeat entering the name without creating connection twice.

Here is my Client.java

package nikochat.com.client;

import nikochat.com.app.AppConstants;
import nikochat.com.service.StreamsManager;
import nikochat.com.ui.UserInterface;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.NoSuchElementException;

public class Client {


    private Socket socket;
    private BufferedReader input;
    private PrintWriter output;
    private boolean stopped;
    private String name;
    private UserInterface ui;
    /**
     * результат регистрации имени на сервере
     */
    private boolean isRegistered = false;

    public boolean isRegistered() {
        return isRegistered;
    }

    public Client(UserInterface ui) {
        this.ui = ui;
    }

    /**
     * This method must firstly be invoked to make a connection with the server
     * initialise input and output streams,
     * and register the user in the chat.
     *
     * @param ip   ip address
     * @param port
     */
    public synchronized void connect(String ip, int port) {
        socket = connectToServer(ip, port);
        input = StreamsManager.createInput(socket, this.getClass());
        output = StreamsManager.createOutput(socket, this.getClass());
    }

    /**
     * Must be invoked after connect() method
     * @param name is the username for registering in the chat
     */
    public void register(String name) {
        output.println(name);
    }
    /**
     * receiving messages
     */
    private void receive() {
        new Thread(new ReceiveMessage()).start();
    }

    private void send() {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        /** Sending messages */
        try {
            while (!stopped) {
                String message = ui.write();
                if (stopped) break;
                if (message.equals("")) continue;
                output.println(message);
                if (message.trim().equals("exit")) {
                    stopped = true;
                    break;
                }
            }
            close();
        } catch (IOException e) {
            System.out.println("Error closing socket");
            e.printStackTrace();
            /** аварийный выход */
        } catch (NoSuchElementException n) {
            stopped = true;
            output.println("exit");
        }
    }

    private Socket connectToServer(String ip, int port) {
        Socket socket = null;
        try {
            socket = new Socket(ip, port);
        } catch (IOException e) {
            System.out.println("Error creating socket in client");
            e.printStackTrace();
        }
        return socket;
    }

    private synchronized void close() throws IOException {
        StreamsManager.closeInput(input, this.getClass());
        StreamsManager.closeOutput(output);
        socket.close();
    }

    class ReceiveMessage implements Runnable {
        @Override


    public void run() {
            while (!stopped) {
                try {
                    String receive = input.readLine();
                    if (receive != null) {
                        switch (receive) {
                            case AppConstants.REPEATED_NAME_ERROR:
                                System.out.println(AppConstants.REPEATED_NAME_MESSAGE);
                                register(ui.getClientName());
                                break;
                            case AppConstants.OK_REGISTERED:
                                isRegistered = true;
                                break;
                            case "MAX":
                                System.out.println("Достигнуто максимальное количество пользователей");
                                stopped = true;
                                break;
                            case "exit":
                                break;
                            case "denied":
                                System.out.println("Сервер недоступен");
                                stopped = true;
                                break;
                            default:
                                System.out.println(receive);
                        }
                    } else {
                        System.out.println(AppConstants.SERVER_UNAVAILABLE_MESSAGE);
                        close();
                        break;
                    }
                } catch (IOException e) {
                    stopped = true;
                    System.out.println("Error receiving message from server ");
                    e.printStackTrace();
                }
            }
        }
    }
}

and here is peace of code that responsible for presentation (Do not pay attention on field GUI gui, It's just another layer that I tried create between logic and presentation, maybe in future this will not be used):

public class Frame extends JFrame {

private JButton confirm;
private JButton cancel;
private JTextField nameText;
private JTextField ipText;
private JComboBox<Integer> portChooser;
private GUI gui;
private Client client;
private int port;

//.......

     public Frame() {
        gui = new GUI();
        client = new Client(gui);
        //............
    }

 confirm.addActionListener(new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
            gui.setName(nameText.getText());
            gui.setIp(ipText.getText());


            new EventQueue().invokeLater(new Runnable() {
                @Override
                public void run() {
                    client.connect(ipText.getText(), port);
                    client.register(nameText.getText());
                    //WHAT TO DO???
                }
            });
        }
    });
}

I think I should show some code from Server.java:

public class Server {


private ServerSocket server;
private final Map<String, ServerThread> clients = Collections.synchronizedMap(new TreeMap<>());
private final Queue<String> history = new ConcurrentLinkedQueue<>();

public Server() {
    System.out.println("Server is running...");
    Log.write("Server is running...");

    new Thread(new ServerMenu(this)).start();

    try {
        server = new ServerSocket(AppConfig.PORT);
    } catch (IOException e) {
        System.out.println("Error creating server");
        Log.write("Error creating server");
        Log.write(e.getMessage());
        e.printStackTrace();
    }


    while (true) {
        try {
            Socket accept = server.accept();
            Log.write("server accept socket");
            ServerThread serverThread = new ServerThread(accept);
            new Thread(serverThread).start();
            Log.write("server start new ServerThread");
        } catch (IOException e) {
            System.out.println("Error accepting client on server");
            Log.write("Error accepting client on server");
            Log.write(e.getMessage());
            e.printStackTrace();
        }
    }
}

private class ServerThread implements Runnable {

    private Socket socket;
    private BufferedReader in;
    private PrintWriter out;
    private String name;

    public ServerThread(Socket socket) {
        this.socket = socket;
        in = StreamsManager.createInput(socket, this.getClass());
        out = StreamsManager.createOutput(socket, this.getClass());
    }

    @Override
    public void run() {
        try {
            boolean goFurther = true; /*for emergency exit*/
            /** firstly, receive client name" */
            try {
                goFurther = readClientName();
            } catch (IOException e) {
                System.out.println("Error reading name from client...");
                Log.write("Error reading name from client...");
                Log.write(e.getMessage());
                e.printStackTrace();
            }
            if (goFurther) {
                String time = getTimeWithoutMillis(LocalTime.now());
                String invitation = time + " " + name + " has joined";
                printHistory();
                addToHistory(invitation);

                System.out.println(time + "  " + name + " has joined");
                System.out.println("numbers of users: " + clients.size());
                resendMessage(invitation);

                /** read from input stream */
                while (true) {
                    String received = null;
                    try {
                        received = in.readLine();
                        time = getTimeWithoutMillis(LocalTime.now());
                    } catch (IOException e) {
                        System.out.println("Error reading message from client...");
                        Log.write("Error reading message from client...");
                        Log.write(e.getMessage());
                        e.printStackTrace();
                    }
                    if (received == null) {
                        Log.write("received message from client is null");
                        break;
                    }

                    if (!received.trim().equals("exit")) {
                        String local = time + " " + name + ": " + received;
                        resendMessage(local);
                        addToHistory(local);
                    } else {
                        received = time + " " + name + " exit from chat";
                        addToHistory(received);
                        resendMessage(received);
                        out.println("exit");
                        System.out.println(received);
                        Log.write(received);
                        break;
                    }
                }
            }
        } finally {
            try {
                closeConnection();
                clients.remove(name);
            } catch (IOException e) {
                System.out.println("Error closing socket on server side");
                Log.write("Error closing socket on server side");
                Log.write(e.getMessage());
                e.printStackTrace();
            }
        }
    }

    private void printHistory() {
        synchronized (history) {
            history.forEach(out::println);
        }
    }

    private boolean readClientName() throws IOException {
        boolean continueProgram = true;
        while (true) {
            name = in.readLine();
            if (name == null) {
                continueProgram = false;
                Log.write("read name is null");
                break;
            }
            if (!(clients.size() < AppConfig.MAX_USERS)) {
                out.println("MAX");
                continueProgram = false;
                Log.write("reduce register new connection");
                break;
            }
            if (clients.get(name) == null) {
                clients.put(name, this);
                Log.write("register new user with the name: " + name);
                out.println(AppConstants.OK_REGISTERED);
                break;
            } else {
                out.println(AppConstants.REPEATED_NAME_ERROR);
                out.print("> ");
            }
        }
        return continueProgram;
    }
    //.....................
}

Solution

  • Move the code which asks for IP address and user name to an isolated class. It should not open connections, it should just collect the data.

    When it has all the data, it should invoke a callback (i.e. in the ActionListener of confirm, invoke another listener).

    The callback needs to look like so:

    client.connect(data.getAddress(), data.getPort());
    try {
        client.register(data.getUserName());
    } catch( DuplicateNameException e ) {
        data.setError( "Name already taken" );
        data.show();
        return;
    }
    

    This opens the dialog again (with the data the user already entered) if there is a problem. You can use the same approach for exceptions during client.connect().

    Also, client should check if there already is an active connection and not connect again when address and port is still the same. Alternatively, you can check for an active connection in client.connect() and close it.