0

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;
    }
    //.....................
}
Nikolas
  • 2,322
  • 9
  • 33
  • 55

2 Answers2

2

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.

Aaron Digulla
  • 321,842
  • 108
  • 597
  • 820
  • As I understand, I should create `class Data extends JFrame{ String name; String ip; int port; void setError(String error){/*print error in frame*/} void show(){/*paint window*/} }` "When it has all the data" - I suggest it will looks like: `if(data.getName()!= null && data.getIp() != null && data.getPort()!=0)` But I steel puzzled how to retreive DuplicateNameException in the client from the server? Because sending and receiving messages are invoked in two threads. And I send data and connect to the server in thread that sending messages. List of user names stored on the server. – Nikolas Sep 04 '14 at 13:09
  • 1
    For the setup phase, things will be much more simple if you read the server's answers in the sending thread until it says "user name is OK". That way, you can throw an exception inside of `client.register()`. If you can't do this, then you need to add the error handler in the receiver which means you need to pass the `data` instance around. – Aaron Digulla Sep 04 '14 at 13:18
  • Aaron, thank you for answers! Could you please explain method data.show() and what to do to open the dialog again (with the data the user already entered)? – Nikolas Sep 04 '14 at 14:20
  • `show()` just displays the frame again. If you use `class Data extends JFrame`, that would be `data.setVisible(true);` – Aaron Digulla Sep 04 '14 at 14:23
  • Great!!! It works!!!))) Thanks a lot for your help! I just should have read the messages in the same thread in which I sent a user name to the server. – Nikolas Sep 04 '14 at 15:12
0

With the help of Aaron Digulla I found the solution. The key was

For the setup phase, things will be much more simple if you read the server's answers in the sending thread

I changed method register() in Client.java. when received message equals to some constant, than throw an Exception and handle it in my client form:

 public void register(String name) {
    output.println(name);
    String receive = null;
    try {
        receive = input.readLine();
        if (receive != null) {
            switch (receive) {
                case AppConstants.REPEATED_NAME_ERROR:
                    System.out.println(AppConstants.REPEATED_NAME_MESSAGE);
                    throw new DuplicateNameException();
                case AppConstants.OK_REGISTERED:
                    isRegistered = true;
                    break;
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    }

and modified listener in Frame.java

confirm.addActionListener(new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {

            name = nameText.getText();
            ip = ipText.getText();

            if (name != null && ip != null && port != 0) {
                client.connect(ip, port);
                try {
                    client.register(name);
                    JOptionPane.showMessageDialog(null,
                            "OK", "Info", JOptionPane.INFORMATION_MESSAGE);
                } catch (DuplicateNameException e1) {
                    JOptionPane.showMessageDialog(null,
                                AppConstants.REPEATED_NAME_MESSAGE, "Info", JOptionPane.WARNING_MESSAGE);
                }
            } else {
                JOptionPane.showMessageDialog(null,
                        "Something missing!", "Info", JOptionPane.WARNING_MESSAGE);
            }
        }
    });
Nikolas
  • 2,322
  • 9
  • 33
  • 55