Newsletter sign-up
View all newsletters

Enterprise Java Newsletter
Stay up to date on the latest tutorials and Java community news posted on JavaWorld

Sponsored Links

Optimize with a SATA RAID Storage Solution
Range of capacities as low as $1250 per TB. Ideal if you currently rely on servers/disks/JBODs

Building an Internet chat system

Multithreaded client/server chat -- the Java way

  • Print
  • Feedback

Page 6 of 7

  protected Socket s;
  protected DataInputStream i;
  protected DataOutputStream o;
  public ChatHandler (Socket s) throws IOException {
    this.s = s;
    i = new DataInputStream (new BufferedInputStream (s.getInputStream ()));
    o = new DataOutputStream (new BufferedOutputStream (s.getOutputStream ()));
  }


The constructor keeps a reference to the client's socket and opens an input and an output stream. Again, we use buffered data streams; these provide us with efficient I/O and methods to communicate high-level data types -- in this case, Strings.

protected static Vector handlers = new Vector ();
  public void run () {
    try {
      handlers.addElement (this);
      while (true) {
        String msg = i.readUTF ();
        broadcast (msg);
      }
    } catch (IOException ex) {
      ex.printStackTrace ();
    } finally {
      handlers.removeElement (this);
      try {
        s.close ();
      } catch (IOException ex) {
        ex.printStackTrace();
      }
    }
  }
  // protected static void broadcast (String message) ...


The run() method is where our thread enters. First we add our thread to the Vector of ChatHandlers handlers. The handlers Vector keeps a list of all of the current handlers. It is a static variable and so there is one instance of the Vector for the whole ChatHandler class and all of its instances. Thus, all ChatHandlers can access the list of current connections.

Note that it is very important for us to remove ourselves from this list afterward if our connection fails; otherwise, all other handlers will try to write to us when they broadcast information. This type of situation, where it is imperative that an action take place upon completion of a section of code, is a prime use of the try ... finally construct; we therefore perform all of our work within a try ... catch ... finally construct.

The body of this method receives messages from a client and rebroadcasts them to all other clients using the broadcast() method. When the loop exits, whether because of an exception reading from the client or because this thread is stopped, the finally clause is guaranteed to be executed. In this clause, we remove our thread from the list of handlers and close the socket.

protected static void broadcast (String message) {
    synchronized (handlers) {
      Enumeration e = handlers.elements ();
      while (e.hasMoreElements ()) {
        ChatHandler c = (ChatHandler) e.nextElement ();
        try {
          synchronized (c.o) {
            c.o.writeUTF (message);
          }
          c.o.flush ();
        } catch (IOException ex) {
          c.stop ();
        }
      }
    }
  }


This method broadcasts a message to all clients. We first synchronize on the list of handlers. We don't want people joining or leaving while we are looping, in case we try to broadcast to someone who no longer exists; this forces the clients to wait until we are done synchronizing. If the server must handle particularly heavy loads, then we might provide more fine-grained synchronization.

Within this synchronized block we get an Enumeration of the current handlers. The Enumeration class provides a convenient way to iterate through all of the elements of a Vector. Our loop simply writes the message to every element of the Enumeration. Note that if an exception occurs while writing to a ChatClient, then we call the client's stop() method; this stops the client's thread and therefore performs the appropriate cleanup, including removing the client from handlers.

Note that the writeUTF() method is not synchronized, so we must explicitly perform synchronization to prevent other threads from writing to the stream at the same time.

Wrapping up

Multithreading is essential for servers with any sophistication. In this article, we have developed a multithreaded broadcast chat server and a simple graphical client. We used data streams to read and write messages in UTF format. Although this certainly makes more sense than breaking everything down into bytes, there is one drawback to this implementation: We can communicate only single String messages between clients. If we wish to communicate different information, then we must change both the client and the server. This can be a serious limitation when we want the server to perform a more generalized purpose, or when we want to extend the system to handle more than just text.

A better solution
The solution to this problem is to develop an application-layer protocol that defines a more powerful level of dialog between a client and the server. Typically, such a protocol would encapsulate messages between the client and server by transmitting a header and a body with each message; the header identifies the type of the message and the length of the message body, and the body contains the actual message information. This concept is similar to the headers that can be attached to HTTP requests and responses.



An encapsulated message



By providing this meta-information, the client and server system can be extended easily. We can add new message types to the protocol without affecting existing code; if a client does not understand a message of a new type, it can simply discard the message. Furthermore, we can introduce much more powerful messages; a message of type MSG_TEXT may consist of simply a String, but a message of type MSG_SCRIBBLE may consist of a sequence of Points. If the application protocol is appropriately defined, then the server does not need to understand these messages; the header identifies the length of each message, and so the server can relay messages between clients without understanding the format of the message bodies.

Message headers also can be used for other purposes. For example, they can be used to identify a particular client to whom a message should be relayed; instead of a broadcast-only server, we can extend the server to provide unicast and multicast services. Headers also can identify control information coming back from the server, so the server can provide notification of events such as clients joining and leaving.

Implementation
When it comes to implementing such an application-layer protocol, there are two main options. The protocol can be supported explicitly by all applications, so that when a client is sending a message, it manually inserts the appropriate header information. An alternative, however, and much more elegant solution, is to abstract the protocol behind a set of stream classes. The specifics of header construction and insertion can be handled automatically by the stream classes, and the client is then left with much the same interface as before: Clients write messages to a stream, but instead of flushing the stream, they call a method that attaches appropriate headers and sends the encapsulated message.

  • Print
  • Feedback

Resources
  • Related books
  • TCP/IP & Related Protocols, Uyless Black, McGraw-Hill
  • Routing in the Internet, Christian Huitema, Prentice Hall
  • IPv6The New Internet Protocol, Christian Huitema, Prentice Hall
  • Applied Cryptography, Bruce Schneier, John Wiley & Sons