Lecture 13

In this lecture we will develop a simple chat server. This example is an illustration for using threads and interprocess communication. Each user communicates via a client program with a simple graphical user interface to a server application which receives all messages and redistributes them back to all clients.

Since in this lecture we don't want to be concerned with the user interface, the graphical part is abstracted away in a separated class called ClientUI. The source code for the client is attached to the lecture. Attached are also the final versions of the two communication classes: ClientHandler and Server. The goal of this lecture is to develop those step by step.

In order to run the graphical interface at all we need a skeleton implementation for the ClientHandler:

public class ClientHandler {
    public ClientHandler() {
    }

    public boolean connect(String host, int port)
    {
        return true;
    }

    public boolean disconnect()
    {
        return true;
    }

    public void send(String message) {
        received(message);
    }

    public void received(String message) {
    }
}

It has four methods:

Now we have to implement the communication between the client and server applications. We start by modifying the implementation for ClientHandler. Before the actual communication we have to establish a communication channel. The channel looks like a file with two ends. On the one end the client can print something by using the methods print and println, just like we did with ordinary files and with the screen. On the other end the server can read from the same file by using again the same methods that were used for files. The channel is also bidirectional, i.e. the client can also read what the server has written. The input and the output streams are connected in a single object of class Socket. Once we obtain the socket we can always get the input and the output stream. This is how we do it on the client side:

public boolean connect(String host, int port)
{
    try {
        clientSocket = new Socket(host, port);
        out = new PrintWriter(clientSocket.getOutputStream(), true);
        in  = new BufferedReader(new InputStreamReader(
                                               clientSocket.getInputStream()));
    } catch (UnknownHostException e) {
        System.err.println(e.getMessage());
        disconnect();
        return false;
    } catch (IOException e) {
        System.err.println(e.getMessage());
        disconnect();
        return false;
    }

    return true;
}

We must handle the exceptions UnknownHostException and IOException. In the example we just print the error message and we return false from the method. We must also call the method disconnect which ensures that all allocated resources are released. The implementation is simple - we just call the method close for the two streams and for the socket if they happen to be initialized:

public boolean disconnect()
{
    try {
        if (out != null)
            out.close();
        if (in != null)
            in.close();
        if (clientSocket != null)
            clientSocket.close();
    } catch (IOException e) {
        System.err.println(e.getMessage());
        return false;
    }

    return true;
}

Once we have got the 'in' and 'out' streams it is easy to start sending messages. We just have to call the method 'println' from the object 'out':

public void send(String message) {
    out.println(message);
}

Although the client now can send messages, still there is no one that listens to it. We need a server. Establishing the connection on the server is not much different. The only difference is that the server must first wait until there is a client to listen to. This happens with the class ServerSocket. We first create the server socket and after that we call the method accept. The method will wait until a client is connected and after that it will return an object of class Socket. All this happens in the main method of the server:

public class Server {
    BufferedReader in;
    PrintWriter out;
    Socket clientSocket;

    public Server() {
        in = null;
        out = null;
        clientSocket = null;
    }

    public boolean connect(Socket socket) {
        try {
            in = new BufferedReader(
                        new InputStreamReader(socket.getInputStream()));
            out = new PrintWriter(socket.getOutputStream(), true);
            clientSocket = socket;
        } catch (IOException e) {
                return false;
        }
 
        return true;
    }

    public void run() {
        try {
            for (;;) {
                String inputLine = in.readLine();
                if (inputLine == null)
                    break;

                out.println(inputLine);
            }

            in.close();
            out.close();
            clientSocket.close();
        } catch (IOException e) {
            System.err.println(e.getMessage());
        }
    }

    public static void main(String[] args) {
        if (args.length != 1) {
            System.out.println("Usage: Server <port>");
            return;
        }

        // Establish connection
        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(Integer.parseInt(args[0]));
        } catch (IOException e) {
            System.err.println(e.getMessage());
            return;
        }

        try {
            // Wait for connection
            Socket clientSocket = serverSocket.accept();
 
            // read messages and send them back
            Server server = new Server();
            server.connect(clientSocket);
            server.run();
        } catch (IOException e) {
            System.err.println(e.getMessage());
            return;
        }
    }
}

The socket object that 'accept' returns represents the other end of the communication channel. Similarly with the client side, we create one 'in' and one 'out' stream on the server. We do this in the constructor for the server. After we have established the connection, we call the method run which in a loop reads messages from the client and sends them back to the same client. Later we will extend this in a way which allows for more than one client for the same server. In this case the server must send the message back not only to the same client but to all other clients as well.

Now we need to update the client to be able to receive messages. When we have many clients, we don't know at which point a new message will come. We must be ready at any time to receive new messages, even if the user is currently busy to write his own message. The easiest way to do this is in a thread. We declare that 'ClientHandler' implements the interface 'Runnable' and we add the method run:

public void run() {
    while (!Thread.interrupted()) {
        try {
            String message = in.readLine();
            if (message == null)
                break;

            received(message);
        } catch (IOException e) {
            System.err.println(e);
        }
    }

    receiverThread = null;
    disconnect();
}

The method simply reads a message from the server and calls the method receive with it. We also have to change the method 'connect' where we actually start the thread. Similarly we change the method disconnect where we terminate the thread. Both changes can be found in the final version of the code in the attachment.

Now the client can send messages to the server which sends them back to the client. The client reads the returned message and shows it in the window. Although there is a lot happening in the background, from the user point of view nothing has changed. He just types something and after that it shows up in the upper part of the window. In order to make the chat useful we need a server that can handle more than one client. There are a few changes that are needed. First we need to call the accept method in a loop since after the first client is connected, the server must continue waiting for the next client. Since now the server will be busy waiting for new connections, we must run its dispatch loop, i.e. the method 'run' in a separate thread. There is one thread for every client. Finally, We must broadcast every message to all connected clients. This is achieved by adding one static variable:

private static ArrayList outs = new ArrayList();

The static variable contains the list of all output streams for all clients. Whenever a thread receives a message, it broadcasts it by writing it to all output streams for all clients. The required changes are again in the attached files.

You can also find the attached .jar file which contains the compiled code for the client and for the server. This is the easiest way to test the chat. You can run the client and the server in the following ways:

java -classpath chat.jar Server 4444
java -classpath chat.jar ClientUI localhost 4444

Exercises