Client Server Chat Program

Assignment

Description

In an attempt to become the next dotcom quadrillionaire, you are inventing an awesome Java application that allows multiple users to do group messaging and private messaging.  (We’re pretty sure this hasn’t been invented yet.)

You’ll be writing a program, called Chatter. The Chatter is a client-server program with GUI. It will mainly consists of two parts:  the ChatterServer, the server and the ChatterClient, the client.

There is one instance of the Chatter server.  It will run on a particular port and receive TCP (“Stream”) connections from one or more Chatter clients.  If a message arrives from a Chatter client, the Chatter server should relay that message to all of the other Chatter clients.  (E.g., if Alice, Bob, and Charlie are using the service and Alice sends a message, the server should relay that message to Bob and Charlie.)

Users participate in the group discussion by running the Chatter client.  The client connects to a Chatter server.  When the user types a message, the Chatter client sends that message to the Chatter server, which in turn will relay it to the other clients.  When a message is received by the Chatter client (from the server), the client prints out the message.

All the chatter clients should have a user interface. A client should be able to display 1) the messages that the client sent, 2) any private message sent to the client by others, 3) any group messages, and 4) any message from the server. The GUI should also provide places (e.g. a text box) for a client to enter text, to exit the chat, and to use the mouse to select whom to have a private chat. The GUI should also display who are currently listed as the clients and a client can select whom to privately chat with from the list.

Command-line Usage and Basic Operation

The Chatter server, which should be called ChatterServer.java, takes a single command-line argument: the port to listen on.  For example, you would invoke it from the command-line as follows

javaChatterServer 9876

Note that ChatterServer must be started before any ChatterClient can join the group chat.

Chat Operations

It is expected that on each client GUI, the complete and current list of clients (and their nicknames) is displayed. A client can use the mouse to select from the list to point to any particular person for a private chat. If nobody is selected, then the client is chatting the entire group.

There should be a place to enter the text, and also some place to display the historical chat messages.

If a client would like to send the message msg directly to the user with nickname name; if no connected client has that nickname, then ignore the message.  No client who is not named “name” should receive the message.

If multiple clients have the same nickname, then they should all receive the message.

Example:

Cosc150Guru Hi there, how’s it going?

To create a nickname for you as a client, the Chatter program supports the following commands. We assume which are typed by users into the Chatter client textbox, are as follows:

  • /nick name

Sets my nickname to be name.  E.g., “/nick Cosc150Guru”.  The nick command may be used more than once, which updates the client’s nickname.  Nicknames must be single words and cannot contain spaces.

When a client would like to quit the Chatter program, the client should be able to ‘exit’.

Solution 

ChatterClient.java 

importjava.awt.BorderLayout;

importjava.awt.event.ActionEvent;

importjava.awt.event.ActionListener;

importjava.io.BufferedReader;

importjava.io.BufferedWriter;

importjava.io.IOException;

importjava.io.InputStreamReader;

importjava.io.OutputStreamWriter;

importjava.net.Socket;

importjava.util.ArrayList;

importjava.util.Collections;

importjava.util.List;

importjava.util.concurrent.BlockingQueue;

importjava.util.concurrent.LinkedBlockingQueue;

importjavax.swing.JButton;

importjavax.swing.JFrame;

importjavax.swing.JList;

importjavax.swing.JPanel;

importjavax.swing.JScrollPane;

importjavax.swing.JTextArea;

importjavax.swing.JTextField;

importjavax.swing.ListSelectionModel;

importjavax.swing.border.TitledBorder;

importjavax.swing.event.ListSelectionEvent;

importjavax.swing.event.ListSelectionListener;

/**

* The implementation of the chat client. To execute the client,

* use the following command in a new terminal. Replace the ip

* and port to the address and port of the chat server.

* For example, if the server is on localhost and listening on port 29876,

* use the following command:

*

*   javaChatterClientlocalhost 29876

*

*/

public class ChatterClient {

private final Socket socket;

private final BufferedReader reader;

private final BufferedWriter writer;

private final Thread recvThread;

private final Thread tranThread;

private final BlockingQueue<String> messages; // pending messages to send

private volatile booleanshouldShutdown;

private List<Client> clients; // the connected clients information

private List<String> history; // received messages

private GUI mainPanel;         // the main frame

publicChatterClient(final String ipaddr, int port) throws IOException {

// create a socket to connect with the server

this.socket = new Socket(ipaddr, port);

this.reader = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));

this.writer = new BufferedWriter(new OutputStreamWriter(this.socket.getOutputStream()));

this.clients = Collections.synchronizedList(new ArrayList<>());

this.history = Collections.synchronizedList(new ArrayList<>());

// create threads to receive and transmit messages from the server.

this.shouldShutdown = false;

this.messages = new LinkedBlockingQueue<>();

this.recvThread = new Thread(new Receiver());

this.tranThread = new Thread(new Sender());

}

public void run() {

// create the GUI frame and start the threads to transmit messages

this.mainPanel = new GUI();

finalJFrame frame = new JFrame();

frame.setTitle(“Chat”);

frame.setContentPane(this.mainPanel);

frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

this.recvThread.start();

this.tranThread.start();

frame.pack();

frame.setVisible(true);

}

private void updateUI() {

this.mainPanel.update();

}

private void receive(final String message) {

final String[] components = message.split(“\\s+”, 2);

if (components.length>= 2) {

final String source = components[0];

if (source.equalsIgnoreCase(“SERVER:”)) { // a command from server

this.handleCommand(components[1]);

return;

}

}

// add the message to the ui directly

this.history.add(message);

this.updateUI();

}

/**

* Handle a command sent directly by the server.

* A command can be used to notify the arrival of a new client,

* client changing nickname, and disconnection of a client.

* All list update is via `list` command.

*/

private void handleCommand(final String command) {

final String[] components = command.split(“\\s+”);

if (“list”.equalsIgnoreCase(components[0])) {

// update the friend list

this.clients.clear();

for (int i = 1; i + 1 <components.length; i += 2) {

finalint id = Integer.parseInt(components[i]);

final String nickname = components[i + 1];

this.clients.add(new Client(id, nickname));

}

updateUI();

}

}

// shutdown the client gracefully

private void shutdown() {

this.shouldShutdown = true;

try {

this.socket.close();

// wait until both threads have shutdown

if (Thread.currentThread() != this.recvThread) {

this.recvThread.interrupt();

this.recvThread.join();

}

if (Thread.currentThread() != this.tranThread) {

this.tranThread.interrupt();

this.tranThread.join();

}

} catch (final Exception e) {

// nothing needs to be done here

}

}

/**

* The implementation of the GUI of the chat client.

*/

private class GUI extends JPanel implements ActionListener, ListSelectionListener {

private static final long serialVersionUID = 1L;

private final JList<String>clientsList;

private final JTextAreahistoryPanel;

private final JTextFieldmessageField;

private final JButtonclearButton;

private final JButtonsendButton;

GUI() {

// initialize the gui components

this.clientsList = new JList<String>();

this.historyPanel = new JTextArea(30, 50);

this.messageField = new JTextField();

this.clearButton = new JButton(“Clear”);

this.sendButton = new JButton(“Send”);

this.clientsList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);

this.historyPanel.setEditable(false);

finalJScrollPane p1 = new JScrollPane(this.clientsList);

finalJScrollPane p2 = new JScrollPane(this.historyPanel);

finalJPanel p3 = new JPanel();

p1.setBorder(new TitledBorder(“Friends”));

p2.setBorder(new TitledBorder(“Messages”));

p3.setLayout(new BorderLayout());

p3.add(this.messageField, BorderLayout.CENTER);

p3.add(this.sendButton, BorderLayout.EAST);

this.setLayout(new BorderLayout());

this.add(p1, BorderLayout.WEST);

this.add(p2, BorderLayout.CENTER);

this.add(p3, BorderLayout.SOUTH);

this.clientsList.addListSelectionListener(this);

this.clearButton.addActionListener(this);

this.sendButton.addActionListener(this);

}

public void update() {

// update the friend list

synchronized(clients) {

final String[] friends = new String[clients.size()];

for (int i = 0; i <friends.length; i++) {

friends[i] = clients.get(i).getNickname();

}

this.clientsList.setListData(friends);

}

// update the message list

synchronized (history) {

finalStringBuildertextBuilder = new StringBuilder();

for (final String message : history) {

textBuilder.append(message).append(“\n”);

}

this.historyPanel.setText(textBuilder.toString());

}

}

@Override

public void actionPerformed(ActionEvent e) {

if (e.getSource() == this.sendButton) {

// send the text to the server if not empty

final String message = this.messageField.getText().trim();

if (message.length() > 0) {

messages.add(message); // add to the pending queue

}

this.messageField.setText(“”);

} else if (e.getSource() == this.clearButton) {

this.messageField.setText(“”);

}

}

@Override

public void valueChanged(ListSelectionEvent e) {

// update the current input text field to send

// private message to the selected client.

final String nickname = this.clientsList.getSelectedValue();

if (nickname != null) {

this.messageField.setText(“@” + nickname + ” “);

this.clientsList.clearSelection();

this.messageField.requestFocus(); // put focus on the text field

}

}

}

/**

* An internal class to store the id and the nickname of one client.

*/

private class Client {

private final int id; // reserved field

private String nickname;

Client(final int id, final String nickname) {

this.id = id;

this.nickname = nickname;

}

public String getNickname() {

returnthis.nickname;

}

}

/**

* A receiver thread receive messages from the server and process it.

*/

private class Receiver implements Runnable {

@Override

public void run() {

while (!shouldShutdown) {

try {

String line = reader.readLine();

if (line == null) {

throw new IOException(“the connection has shutdown.”);

}

receive(line);

} catch (final IOExceptionioe) {

shutdown();

}

}

}

}

/**

* A receiver thread receive messages from the

*

*/

private class Sender implements Runnable {

@Override

public void run() {

while (!shouldShutdown) {

try {

String message = messages.take();

writer.write(message);

writer.newLine();

writer.flush();

} catch (final InterruptedExceptionie) {

} catch (final IOExceptionioe) {

// cannot send message to the server.

// either the server is down or the network is breaking.

shutdown();

}

}

}

}

public static void main(String[] args) {

String ip;

int port;

try {

ip = args[0];

port = Integer.parseInt(args[1]);

} catch (final RuntimeException re) {

System.out.println(“Usage: java ChatterClientipaddr port”);

return;

}

// create and start the client

try {

finalChatterClient client = new ChatterClient(ip, port);

client.run();

} catch (final Exception e) {

e.printStackTrace();

}

}

} 

ChatterServer.java 

importjava.io.BufferedReader;

importjava.io.BufferedWriter;

importjava.io.IOException;

importjava.io.InputStreamReader;

importjava.io.OutputStreamWriter;

importjava.net.ServerSocket;

importjava.net.Socket;

importjava.util.concurrent.BlockingQueue;

importjava.util.concurrent.CopyOnWriteArrayList;

importjava.util.concurrent.LinkedBlockingQueue;

/**

* Implement the server for the chat program. The port of the server

* should be a number no less than 10000.

* To execute the program, use the following command:

*

*   javaChatterServer 29876

*

*/

public class ChatterServer {

private final ServerSocketss;

private final CopyOnWriteArrayList<Client> clients; // all the clients currently connected

privateintnextClient; // to create an ordered temporary name for new client.

publicChatterServer(final int port) throws IOException {

// create a server socket on the given port

this.ss = new ServerSocket(port);

this.ss.setReuseAddress(true);

this.clients = new CopyOnWriteArrayList<>();

}

/**

* This function accepts new clients and forwards messages between clients.

* Each client is handled by an individual thread.

*/

public void run() {

Socket socket;

while (true) {

try {

socket = ss.accept();

} catch (final IOException e) {

// cannot accept new client. The server has been shutdown

return;

}

// create a client instance to handle the new client.

final String id = “” + this.nextClient;

final String nickname = “anonymous-” + this.nextClient;

this.nextClient ++;

try {

final Client client = new Client(id, socket, nickname);

this.clients.add(client);

System.out.printf(“info: new client [%s]\n”, nickname);

this.broadcastList();

} catch (final IOException e) {

// cannot create the client. The client has disconnected immaturely.

// nothing needs to do here.

}

}

}

/**

* If message starts with ‘@’, the message is sent to specified clients.

* Otherwise, broadcast to all clients.

*/

private void receive(final Client client, final String message) {

System.out.printf(“receive [%s] %s\n”, client.getNickname(), message);

if (message.startsWith(“/”)) {

if (handleCommand(client, message)) {

return;

}

}

String target = null;

if (message.startsWith(“@”)) {

final String[] components = message.split(“\\s+”, 2);

target = components[0].substring(1); // ignore the heading @ in the nickname

}

final String content = client.getNickname() + “: ” + message;

send(target, null, content);

if (target != null && !target.equals(client.getNickname())) {

// forward the private message to the sender too

send(null, client.id, content);

}

}

/**

* Handle a command sent by the client. For example,

*   \nickname – to change the nickname (only one word in the nickname is supported).

*/

privatebooleanhandleCommand(final Client client, final String command) {

final String[] components = command.split(“\\s+”);

if (“/nick”.equalsIgnoreCase(components[0])) {

if (components.length>= 2) {

// update the nickname of the client, send notification to all clients.

final String nickname = components[1];

if (nickname.equalsIgnoreCase(“SERVER”)) {

System.out.printf(“error: refuse to change [%s] to [%s]\n”, client.nickname, nickname);

} else {

System.out.printf(“info: [%s] changed to [%s]\n”, client.nickname, nickname);

client.setNickname(nickname);

this.broadcastList();

}

}

return true;

}

return false;

}

/**

* Send the message to the client. If the nickname is null, broadcast

* the message to all clients. Otherwise, send message to the clients only

* with the specified nickname.

*/

private void send(final String target, final String id, final String message) {

System.out.printf(“send [%s] %s\n”, target, message);

for (final Client client : this.clients) {

if (id != null) { // match exact id of the client

if (client.id.equals(id)) {

client.sendMessage(message);

break;

}

} else if (target == null || target.equalsIgnoreCase(client.getNickname())) {

// match nickname of client

client.sendMessage(message);

}

}

}

/**

* Broadcast the current list of clients to all clients.

*/

private void broadcastList() {

finalStringBuilder builder = new StringBuilder(“SERVER: LIST”);

for (final Client client : this.clients) {

builder.append(” “).append(client.id).append(” ” ).append(client.getNickname());

}

send(null, null, builder.toString());

}

/**

* Close all clients and shutdown the server gracefully.

*/

public void shutdown() {

for (final Client client : this.clients) {

client.shutdown();

}

}

/**

* An internal class to represent each client, which utilizes two threads to

* receive and send messages for the client.

* Messages must be separated by line separators.

*/

private class Client {

private final String id; // the unique id for the client

private String nickname;

private final Socket client;

private final Thread recvThread;

private final Thread tranThread;

private final BlockingQueue<String> messages; // the messages pending to send to clients

private volatile booleanshouldShutdown; // should this worker shutdown.

Client(final String id, final Socket client, String nickname) throws IOException {

this.id = id;

this.nickname = nickname;

this.client = client;

// create the queue to contain the messages to send to the client

this.messages = new LinkedBlockingQueue<>();

// use reader/writer to communicate with the client.

finalBufferedReader reader = new BufferedReader(new InputStreamReader(this.client.getInputStream()));

finalBufferedWriter writer = new BufferedWriter(new OutputStreamWriter(this.client.getOutputStream()));

// create the threads for the client

this.recvThread = new Thread(new Receiver(this, reader));

this.tranThread = new Thread(new Sender(this, writer));

this.recvThread.start();

this.tranThread.start();

}

public void setNickname(final String name) {

this.nickname = name;

}

public String getNickname() {

returnthis.nickname;

}

public void sendMessage(final String message) {

this.messages.offer(message);

}

/**

* Get a message pending to send from the queue.

*/

public String getPendingMessage() {

while (!this.shouldShutdown) {

try {

returnthis.messages.take();

} catch (final InterruptedException e) {

}

}

return null;

}

/**

* Shut down the client gracefully.

*/

public void shutdown() {

if (clients.remove(this)) {

this.shouldShutdown = true;

try {

this.client.close();

// wait until both threads have shutdown

if (Thread.currentThread() != this.recvThread) {

this.recvThread.interrupt();

this.recvThread.join();

}

if (Thread.currentThread() != this.tranThread) {

this.tranThread.interrupt();

this.tranThread.join();

}

} catch (final Exception re) {

// simply ignore the exception while shutting down the client.

} finally {

// notify all the clients that this client has shutdown.

System.out.printf(“info: client [%s] terminated\n”, this.nickname);

broadcastList();

}

}

}

}

/**

* The receiver receives messages from the corresponding client.

*/

private class Receiver implements Runnable {

private final Client host;

private final BufferedReader reader;

Receiver(final Client host, final BufferedReader reader) {

this.host = host;

this.reader = reader;

}

@Override

public void run() {

while (!this.host.shouldShutdown) {

try {

String line = reader.readLine();

if (line == null) {

throw new IOException(“the connection has shutdown.”);

}

receive(this.host, line);

} catch (final IOExceptionie) {

// the client has disconnected, remove this worker from the server.

this.host.shutdown();

break;

}

}

}

}

/**

* The sender sends messages to the corresponding client.

*/

private class Sender implements Runnable {

private final Client host;

private final BufferedWriter writer;

Sender(final Client host, final BufferedWriter writer) {

this.host = host;

this.writer = writer;

}

@Override

public void run() {

while (!this.host.shouldShutdown) {

final String message = this.host.getPendingMessage();

if (message == null) { // should terminate

break;

}

try {

this.writer.write(message);

this.writer.newLine();

this.writer.flush();

} catch (final IOExceptionioe) {

// shutdown the client if it is disconnected.

this.host.shutdown();

break;

}

}

}

}

public static void main(String[] args) {

int port; // get the port number from the parameter

try {

port = Integer.parseInt(args[0]);

} catch (Exception e) {

System.out.println(“Usage: java ChatterServer [port]”);

return;

}

// construct and start a server on the given port

try {

ChatterServer server = new ChatterServer(port);

System.out.println(“The server is listening on port ” + port + “.”);

server.run();

server.shutdown();

} catch (final IOException e) {

e.printStackTrace();

}

System.out.println(“The server has been shutdown!”);

}

}