TCP Chat


In this article we will build a simple TCP chat. The chat will have a server and a simple command line client. The protocol between them will be very simple: the client is expected to provide a nick upon connecting and after that send MSG commands specifying the other party’s nick and message text.

The article will be split as best as possible into smaller steps since the final example is rather long and would be hard to explain everything all at once.

Step 1 – The Client Reading Input

We will start by writing a bit of code for the chat client. We said the client will have a command line interface so we will need a way to read input from standard input. The code below reads from stdin and echos what was read.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* ChatClient.java */
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
 
class ChatClient {
    public static void main (String[] args) throws IOException {
        BufferedReader stdIn = new BufferedReader(
                new InputStreamReader(System.in));
        String msg;
 
        /* loop reading lines from stdin and output what was read in */
        while ((msg = stdIn.readLine()) != null) {
            System.out.println("You said: " + msg);
        }
    }
}

Step 2 – Connecting Client and Server

Next, we will write the foundation of our server. The server will use a ServerSocket to listen and accept client connections on a predefined port.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* ChatServer.java */
import java.net.ServerSocket;
import java.net.Socket;
 
import java.io.IOException;
 
class ChatServer {
    private static int port = 1001; /* port the server listens on */
 
    public static void main (String[] args) {
        ServerSocket server = null;
        try {
            server = new ServerSocket(port); /* start listening on the port */
        } catch (IOException e) {
            System.err.println("Could not listen on port: " + port);
            System.err.println(e);
            System.exit(1);
        }
 
        Socket client = null;
        try {
            client = server.accept();
        } catch (IOException e) {
            System.err.println("Accept failed.");
            System.err.println(e);
            System.exit(1);
        }
    }
}

The client attempts to connect to the server using a Socket. We’ve omitted the earlier code about reading in from stdin as it is irrelevant here. We’ll see it again later. Here is the code to open a socket given a port and a host name.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* ChatClient.java */
import java.net.Socket;
import java.net.UnknownHostException;
import java.io.IOException;
 
class ChatClient {
    private static int port = 1001; /* port to connect to */
    private static String host = "localhost"; /* host to connect to */
 
    public static void main (String[] args) {
 
        Socket server;
 
        try {
            /* try to open a socket to the server at the given host:port */
            server = new Socket(host, port); 
        } catch (UnknownHostException e) {
            System.err.println(e);
            System.exit(1);
        } catch (IOException e) {
            System.err.println(e);
            System.exit(1);
        }
    }
}

Note that since the server and client are being tested on the same machine the host is localhost. If your server is running on a different machine you should specify its name or IP address in the static String host field.

Step 3 – Basic Client-Server Communication

Now that we have seen how to read from stdin in the client and how to connect the client and server we are ready to send some messages from the client to the server.

The idea in this step is to combine step one and two. The client will read from stdin and use the opened socket to send what was read to the server. The server will read from the “client” socket and output to stdout.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/* ChatServer.java */
import java.net.ServerSocket;
import java.net.Socket;
 
import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
 
class ChatServer {
    private static int port = 1001; /* port the server listens on */
 
    public static void main (String[] args) throws IOException {
        ServerSocket server = null;
        try {
            server = new ServerSocket(port); /* start listening on the port */
        } catch (IOException e) {
            System.err.println("Could not listen on port: " + port);
            System.err.println(e);
            System.exit(1);
        }
 
        Socket client = null;
        try {
            client = server.accept();
        } catch (IOException e) {
            System.err.println("Accept failed.");
            System.err.println(e);
            System.exit(1);
        }
 
        /* obtain an input stream to the client */
        BufferedReader in = new BufferedReader(new InputStreamReader(
                    client.getInputStream()));
 
        String msg;
        /* loop reading lines from the client and display them */
        while ((msg = in.readLine()) != null) {
            System.out.println("Client says: " + msg);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/* ChatClient.java */
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.PrintWriter;
 
import java.net.Socket;
import java.net.UnknownHostException;
 
class ChatClient {
    private static int port = 1001; /* port to connect to */
    private static String host = "localhost"; /* host to connect to */
 
    public static void main (String[] args) throws IOException {
 
        Socket server;
        PrintWriter out = null;
 
        try {
            /* try to open a socket to the server at the given host:port */
            server = new Socket(host, port);
            /* obtain an output stream to the server */
            out = new PrintWriter(server.getOutputStream(), true);
        } catch (UnknownHostException e) {
            System.err.println(e);
            System.exit(1);
        }
 
        BufferedReader stdIn = new BufferedReader(
                new InputStreamReader(System.in));
        String msg;
 
        /* loop reading lines from stdin and output what was read 
         * to the server */
        while ((msg = stdIn.readLine()) != null) {
            out.println(msg);
        }
    }
}

Step 4 – Multiple Clients

So far we have the client sending some messages to the server but notice that the server is sitting in a loop reading from the socket. With that setup there is no way the server can handle more than one client. We alleviate this problem by introducing threads into the server.

The idea now is that the server’s main thread will loop on accepting incoming client connections and for each client connection will start a thread that will handle the communication with that client. We also send a message back to the client confirming the message was received.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/* ChatServer.java */
import java.net.ServerSocket;
import java.net.Socket;
 
import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
 
class ChatServer {
    private static int port = 1001; /* port to listen on */
 
    public static void main (String[] args) throws IOException {
 
        class ClientConn implements Runnable {
            private Socket client;
 
            ClientConn(Socket client) {
                this.client = client;
            }
 
            public void run() {
                BufferedReader in = null;
                PrintWriter out = null;
                try {
                    /* obtain an input stream to this client ... */
                    in = new BufferedReader(new InputStreamReader(
                                client.getInputStream()));
                    /* ... and an output stream to the same client */
                    out = new PrintWriter(client.getOutputStream(), true);
                } catch (IOException e) {
                    System.err.println(e);
                    return;
                }
 
                String msg;
                try {
                    /* loop reading messages from the client, 
                     * output to stdin and send back an "OK" back */
                    while ((msg = in.readLine()) != null) {
                        System.out.println("Client says: " + msg);
                        out.println("OK");
                    }
                } catch (IOException e) {
                    System.err.println(e);
                }
            }
        }
 
        ServerSocket server = null;
        try {
            server = new ServerSocket(port); /* start listening on the port */
        } catch (IOException e) {
            System.err.println("Could not listen on port: " + port);
            System.err.println(e);
            System.exit(1);
        }
 
        Socket client = null;
        while(true) {
            try {
                client = server.accept();
            } catch (IOException e) {
                System.err.println("Accept failed.");
                System.err.println(e);
                System.exit(1);
            }
            /* start a new thread to handle this client */
            Thread t = new Thread(new ClientConn(client));
            t.start();
        }
    }
}

The only change to our client is that is now capable of receiving messages from the server as well as sending messages.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/* ChatClient.java */
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.PrintWriter;
 
import java.net.Socket;
import java.net.UnknownHostException;
 
class ChatClient {
    private static int port = 1001; /* port to connect to */
    private static String host = "localhost"; /* host to connect to */
 
    public static void main (String[] args) throws IOException {
 
        Socket server = null;
 
        try {
            /* try to open a socket to the server at the given host:port */
            server = new Socket(host, port);
        } catch (UnknownHostException e) {
            System.err.println(e);
            System.exit(1);
        }
 
        /* obtain an output stream to the server... */
        PrintWriter out = new PrintWriter(server.getOutputStream(), true);
        /* ... and an input stream */
        BufferedReader in = new BufferedReader(new InputStreamReader(
                    server.getInputStream()));
        /* stdin stream */
        BufferedReader stdIn = new BufferedReader(
                new InputStreamReader(System.in));
 
        String msg;
 
        /* loop reading messages from stdin, send them to the server 
         * and read the server's response */
        while ((msg = stdIn.readLine()) != null) {
            out.println(msg);
            System.out.println(in.readLine());
        }
    }
}

Full Example

In the full-length example we add several pieces to make the overall program more robust. Let’s start by looking at the changes in the server.

The server now has a new class that handles the communication protocol between the server and a client. Basically, once we read a message from a client, we pass that to an instance our protocol handler (ChatServerProtocol) where it is processed and a reply for the client is generated. We take the response and pass it back to the client via the socket. The protocol itself is fairly simple and warrants little discussion here.

The only other noteworthy change is that we also maintain a table with user nicks and their corresponding sockets. When a message comes in from one of the clients saying that it wants to send a message to another nick (another client), we look up the socket for that nick and write the message to it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
/* ChatServer.java */
import java.net.ServerSocket;
import java.net.Socket;
 
import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
 
import java.util.Hashtable;
 
public class ChatServer {
    private static int port = 1001; /* port to listen on */
 
    public static void main (String[] args) throws IOException {
 
        ServerSocket server = null;
        try {
            server = new ServerSocket(port); /* start listening on the port */
        } catch (IOException e) {
            System.err.println("Could not listen on port: " + port);
            System.err.println(e);
            System.exit(1);
        }
 
        Socket client = null;
        while(true) {
            try {
                client = server.accept();
            } catch (IOException e) {
                System.err.println("Accept failed.");
                System.err.println(e);
                System.exit(1);
            }
            /* start a new thread to handle this client */
            Thread t = new Thread(new ClientConn(client));
            t.start();
        }
    }
}
 
class ChatServerProtocol {
    private String nick;
    private ClientConn conn;
 
    /* a hash table from user nicks to the corresponding connections */
    private static Hashtable<String, ClientConn> nicks = 
        new Hashtable<String, ClientConn>();
 
    private static final String msg_OK = "OK";
    private static final String msg_NICK_IN_USE = "NICK IN USE";
    private static final String msg_SPECIFY_NICK = "SPECIFY NICK";
    private static final String msg_INVALID = "INVALID COMMAND";
    private static final String msg_SEND_FAILED = "FAILED TO SEND";
 
    /**
     * Adds a nick to the hash table 
     * returns false if the nick is already in the table, true otherwise
     */
    private static boolean add_nick(String nick, ClientConn c) {
        if (nicks.containsKey(nick)) {
            return false;
        } else {
            nicks.put(nick, c);
            return true;
        }
    }
 
    public ChatServerProtocol(ClientConn c) {
        nick = null;
        conn = c;
    }
 
    private void log(String msg) {
        System.err.println(msg);
    }
 
    public boolean isAuthenticated() {
        return ! (nick == null);
    }
 
    /**
     * Implements the authentication protocol.
     * This consists of checking that the message starts with the NICK command
     * and that the nick following it is not already in use.
     * returns: 
     *  msg_OK if authenticated
     *  msg_NICK_IN_USE if the specified nick is already in use
     *  msg_SPECIFY_NICK if the message does not start with the NICK command 
     */
    private String authenticate(String msg) {
        if(msg.startsWith("NICK")) {
            String tryNick = msg.substring(5);
            if(add_nick(tryNick, this.conn)) {
                log("Nick " + tryNick + " joined.");
                this.nick = tryNick;
                return msg_OK;
            } else {
                return msg_NICK_IN_USE;
            }
        } else {
            return msg_SPECIFY_NICK;
        }
    }
 
    /**
     * Send a message to another user.
     * @recepient contains the recepient's nick
     * @msg contains the message to send
     * return true if the nick is registered in the hash, false otherwise
     */
    private boolean sendMsg(String recipient, String msg) {
        if (nicks.containsKey(recipient)) {
            ClientConn c = nicks.get(recipient);
            c.sendMsg(nick + ": " + msg);
            return true;
        } else {
            return false;
        }
    }
 
    /**
     * Process a message coming from the client
     */
    public String process(String msg) {
        if (!isAuthenticated()) 
            return authenticate(msg);
 
        String[] msg_parts = msg.split(" ", 3);
        String msg_type = msg_parts[0];
 
        if(msg_type.equals("MSG")) {
            if(msg_parts.length < 3) return msg_INVALID;
            if(sendMsg(msg_parts[1], msg_parts[2])) return msg_OK;
            else return msg_SEND_FAILED;
        } else {
            return msg_INVALID;
        }
    }
}
 
class ClientConn implements Runnable {
    private Socket client;
    private BufferedReader in = null;
    private PrintWriter out = null;
 
    ClientConn(Socket client) {
        this.client = client;
        try {
            /* obtain an input stream to this client ... */
            in = new BufferedReader(new InputStreamReader(
                        client.getInputStream()));
            /* ... and an output stream to the same client */
            out = new PrintWriter(client.getOutputStream(), true);
        } catch (IOException e) {
            System.err.println(e);
            return;
        }
    }
 
    public void run() {
        String msg, response;
        ChatServerProtocol protocol = new ChatServerProtocol(this);
        try {
            /* loop reading lines from the client which are processed 
             * according to our protocol and the resulting response is 
             * sent back to the client */
            while ((msg = in.readLine()) != null) {
                response = protocol.process(msg);
                out.println("SERVER: " + response);
            }
        } catch (IOException e) {
            System.err.println(e);
        }
    }
 
    public void sendMsg(String msg) {
        out.println(msg);
    }
}

In the client we’ve added a bit of code to ensure that the user enters a nick that the server will accept. We introduce threading in the client as well since we can no longer rely on messages coming in only as server responses; other clients can send us messages via the server asynchronously. The helper thread is responsible for reading all messages coming from the socket and displaying them on the screen.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
/* ChatClient.java */
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.PrintWriter;
 
import java.net.Socket;
import java.net.UnknownHostException;
 
public class ChatClient {
    private static int port = 1001; /* port to connect to */
    private static String host = "localhost"; /* host to connect to */
 
    private static BufferedReader stdIn;
 
    private static String nick;
 
    /**
     * Read in a nickname from stdin and attempt to authenticate with the 
     * server by sending a NICK command to @out. If the response from @in
     * is not equal to "OK" go bacl and read a nickname again
     */
    private static String getNick(BufferedReader in, 
                                  PrintWriter out) throws IOException {
        System.out.print("Enter your nick: ");
        String msg = stdIn.readLine();
        out.println("NICK " + msg);
        String serverResponse = in.readLine();
        if ("SERVER: OK".equals(serverResponse)) return msg;
        System.out.println(serverResponse);
        return getNick(in, out);
    }
 
    public static void main (String[] args) throws IOException {
 
        Socket server = null;
 
        try {
            server = new Socket(host, port);
        } catch (UnknownHostException e) {
            System.err.println(e);
            System.exit(1);
        }
 
        stdIn = new BufferedReader(new InputStreamReader(System.in));
 
        /* obtain an output stream to the server... */
        PrintWriter out = new PrintWriter(server.getOutputStream(), true);
        /* ... and an input stream */
        BufferedReader in = new BufferedReader(new InputStreamReader(
                    server.getInputStream()));
 
        nick = getNick(in, out);
 
        /* create a thread to asyncronously read messages from the server */
        ServerConn sc = new ServerConn(server);
        Thread t = new Thread(sc);
        t.start();
 
        String msg;
        /* loop reading messages from stdin and sending them to the server */
        while ((msg = stdIn.readLine()) != null) {
            out.println(msg);
        }
    }
}
 
class ServerConn implements Runnable {
    private BufferedReader in = null;
 
    public ServerConn(Socket server) throws IOException {
        /* obtain an input stream from the server */
        in = new BufferedReader(new InputStreamReader(
                    server.getInputStream()));
    }
 
    public void run() {
        String msg;
        try {
            /* loop reading messages from the server and show them 
             * on stdout */
            while ((msg = in.readLine()) != null) {
                System.out.println(msg);
            }
        } catch (IOException e) {
            System.err.println(e);
        }
    }
}

This concludes our TCP Client/Server chat example. We now have the complete code for a Java chat program. Possible further extensions include the addition of chat rooms, handling disconnects, nick registration and authentication, hardening the server against malicious clients and others. If you are interested in any of these extensions please let us know in the discussion thread for this article (link below) and we will do our best to provide it.