import java.io.*; import java.net.*; import java.util.ArrayList; /** * A BuddyChatServer keeps a list of available BuddyChat users and makes that list * available to each user that has registered itself on the list. The list is a list * of users who are willing to "chat" with other users. Each user provides a port * on which it can be contacted; an ip address for the user is taken from the user's * connection to the server. The user also specifies a "handle", which is the name * under which the user will appear in the buddy lists. The server creates a "secret" * for each user, which is a password that must be sent by other users who wish to * connect to that user. When the user connects to the server, the server sends a * list of all users that are connected to the server. Whenever a user is added to * or deleted from the list, that change is sent to all connected users so that * they can keep their buddy lists updated. *

The protocol for establishing a connection from a client to the server is: * (1) server sends string "BuddyChatServer"; (2) client sends string "BuddyChatClient"; * (3) client sends its handle; (4) client sends its port; (5) server creates a * secret for the client and sends it to the client. All messages are terminated * by line feeds. *

Once a connection is established, the server sends a list of connected users * to the client. (This list does not inlude the client itself.) The format for * a list of users is (1) the word "clients" on a line by itself; (2) one line of infor * for each client; (3) the word "endclients" on a line by itself. The client * info consits of pieces of information: the client's handle, ip address, port * number, and secret. These are separated by "~" characters. There are no spaces * or "~" characters in the info (the user's handle is modified to make this true, * if necessary). *

The connection between server and client remains open until the client * closes it (or the server goes down). The client can send the following two * messages to the server: (1) "ping" (and the server responds by sending * "pingresponse" -- this is a way of checking that the connection is still in * place); (2) "refresh" (and the server responds by sending the complete list * of connected clients, omitting the client who sent the "refresh" command). * The server sends the following additional messages, with no response expected * from the client. (1) "ping"; (2) "addclient" followed on the next line by * the info string for the newly added client; (3) "removeclient" followed on * the next line by the info string of the client who has been removed. *

In addition to this, the server can be shut down gracefully by connecting * to the server, receciving the message "BuddyChatServer", sending "BuddyChatClient", * then sending the shutdown string as a message. The shutdown string is a * random-looking string that is not likely to be used as a handle. It is stored, * if possible, in a file named ".BuddyChatServer_shutdown_string_port" in the home * directory of the user who runs the server, where "port" is replaced by the * port number on which the server is listening. If it is not possilbe to save the * shutdown string, a default string is used. The file is deleted when the server * shuts down. *

The server listens for client connections on a default port number, but a * different listening port can be specified on the command line. */ public class BuddyChatServer { private static final int DEFAULT_PORT = 12001; private static final String DEFAULT_SHUTDOWN_MESSAGE ="skRl@(Gjfd908.89d&*hgfd"; private static String shutdownString; // The actual shutdown string. private static int listeningPort; private static ServerSocket listener; private static ClientList clients; // Connected clients; ClientList is a nested class. private volatile static boolean isShutDown; /** * Main routine starts a listener and listens for connections until the * server is shut down or an error occurs. * @param args an alternative listening port can be specified on the command line. */ public static void main(String[] args) { listeningPort = DEFAULT_PORT; if (args.length > 0) { try { int p = Integer.parseInt(args[0]); if (p <= 0 || p > 65535) throw new NumberFormatException(); listeningPort = p; } catch (NumberFormatException e) { } } try { listener = new ServerSocket(listeningPort); } catch (Exception e) { System.out.println("Can't create listening socket on port " + listeningPort); System.exit(1); } System.out.println("Listening on port " + listeningPort); clients = new ClientList(); getShutdownString(); try { while (true) { // Listen until error occurs or socket is closed. Socket socket = listener.accept(); clients.add( socket ); } } catch (Throwable e) { if (!isShutDown) { // Don't report an error after normal shutdown. System.out.println("Server closed with error:"); System.out.println(e); } } finally { System.out.println("Shutting down."); clients.shutDown(); } } /** * Tries to create a unique shutdown string and write it to a file, * so that it can be used by the BuddyChatServerShutdown program. */ private static void getShutdownString() { File file = new File(System.getProperty("user.home"), ".BuddyChatServer_shutdown_string_" + listeningPort); shutdownString = DEFAULT_SHUTDOWN_MESSAGE + Math.random(); try { if (file.createNewFile()) { file.deleteOnExit(); PrintWriter out = new PrintWriter(new FileWriter(file)); out.println(shutdownString); out.close(); } else { BufferedReader in = new BufferedReader(new FileReader(file)); String line = in.readLine(); if (line.startsWith(DEFAULT_SHUTDOWN_MESSAGE)) shutdownString = line; } } catch (Exception e) { shutdownString = DEFAULT_SHUTDOWN_MESSAGE; } } /** * Utility routine for converting the ip address of an InetAddress * to its usual string form. */ private static String convertAddress(InetAddress ip) { byte[] bytes = ip.getAddress(); if (bytes.length == 4) { String addr = "" + ( (int)bytes[0] & 0xFF ); for (int i = 1; i < 4; i++) addr += "." + ( (int)bytes[i] & 0xFF ); return addr; } else if (bytes.length == 16) { String[] hex = new String[16]; for (int i = 0; i < 16; i++) hex[i] = Integer.toHexString( (int)bytes[i] & 0xFF ); String addr = "" + hex[0] + hex[1]; for (int i = 2; i < 16; i += 2) addr += ":" + hex[i] + hex[i+1]; return addr; } else throw new IllegalArgumentException("Unknown IP address type"); } /** * A list of all the currently connected clients, with some routines for * adding and removing clients. The routines are synchronized, since * they can be called from multiple client threads. */ private static class ClientList { ArrayList clientList = new ArrayList(); // The clients. /** * Add a new client, already connected through the socket. No information * has been exchanged over the socket, so the identity of the client is * unknown. When the client thread has obtained client info, it will * call the announceConnection() method. Clients that are not yet fully * connected have c.info == null; such clients are not part of the client * lists that have been sent to other clients. */ synchronized void add(Socket socket) { Client c = new Client(socket); System.out.println("Client " + c.clientNumber + " created."); clientList.add(c); } /** * Remove a client. This is called by the client thread when the connection * to the client closes. If the client was fully connected (client.info != null), * then a report is sent to other clients. However, no messages are sent * if the server is shutting down. */ synchronized void remove(Client client) { System.out.println("Client " + client.clientNumber + " removed."); if (!isShutDown && clientList.remove(client) && client.info != null) { for (Client c : clientList) c.clientRemoved(client); } } /** * This is called when info about a client has been obtained. It sends * a report of the newly added client to all connected clients. */ synchronized void announceConnection(Client newlyConnectedClient) { System.out.println("Client " + newlyConnectedClient.clientNumber + " connection established with info " + newlyConnectedClient.info); for (Client c : clientList) c.clientAdded(newlyConnectedClient); } /** * Return a copy of the ArrayLisit of clients. (The copy won't be * modified asynchronously like the original list can be.) */ synchronized ArrayList copy() { ArrayList c = new ArrayList(); for (Client client : clientList) c.add(client); return c; } /** * This is called by the main program to shut down connections to all * the clients when the server is shutting down. This will allow all * the client threads to die, so that the program can end. */ synchronized void shutDown() { for (Client client : clientList) client.shutDown(); } } // end nested class ClientList /** * Each connected client is represented by an object of type Client. * Each client has a connected socket and TWO threads, one for reading from * the socket and one for writing to it. The writing thread is the * main thread, which is also responsible for setting up the connection. */ private static class Client { static int clientsCreated; // How many clients have been created. int clientNumber; // Clients are numbered 1, 2, ... as they are created. volatile String info; // Info string that contains all important info about // this client, of the form handle~ip~port~secret; // this is the info string that is sent to other clients. // It is constructed by the main client thread. String messageOut = ""; // Message waiting to be sent by writer thread. // Note that access to messageOut is synchronized on // this Client object. Whenever a message is added // to this string, notify() is used to wake the writer // thread so that it can send the message. It is // possible that several messages might be added to // the string before it gets sent. ClientThread clientThread; // Writer thread, also sets up connection. ReaderThread readerThread; String secret; Socket socket; volatile boolean connected; volatile boolean closed; /** * Construct a client; make its "secret" string, and create its main thread. */ Client(Socket socket) { clientsCreated++; clientNumber = clientsCreated; secret = clientNumber + "!" + Math.random(); this.socket = socket; clientThread = new ClientThread(); clientThread.start(); } /** * Called by the ClientList when a client is removed. * Sends an announce of the removed client to this client. */ void clientRemoved(Client c) { if (c != this) { send("removeclient\n" + c.info + '\n'); } } /** * Called by the ClientList when a new client (with its info string) * becomes available. Sends an announce of the new client to this client. */ void clientAdded(Client c) { if (c != this) { send("addclient\n" + c.info + '\n'); } } /** * Called by ClientList when the server is shutting down; * Closes this client's socket (which terminates the reader thread) * and wakes up the writer thread so it can terminate. */ synchronized void shutDown() { if (! closed) { closed = true; try { socket.close(); } catch (Exception e) { } synchronized(this) { notify(); // Notifies writer thread. } } } /** * Called when either the reader or writer thread shuts down. This * can happen because of an error or because the connection is closed * from the other side. (It will also be called during server shutdown.) * If connection is already closed, nothing is done. */ synchronized void close() { if (!closed) { closed = true; try { socket.close(); } catch (Exception e) { } notify(); clients.remove(this); } } /** * Schedule a message to be sent by the writer thread. This method does * NOT actually send the message, so it does not block. (Note: NO line * feed is added to the message!) */ synchronized void send(String message) { messageOut += message; // Add message onto the waiting outgoing string. notify(); // Wake up writer thread so it can send the message. } /** * Schedule the client list to be sent by the writer thread. This method * does not actually send the list, so it does not block. */ synchronized void sendClientList() { ArrayList c = clients.copy(); messageOut += "clients\n"; for (Client client : c) if (client != this) messageOut += client.info + '\n'; // Add info to outgoing string. messageOut += "endclients\n"; notify(); // Wake up writer thread so it can send the message. } /** * Defines the main client thread, which is responsible for setting up * the connection, starting the reader thread, and then writing all messages * to the client. */ class ClientThread extends Thread { public void run() { try { String ip = convertAddress(socket.getInetAddress()); PrintWriter out; BufferedReader in; out = new PrintWriter(socket.getOutputStream()); in = new BufferedReader(new InputStreamReader(socket.getInputStream())); out.println("BuddyChatServer"); out.flush(); if (out.checkError()) throw new Exception("Error while trying to send handshake to client"); String handshake = in.readLine(); if (! "BuddyChatClient".equals(handshake)) throw new Exception("Client did not properly identify itself."); String handle = in.readLine(); if (handle.equals(shutdownString)) { out.println("shutting down"); out.flush(); isShutDown = true; listener.close(); return; } handle = handle.replaceAll("~","-"); // Make sure handle contains no "~" String portString = in.readLine(); int port; try { port = Integer.parseInt(portString); } catch (NumberFormatException e) { throw new Exception("Did not receive port number from client."); } if (port <= 0 || port > 65535) throw new Exception("Illegal port number received from client."); out.println(secret); out.flush(); if (out.checkError()) throw new Exception("Error while sending initial data to client."); info = handle + "~" + ip + "~" + port + "~" + secret; info = info.replaceAll(" ","_"); // Make sure info contains no spaces. connected = true; clients.announceConnection(Client.this); // Connection has been set up. readerThread = new ReaderThread(in); readerThread.start(); sendClientList(); // First message will be the client list. while (!closed && !isShutDown) { String messageToSend; synchronized(Client.this) { // Get the message; don't synchronize the actual send. messageToSend = messageOut; messageOut = ""; } if (closed || isShutDown) break; if (messageToSend.length() == 0) messageToSend = "ping\n"; // if there is no message, send a "ping" out.print(messageToSend); out.flush(); if (out.checkError()) throw new Exception("Error while sending to client."); synchronized(Client.this) { if (!closed && !isShutDown && messageOut.length() == 0) { try { // sleep for about 10 minutes or until notified of a new message. Client.this.wait(10*(50+(int)(15*Math.random()))*1000); } catch (InterruptedException e) { } } } } } catch (Exception e) { if (!closed && ! isShutDown) System.out.println("Client " + clientNumber + " error: " + e); } finally { close(); } } } /** * Defines a relatively simple thread that reads messages from the client * and responds to them. This will end when the client closes the connection, * or when the socket is closed on this side (for example, when server is shutting * down). */ class ReaderThread extends Thread { BufferedReader in; ReaderThread(BufferedReader in) { this.in = in; } public void run() { try { while (true) { String messageIn = in.readLine(); if (messageIn == null) break; // connection closed from other side else if (messageIn.equals("ping")) send("pingresponse\n"); else if (messageIn.equals("refresh")) sendClientList(); else throw new Exception("Illegal data received from client"); } } catch (Exception e) { if (!closed && !isShutDown) System.out.println("Client " + clientNumber + " error: " + e); } finally { close(); } } } } // end nested class Client }