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
}