ChatServ.java

 

Download ChatServ.java source file

// The "Coffee Talk" server, also known as "Java chat" server.
// This program is a chat server program that shows multi-threaded
// network application.  Each time a chat "client" tries to connect
// to this chat server, a separate thread is created to handle
// just that connection.  The design of each thread is simple:
// Wait for incoming data, then broadcast it to all connected
// clients, then wait for more incoming data.  The tricky parts
// are to allow users to "login" (or attach) to the server, to logout
// (or hang up, or quit the chat session), and to switch chat rooms
// (a feature I plan to add later, which will change the protocol).
// Another future addition is a MaxConnections to limit the chat server
// to a reasonable number of simultaneous clients.
//
// The protocol I invented between the server and the client is as follows:
//             CLIENT                             SERVER
//   opens a socket connection
//   to server
//                                     returns "Connected to JavaChat\nUser: "
//   sends name (e.g. "Joe")
//                                     returns "NAME: Joe" (Maybe "Joe2" or "Joe3")
//                                     (If name is missing it is set to "Anonymous")
//                                     broadcasts "NEW: Joe", waits
//   sends message ("MSG: Hi world")
//                                     broadcasts "FROM: Joe\t Hi World", waits
//   sends message ("WHO:")
//                                     returns "USERS: Joe\tWayne\t ..."
//   ...
//
//   sends message "BYE:" (or disconnect)
//                                     broadcasts "EXIT: Joe", closes connection
//
// Note that the client code is not included here.  It is expected that the
// client program will also be multi-threaded, and support this same protocol.
//
// There is also a timeout feature to close a socket after 5 minutes.  This time
// is short for demonstration purposes.  A Windows bug prevents sockets from
// closing completely, so after awhile the chat server will run out of memory
// and probably crash.
//
// This program was inspired by "The Chatter System", a similar program developed
// by Judy Bishop in her fine book "Java Gently", 2nd Ed.  Addison-Wesley 1998,
// ISBN 0-201-34297-9.  Pages 417-421.
//
// (C) 1999 Wayne Pollock, Tampa Florida USA.  All Rights Reserved.

import java.io.*;
import java.net.*;
import java.util.*;  // for Vector and StringTokenizer;

public class ChatServ
{
   private static Vector clientSocketList = new Vector();
   private static Vector clientNameList = new Vector();
   private static final int defaultPort = 8181; // Any unused int > 1024 is OK.

   public static void main (String[] args)
   {
      int port = 8181;  // Note: 0 would mean to use the first available port.
      if ( args.length > 0 )  // Assume first arg is port number to use.
      {  try
         {  port = Integer.parseInt( args[0] );
         }  catch (Exception e) { port = defaultPort; }
      }

      try
      {  ServerSocket listener = new ServerSocket( port );
         listener.setSoTimeout( 0 );  // Disable timeout.
         // A bug prevents getInetAddr() from working on Windows; use
         // "winipcfg.exe" from Start-->Run... to determine IP addr of server.
         System.out.println( "Chat server initialized and listening on port "
                           + port + ", at: " + listener.getInetAddress() );
         System.out.println( "Hit CONTROL+C to stop server." );

         // Wait for some chat client to connect:
         for (;;)    // Repeat forever until program is killed.
         {
            Socket clientSocket = listener.accept();     // Wait here.
            addSock( clientSocket );
            new ChatHandler( clientSocket ).start();     // Start new thread.
        }
      }
      catch ( IOException ioe )
      {  System.err.println( "Chat Server initialization at port " + port
                             + " failed: " + ioe );
         System.exit( -1 );
      }
   }


   // broadcast sends a message from a user to every chat client:

   static synchronized void broadcast ( String msgHeader, String user, String msg )
   {
      Socket s;
      PrintWriter p;
      for ( int i = 0; i < clientSocketList.size(); ++i )
      {
         s = (Socket) clientSocketList.elementAt( i );
         try
         {  p = new PrintWriter( s.getOutputStream(), true ); // true: auto-flush
            p.println( msgHeader + user + "\t" + msg );
         } catch ( IOException ignored ) { }
      }
   }


   // remove deletes the Socket from the Vector clientSocketList.  Note
   // that this can be slow for Vectors, and probably shouldn't be used
   // in a synchronized method.  In Java 1.2 there is a java.util.List
   // class, which would be a better choice for this project:

   static synchronized void remove ( Socket sock, String clientName )
   {
      clientSocketList.removeElement( sock );
      if ( clientName != null )
          clientNameList.removeElement( clientName );
   }


   static synchronized void addSock ( Socket sock )
   {  clientSocketList.addElement( sock );  }


   static synchronized String addName ( String userName )
   {
      String users = ChatServ.listUsers().toLowerCase();

      int n = 0;
      while ( users.indexOf( userName.toLowerCase() ) > -1 )
      {  ++n;
      userName = userName + n;
      }

      clientNameList.addElement( userName );
      return userName;
   }


   static synchronized String listUsers ()
   {
      Enumeration e = clientNameList.elements();
      StringBuffer users = new StringBuffer();
      while ( e.hasMoreElements() )
         users.append( (String) e.nextElement() ).append( '\t' );
      return users.toString().trim();  // remove trailing tab.
   }
}


// One ChatHandler object is created per client, each its own thread, so
// the instance data is separate for each thread.  Using static variables
// here would cause all threads to share the same variables!
// Each client implements the server side of the chat protocol.
// The Thread exits when we exit the run() method.

class ChatHandler extends Thread
{
   private static final int timeoutInterval = 1000 * 60 * 5;  // 5 minutes
   private BufferedReader in;
   private PrintWriter out;
   private Socket client;
   private String userName;

   ChatHandler ( Socket sock )
   {  client = sock;  }


   public void run ()
   {
      try
      {
         in = new BufferedReader(
                 new InputStreamReader( client.getInputStream() ) );
         out = new PrintWriter( client.getOutputStream(), true );

         // First, complete the "hand-shake" to join the chat:

         out.println( "Connected to JavaChat" );
         out.print( "User: " );
         out.flush();
         userName = in.readLine();

         // Check for duplicate or missing user names,
         // create unique name if necessary (e.g. "Jane2"):

         StringTokenizer st = new StringTokenizer( userName );
         if ( ! st.hasMoreTokens() )
             userName = "Anonymous";
         else
            userName = st.nextToken();  // Strips the whitespace.

         userName = ChatServ.addName( userName );

         out.println( "NAME: " + userName );  // Tell client their name
         ChatServ.broadcast( "NEW: ", userName, "" );

         // All set!  Now we wait for an incoming message and respond
         // to it, according to our protocol.  But first, set a timeout
         // in case the user just walks away, so the connection can be closed:

         client.setSoTimeout( timeoutInterval );

         while ( true )  // an alternative to "for (;;)"
         {
            String packet = in.readLine();
            if ( packet.startsWith( "MSG: " ) )
               ChatServ.broadcast( "FROM: ", userName, packet.substring(5) );
            else if ( packet.startsWith( "BYE:" ) )
               break;
            else if ( packet.startsWith( "WHO:" ) )
            {  String users = ChatServ.listUsers();
               out.println( "USERS: " + users );
            }
            else
               ;  // do nothing, ignore unknown message types
         }
      }
      catch ( Exception ignored )  { }   // Get to here on error (or disconnect)

      // Clean up conversation:
      if ( userName != null && ! userName.equals( "Anonymous" ) )
         ChatServ.broadcast( "EXIT: ", userName, "" );
      ChatServ.remove( client, userName );
      try { client.close(); } catch ( IOException ignored ) { }
   }
}