Search code examples
javamultithreadingsocketschatserversocket

How do I get my multithreaded server/client chat program to echo messages to all clients using sockets?


right now I have a java program that uses threads and sockets to echo text responses like a real chat window. Currently, my program works by running the server and than as many clients as I want. When a client enters a message, that message is echoed to the server and also to the client that sent the message.

My problem is that I want the message any client enters to be sent not only to the server and to themselves, but to every other client as well.

Heres how it currently works:

Server:

Received client message: test1

Client 1:

Enter message: test1

test1

Client 2:

Enter message:

Client 1 enters test1, receives test1 back and the server also receives test1. Client 2 gets nothing. My goal is to have any messages entered in the clients display on the client that sent the message as well as the other clients and server.

Working example:

Server:

Received client message: test1

Received client message: hello

Client 1:

Enter message: test1

test1

From client 2: hello

Client 2:

Enter message:

From client 1: test1

hello

The formatting doesnt have to be exactly like that, but thats the idea. My code so far is below. Ive read that I need to add my clients to a list and then loop over them and send them all the message but im not sure. Any help would be great.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.Scanner;

public class EchoMultiThreadClient {

    public static void main(String[] args) {
        try (Socket socket = new Socket("localhost", 4000)) {
            
            //socket.setSoTimeout(5000);
            BufferedReader br = new BufferedReader(
                    new InputStreamReader(socket.getInputStream()));
            PrintWriter pw = new PrintWriter(socket.getOutputStream(), true);

            Scanner scanner = new Scanner(System.in);
            String echoString;
            String response;

            do {
                System.out.println("Enter string to be echoed: ");
                echoString = scanner.nextLine();

                pw.println(echoString);
                if(!echoString.equals("exit")) {
                    response = br.readLine();
                    System.out.println(response);
                }                          
                
                
            } while(!echoString.equals("exit"));
            
       // }catch(SocketTimeoutException e) {
        //  System.out.println("The Socket has been timed out");

        } catch (IOException e) {
            System.out.println("Client Error: " + e.getMessage());

        }
    }
    
}   

server code

    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.io.PrintWriter;
    import java.net.ServerSocket;
    import java.net.Socket;
import java.util.Vector;

    public class EchoMultiThreadServer {
        private static Vector<Echoer> clients = new Vector<Echoer>();
        public static void main(String [] args) {
            try(ServerSocket serverSocket = new ServerSocket(4000)){
                while(true) {                                           
                        
                         Socket socket = serverSocket.accept();
                         Echoer echoer = new Echoer(socket);
                         echoer.start(); 
                         clients.add(echoer);

                        } 
                    }catch(IOException e) {
                        System.out.println("Server Exception"+e.getMessage());
                }
                
        }
}

thread code

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

public class Echoer extends Thread{
    
    private Socket socket;
    public Echoer(Socket socket) {
        this.socket = socket;
    }
    
    @Override
    public void run() {
        try {
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            PrintWriter wr = new PrintWriter(socket.getOutputStream(), true);
            
            while(true) {
                
                String echoString = in.readLine();
                System.out.println("Received Client Input: " + echoString);
                if(echoString.equals("exit")) {
                    break;
                }

                wr.println(echoString);
            }
        }catch(IOException e) {
            System.out.println("Oooops " + e.getMessage());
        }finally {
            try {
                socket.close();
            }catch(IOException e) {
                // later
            }
            
        }
        
    }

}

Solution

  • I can see two problems with your current logic:

    1. At the client side, you are essentially reading user input, then sending to server and getting a (single) response. So the problem here is that you only get one response, while you should take more than one for each user input line: that is the user's input plus the other users' input. Since you don't know when and how many the other users' inputs are going to be, you need to go asynchronous. I mean that you need 2 threads: one for reading user input and the other for reading server input/response (note: we are still at the client side). Since you already have one of the 2 threads, ie the one which runs the main method, then you can use it instead of creating a new one.
    2. At the server side, your Echoer is reading user input but only sending it back to the same client. You need for example a loop to send the client's input to all other clients too.

    So what would seem to me a proper logic is:

    Client side:

    Reading server's responses thread logic:

    forever, do:
        get server's message.
        print server's message to user.
    

    main method:

    connect to server.
    start a "Reading server's responses thread".
    get user input.
    while the user's input it not "exit", do:
        send user's input to server.
        get user input.
    disconnect from server.
    

    Server side:

    Echoer thread:

    forever, do:
        read client's message.
        for every client, do:
            send the message to the client.
    

    main method:

    start server socket.
    forever, do:
        accept incoming connection.
        start an Echoer thread for the accepted connection.
    

    There are some missing bits though, such as how to maintain the list of all clients, but for that I can see you are already using a Vector<Echoer> clients at the server side. So just pass that Vector to every Echoer you create, so they can do the broadcasting of each incomming message. Important note here: at the server side, you have more than one threads: the main one and each Echoer, so make sure you synchronize on the Vector while you are modifying it at the main thread and also while broadcasting at the Echoers.


    Notes:

    1. I am assuming in all the above logic that there is no particular order in which the clients send their messages. For example if always client A sent first, then client B and so on, and the whole process was repeating, then you wouldn't need to go multithreading at all.
    2. Please take your time. First implement it and then tell me if you encouter any problems.

    Edit 1: full sample code.

    Client code:

    import java.io.BufferedReader;
    import java.io.BufferedWriter;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.io.OutputStream;
    import java.io.OutputStreamWriter;
    import java.net.Socket;
    import java.nio.charset.StandardCharsets;
    import java.util.Scanner;
    
    public class Client {
        
        //This is the "Reading server's responses thread" I am talking about in the answer.
        private static class ReadingRunnable implements Runnable {
            
            private final BufferedReader serverInput;
            
            public ReadingRunnable(final InputStream is) {
                serverInput = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
            }
            
            @Override
            public void run() {
                try {
                    //While the server is not disconnected, we print each line to 'System.out':
                    for (String line = serverInput.readLine(); line != null; line = serverInput.readLine())
                        System.out.println(line);
                }
                catch (final IOException iox) {
                    iox.printStackTrace(System.out);
                }
                finally {
                    System.out.println("Input from server stopped.");
                }
            }
        }
        
        public static void main(final String[] args) {
            try {
                System.out.print("Connecting... ");
                try (final Socket sck = new Socket("localhost", 50505);
                     final OutputStream os = sck.getOutputStream();
                     final InputStream is = sck.getInputStream()) {
                    System.out.println("Connected.");
                    new Thread(new ReadingRunnable(is)).start();
                    final BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8));
                    final Scanner scan = new Scanner(System.in);
                    for (String userInput = scan.nextLine(); !"exit".equalsIgnoreCase(userInput); userInput = scan.nextLine()) {
                        bw.write(userInput);
                        bw.newLine();
                        bw.flush();
                    }
                }
            }
            catch (final IOException iox) {
                iox.printStackTrace(System.out);
            }
            finally {
                System.out.println("Output from user stopped.");
            }
        }
    }
    

    Server code:

    import java.io.BufferedReader;
    import java.io.BufferedWriter;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.io.OutputStream;
    import java.io.OutputStreamWriter;
    import java.net.ServerSocket;
    import java.net.Socket;
    import java.nio.charset.StandardCharsets;
    import java.util.ArrayList;
    import java.util.Objects;
    
    public class Server {
        
        private static class Echoer implements Runnable {
            private final ArrayList<Echoer> all;
            private final BufferedWriter bw;
            private final BufferedReader br;
            
            public Echoer(final ArrayList<Echoer> all,
                          final InputStream is,
                          final OutputStream os) {
                this.all = Objects.requireNonNull(all);
                bw = new BufferedWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8));
                br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
            }
            
            //Instead of exposing 'bw' via a getter, I just built a helper method to send a message to the Echoer:
            public void send(final String msg) throws IOException {
                bw.write(msg);
                bw.newLine();
                bw.flush();
            }
            
            @Override
            public void run() {
                try {
                    for (String line = br.readLine(); line != null; line = br.readLine()) {
                        System.out.println(line); //Print the received line at the server.
                        synchronized (all) { //We are reading from a collection which may be modified at the same time by another (the main) Thread, so we need to synchronize.
                            //Broadcast the received line:
                            for (int i = all.size() - 1; i >= 0; --i) {
                                try {
                                    all.get(i).send(line);
                                }
                                catch (final IOException iox) {
                                    all.remove(i); //In case we cannot send to the client, disconnect him, ie remove him from the list in this simple case.
                                }
                            }
                        }
                    }
                }
                catch (final IOException iox) {
                }
                finally {
                    synchronized (all) {
                        all.remove(this); //Disconnect him, ie remove him from the list in this simple case.
                    }
                    System.out.println("Client disconnected.");
                }
            }
        }
        
        public static void main(final String[] args) throws IOException {
            System.out.print("Starting... ");
            try (final ServerSocket srv = new ServerSocket(50505)) {
                final ArrayList<Echoer> all = new ArrayList<>();
                System.out.println("Waiting for clients...");
                while (true) {
                    final Socket sck = srv.accept();
                    try {
                        final OutputStream os = sck.getOutputStream();
                        final InputStream is = sck.getInputStream();
                        final Echoer e = new Echoer(all, is, os); //Pass all the Echoers at the new one.
                        synchronized (all) { //We will write to a collection which may be accessed at the same time by another (an Echoer) Thread, so we need to synchronize.
                            all.add(e); //Update list of Echoers.
                        }
                        new Thread(e).start(); //Start serving Echoer.
                    }
                    catch (final IOException iox) {
                        System.out.println("Failed to open streams for a client.");
                    }
                }
            }
        }
    }