Write your own threaded discussion forum: The communications and server components, part 2

Learn how to implement a simple communications protocol to get our forum discussion group up and running

Before we get going on this month's task -- creating the client-side networking and building the server for our Interchange discussion forum -- let's quickly review the work we've completed on our forum at this point.

In last month's edition, we discussed the specifications of the Interchange client -- the applet parameters, the GUI, the modes of operation, and the user's identity -- and worked through the ForumLauncher and Forum classes to implement them. ForumLauncher displays the icon that stores the client app, while Forum presents the GUI and contains the majority of the app's logic. If you haven't already done so, click on the icon at the top of this article to see the discussion forum applet in motion.

I told you we were only going to do a brief review. If you're feeling a bit behind the eight ball on these topics, I recommend that you review last month's installment and brush up before running headlong in to this month's article.

Oh, and if it's Java code you want, you'll find the full source for the forum here.

Developing lines of communication

As we saw last month, our system is going to follow a simple communications protocol to read and post topics. This simple protocol moves thread listings from server to client and articles from client to server (and vice versa). The networking classes (ForumComm, ForumConnectionHandler, and ForumServer), which we'll cover in detail shortly, will implement the following actions:

  • "Load all threads" retrieves all current threads from the server.
  • "Load all articles in thread T" retrieves all the articles in thread T from the server.
  • "Post article A to thread T" posts article A to the server under thread T.

The client drives the server's actions with requests. The client will likely make very few requests over the course of its life; users typically spend the majority of session time reading articles and composing posts. For this reason, as well as the fact that requests are all discreet in nature (as opposed to transmission of real-time data, for example), each client request is serviced in a single connection, which is then closed down. The other alternative -- leaving a connection open between each client and the server for some specified period of time -- would waste precious server resources.

Cutting through the static: Client-side communications

The communications code for the client is bundled up nicely in one class, ForumComm. This class, shown in Listing 1, is a communications library that implements client-side networking calls.

import java.net.*;
import java.util.*;
import java.io.*;
import ForumLauncher;
public class ForumComm {
  // port for server to listen for Forum clients
  static final int FORUM_PORT = 5000;
  // possible client requests
  static final int LOAD_ALL_THREADS = 1;
  static final int LOAD_THREAD_ARTICLES = 2;
  static final int POST_ARTICLE = 3;
  ForumLauncher gp;
  public ForumComm (ForumLauncher gparent) {
    gp = gparent;
  }
Listing 1: The ForumComm class

The top section of the code defines the TCP port used for the Interchange service, as well as the protocol requests that the client can send to the server. These definitions have identical counterpart definitions in the server's ForumConnectionHandler class. The constructor does nothing except provide a pointer to the ForumLauncher applet to allow access to its getCodeBase() method.

Each instance of ForumComm provides three methods: loadAllThreads(), loadThreadArticles(), and postArticle(), shown in Listings 2, 3, and 4. Let's take a look at each of these methods in more detail.

  Hashtable loadAllThreads() {
    Hashtable a = new Hashtable();
    String thread = "";
    try {
      URL serverURL = gp.getCodeBase();
      Socket server = new Socket (serverURL.getHost(), FORUM_PORT);
      InputStream in = new BufferedInputStream (server.getInputStream());
      OutputStream out = new BufferedOutputStream (server.getOutputStream());
      DataInputStream dIn = new DataInputStream (in);
      DataOutputStream dOut = new DataOutputStream (out);
      dOut.writeInt (LOAD_ALL_THREADS);
      dOut.flush();
      thread = dIn.readUTF();
      while (!thread.equals ("")) {
    a.put (thread, new Vector()); 
    thread = dIn.readUTF();
      }
    } catch (IOException ex) {
      System.out.println ("Error reading threads from server.");
    }     
    return a;
  }
Listing 2: The loadAllThreads() method

The loadAllThreads() method returns a Hashtable, which contains the server's discussion topics as keys, each with an empty Vector as its value.

The method first creates an empty Hashtable and tries to set up a Socket to the server's port FORUM_PORT. If the attempt succeeds, loadAllThreads gets the InputStream and OutputStream associated with the Socket and attaches a DataInputStream and DataOutputStream to the respective buffered I/O streams. Buffered I/O streams increase the efficiency of the networking calls.

The method next writes the LOAD_ALL_THREADS request to the DataOutputStream and flushes the stream to make sure that the request is sent immediately. When the server gets the LOAD_ALL_THREADS request, it replies with the discussion threads, ending with an empty string.

loadAllThreads() then does a dIn.readUTF() call for each thread the server sends, exiting when it finds an empty string. Each thread is sent in UTF format. UTF is the best way to communicate textual data between Java clients and servers because it preserves Unicode characters, allowing for the use of a wide range of non-ASCII character sets. Unfortunately, if the server is running under JDK 1.0.2, a bug will prevent it from handling anything but ASCII, Greek, Hebrew, and Arabic characters. (All is not lost, though: The release of JDK 1.1 should be available by the time you read this.)

When a new thread is read from the server, it is put into Hashtable a as a key for a new empty Vector. When all threads have been loaded, the method exits, returning the Hashtable containing the threads. If the loadAllThreads() method fails, the client prints out an error message to the system's Java console and returns an empty Hashtable.

Now let's examine the loadThreadArticles() method.

  Vector loadThreadArticles (String t) {
    Vector ta = new Vector();    
    String art = "";
    try {
      URL serverURL = gp.getCodeBase();
      Socket server = new Socket (serverURL.getHost(), FORUM_PORT);
      DataInputStream dIn = new DataInputStream (in);
      DataOutputStream dOut = new DataOutputStream (out);
      InputStream in = new BufferedInputStream (server.getInputStream());
      OutputStream out = new BufferedOutputStream (server.getOutputStream());
      dOut.writeInt (LOAD_THREAD_ARTICLES);
      dOut.writeUTF (t);
      dOut.flush();
      art = dIn.readUTF();
      while (!art.equals ("")) {
    ta.addElement (art);
    art = dIn.readUTF();
      }
    } catch (IOException ex) {
      System.out.println ("Error reading articles for thread '" + t  + "' 
                           from server.");
    } 
    return ta;
  }
Listing 3: The loadThreadArticles() method

The loadThreadArticles() method creates Vector ta. This Vector stores all articles in the discussion thread as elements. The method then sets up the same data streams that loadAllThreads() uses.

Next, loadThreadArticles() writes the LOAD_THREAD_ARTICLES request to dOut, follows that with a write of the selected thread t to dOut, and flushes the stream to make sure that the request goes out immediately. The server replies with a series of articles.

The method then attempts to read each article as it is sent by the server and add it to ta. When this operation is complete, the method returns ta and exits. If the method fails, the client prints out an error message to the system's Java console and returns an empty Vector.

Finally, we come to postArticle().

  boolean postArticle (String art, String t) {
    try {
      URL serverURL = gp.getCodeBase();
      Socket server = new Socket (serverURL.getHost(), FORUM_PORT);
      InputStream in = new BufferedInputStream (server.getInputStream());
      OutputStream out = new BufferedOutputStream (server.getOutputStream());
      DataInputStream dIn = new DataInputStream (in);
      DataOutputStream dOut = new DataOutputStream (out);
      dOut.writeInt (POST_ARTICLE);
      dOut.writeUTF (t);
      dOut.writeUTF (art);
      dOut.flush();
      return true;
    } catch (IOException ex) {
      System.out.println ("Error posting article in thread '" + t  + "' 
                           to server.");
      return false;
    }
  }
}
Listing 4: The postArticle() method

This method attempts to post an article to the thread t. It performs the same setup as the previous methods and follows that step with a POST_ARTICLE request to the server. The method then sends the post thread t and the article to the server and flushes the stream.

If these operations are successful, the method exits returning true. If the operations fail for some reason, such as a bad network connection, the method exits returning false.

Each ForumComm method provides its own local copy of the streams that it uses. This prevents collisions if more than one method is called simultaneously. Such a problem might occur in a multithreaded version of the client, so I included it in case you choose to optimize the system in such a way. (A little planning in the beginning sure can help you out when you decide to enhance later on!)

That's it for the client side of things. Let's now turn our attention to the server portion of our threaded discussion forum. I'll provide a bit of background before we get into the actual classes that implement the server side of our system.

Making the connection: Creating the server for the forum system

The server we're constructing will include the following features:

  • Multithreaded connection-handling facility
  • Storage and retrieval of article database
  • Configuration via configuration file
  • Administrator shutdown control from a terminal (console window)
  • Connection and status logging to terminal

Note: For tips on using the server, including installation, configuration, and starting and stopping, see the the sidebar Setting up the Interchange forum server.

Our first step is dealing with the connection request made by the client.

Class ForumConnectionHandler

Each time ForumServer (we'll get to the ForumServer class later on in the article) accepts a new connection, the connection is handed off to a new instance of ForumConnectionHandler for processing. ForumConnectionHandler, which is shown in Listing 5, talks directly to ForumComm.

import java.net.*;
import java.util.*;
import java.io.*;
public class ForumConnectionHandler extends Thread {
  // possible client requests
  static final int LOAD_ALL_THREADS = 1;
  static final int LOAD_THREAD_ARTICLES = 2;
  static final int POST_ARTICLE = 3;
  long id;
  Socket client;
  long memoryLimit;
  Hashtable articles;
  InputStream in;
  OutputStream out;
  DataInputStream dIn;
  DataOutputStream dOut;
  public ForumConnectionHandler (long i, Socket c, long m, 
                                 Hashtable ar, ThreadGroup h) {
    super (h, "Forum Connection Handler " + i);
    id = i;
    client = c;
    memoryLimit = m;
    articles = ar;
  }
Listing 5: The ForumConnectionHandler class.

The ForumConnectionHandler class extends Thread so that each instance will map to a new thread and therefore handle its connection separately. Language features like this make writing a server in Java a real treat.

The setup code declares the requests that it may receive from the client, as well as the instance variables to be used in this connection. The constructor sets these to the appropriate values as specified by parameters from the caller.

Each ForumConnectionHandler has an id number, which is the number of the last connection plus one, receives pointers to the connection's Socket, a memory limit restriction, and a pointer to the articles database. ThreadGroup h is passed in, and the superclass (Thread) constructor is called with ThreadGroup h and a text description as arguments. This technique puts all handler threads in the same group so that they can be stopped easily when the server shuts down.

  public void run() {
    try {
      in = new BufferedInputSteam (client.getInputStream());    
      out = new BufferedOutputStream (client.getOutputStream());
      dIn = new DataInputStream (in);
      dOut = new DataOutputStream (out);
      String t, type = "";
      int request = -1;
      Vector threadArts;
      request = dIn.readInt();
      switch (request) {

The run() method of ForumConnectionHandler does all the work involved in processing the connection associated with it. The first order of business is to instantiate the streams (which are buffered for efficiency) to be used for the connection. After that, a String is set up for the thread name and a message to be printed, and a Vector is defined to hold articles in a discussion thread. An int called request is assigned the value of the request read from the client. As soon as the request comes in, a switch statement routes it to the proper case statement.

Let's take a look at the possible requests.

      case LOAD_ALL_THREADS:
    Enumeration en = articles.keys();
    while (en.hasMoreElements()) 
      dOut.writeUTF ((String) en.nextElement());
    dOut.writeUTF("");
    dOut.flush();
    type = "LOAD_ALL_THREADS";
    break;

If the request is a LOAD_ALL_THREADS, the code first creates an enumeration of all of the threads. It then loops to write each thread, ending with an empty string to signal the end of the transmission. The BufferedOutputStream dOut is flushed to make sure the data go out immediately. The type of the request is stored in the string type and the code breaks to the end of the switch.

      case LOAD_THREAD_ARTICLES: 
    t = dIn.readUTF();
    threadArts = (Vector) articles.get (t);
    if (threadArts != null && threadArts.size() > 0) {
      Enumeration en2 = threadArts.elements();
      while (en2.hasMoreElements())
        dOut.writeUTF ((String) en2.nextElement()); 
    }
    dOut.writeUTF ("");
    dOut.flush();
    type = "LOAD_THREAD_ARTICLES for thread " + t;
    break;

If the request is a LOAD_THREAD_ARTICLES, the code reads in the thread name into the string t and attempts to load threadArts from the article database. If the thread the client requested actually exists, the code loops, writing each article to the client. When the last article has been sent, or if the client's requested thread does not exist, the code writes back an empty String. The type of the request is stored in the string type and the code breaks to the end of the switch statement.

      case POST_ARTICLE:
    // ignores posts to threads that don't exist
    t = dIn.readUTF();
    String art = dIn.readUTF();
    threadArts = (Vector) articles.get (t);
    if (threadArts != null && memoryLimitCheck (art))
      threadArts.addElement (art);
    type = "POST_ARTICLE for thread " + t;
    break;

If the client request is a POST_ARTICLE, the code reads in the post thread and the article to post. It then tries to obtain a Vector from the articles database with the key t. If there is no entry, threadArts will be null. This check effectively ignores posts to threads that do not exist on the server. To enable clients to create their own threads, it is only necessary to modify this case to add unrecognized discussion threads to the database.

This code also performs a memory limit check to ensure that the addition of this article will not exceed some preset quota. type is set, and the code breaks to the end of the switch.

    
      default:
    type = "unknown request: " + request;
      } // end switch
      System.out.println ("#" + id + ": " + type + " from " 
                           + client.getInetAddress());

The default case is encountered if the client sends a request that is not defined in the connection handler. If this occurs, type is set to an appropriate error message.

No matter what request is processed, results of the request are printed to the console by the System.out.println() call, shown in this snippet:

    } catch (IOException ex) {
      System.out.println ("Client request processing failed for 
                           connection #" + id + ".");
    }
    try {
      client.close();
    } catch (IOException ex) {
      System.out.println ("Socket close for connection #" + id + " failed.");
    }
  }

The final try attempts to close the socket at the end of connection processing. If the attempt fails, try prints an error message. The run() method then exits.

The memoryLimitCheck() method shown in the next snippet is not currently implemented in this version of the connection handler. If you are interested in controlling the size of the articles database by checking it against memoryLimit before adding an article, you can implement the method.

  static boolean memoryLimitCheck (String art) {
    // checks attempted post to see if it will exceed memory
    return true;
  }

Class ForumServer

In general, servers use a ServerSocket to listen to a specified port for connection requests. When a connection request is made, a multithreaded server spawns a new thread containing a handler for the connection. The handler will deal with the connection and then exit when the connection closes or a fatal error occurs.

The ForumServer class preforms setup and provides a framework for the operation of threaded connection handlers. ForumServer also provides logging to the console window and allows the administrator to shut the server down by pressing the Return key while in the window.

Listing 6 contains ForumServer's class definition.

import java.net.*;
import java.util.*;
import java.io.*;
import ForumConnectionHandler;
public class ForumServer extends Thread {
  static final String DEFAULT_FORUM_CONFIGFILE = "forum.cfg";
  static final String CONFIGFILE_THREADS = "threads";
  static final String CONFIGFILE_DATABASE = "database";
  // port for server to listen for forum clients
  static final int FORUM_PORT = 5000;
  Hashtable articles;
  String databaseFilename;
  long connections = 0;
  long memoryLimit, diskLimit;
  Vector acl;
  ThreadGroup handlers;
  ServerSocket listener;
  String argv[];
  public ForumServer (String argv[]) {
    handlers = new ThreadGroup ("Forum Connection Handlers");
    articles = new Hashtable();
    this.argv = argv;
  }
}
Listing 6: The ForumServer class

ForumServer needs two threads: one to listen to the keyboard for the signal to shut down, and the other to listen to the network for connection requests on the TCP port FORUM_PORT. The main() method provides the keyboard listener and ForumServer extends Thread in order to use its own run() method as the network listener.

The setup code defines the default config file name and important config file keywords implemented in this version, as well as the TCP port to listen for connections. The memory copy of the article database, the total number of connections, a ThreadGroup to contain all of the connection handlers, and some other instance variables are declared as well.

The constructor is called by the main() method, which creates a new ThreadGroup for the connection handlers and instantiates the articles Hashtable. It then sets up this.argv to point to the command-line arguments that main() retrieved, for later use by the setup() method.

  public void run() {
    try {
      setup (argv);
      listener = new ServerSocket (FORUM_PORT);
      while (true) {
    Socket c = listener.accept();
    if (aclCheck (c)) {
      connections ++;
      ForumConnectionHandler h = new ForumConnectionHandler (connections, 
                                          c, memoryLimit, articles, handlers);
      h.start();
    } 
        else {
      // could send client a "denied" message here
      InetAddress denied = c.getInetAddress();
      System.out.println ("Denied connection from: " + denied + ".\n");
      try {
        c.close();
      } catch (IOException ex) {
        System.out.println ("Denied close failed: " + ex + ".\n");
      }
        }
      } 
    } catch (IOException ex) {
      // this is to ignore bug when closing down ServerSocket
      if (!(ex instanceof SocketException))
    ex.printStackTrace();
      }
    }
Listing 7: ForumServer's run() method

The run() method, shown in Listing 7, first handles all setup requirements with a call to the setup() method, passing in the command-line arguments from main(). If this succeeds without throwing an exception, listener is instantiated and while is put into a continuous loop to listen for connection requests.

When a connection occurs, it is checked to see if it is permitted by the access control list (ACL). If so, run() spawns a ForumConnectionHandler, hands the connection off to it, and starts its thread. If not, the connection is closed and a message is printed to the console.

The catch clause catches any IOException that may be thrown by the setup() method or the run() method, and prints its stack trace to the console. If the close() on a denied connection throws an IOException, it is dealt with separately to prevent it from breaking out of the while loop. It is worth noting that under JDK 1.0.2, when a ServerSocket is listening in a thread and the thread is stopped, a SocketException is thrown. The code in catch() ignores this exception.

      try {
    if (listener != null)
      listener.close();
      } catch (IOException ex) {
    System.out.println ("ServerSocket close failed.\n");
      }
      handlers.stop();
      try {
    if (databaseFilename != null && !databaseFilename.equals("")) {
      if (diskLimitCheck())
        dumpArticlesToDatabase (databaseFilename);
    }
    else
      System.out.println ("Article database not saved to disk.\n");
      } catch (IOException ex) {
    System.out.println ("Article database save failed: " + ex + ".\n");
      }
      System.out.println ("The server has shut down.");
    }
  }
Listing 8: Cleaning up with finally

When an exception is thrown or the ForumServer thread is stopped by the main() method, the finally clause is executed. The finally clause cleans up for the server process by attempting to close the ServerSocket, stopping the handlers, and dumping the articles database to the file system if appropriate.

  public static void main (String argv[]) throws IOException {
    ForumServer accept = new ForumServer (argv);
    accept.start();
    // listen for keypress
    while (true) {
      Thread.yield();
      if (System.in.available() != 0) {
    while (System.in.available() > 0)
      System.in.read();
    accept.stop();
      break;
      }
    }
  }
Listing 9: The ForumServer class

The ForumServer main() method, shown in Listing 9, creates an instance of ForumServer, which will itself listen for connection requests and thread off connection handlers. main() then listens to the keyboard in the while loop and exits when Return is pressed.

Disclaimer: Unfortunately, this process doesn't work too well under Windows (NT 4.0 and 95 tested). The main() method throws an exception when asked to listen to the keyboard. The server runs, but can't be shut down cleanly to make the database dump to the file system. One possible fix would be to write a control client that connects to the server to send it a control command, and set access restrictions so that the server authenticates the client.

The rest of the ForumServer methods are support functions. These methods throw exceptions which are then handled by the run() method. Let's take a look.

  void setup (String argv[]) throws IOException {
    // read from config file
    String configFileName = DEFAULT_FORUM_CONFIGFILE;
    if (argv.length > 0)
      configFileName = argv[0];    
    File configFile = new File (configFileName);
    if (! configFile.exists()) {
      throw new IOException ("Config file " + configFileName + " not
                              found.\nSpecify a an alternate config 
                              file or provide a file
                              named " + DEFAULT_FORUM_CONFIGFILE + ".\n");
    }
    Properties config = getConfig (configFile);
    // load in previous article database, ignoring old threads
    databaseFilename = config.getProperty (CONFIGFILE_DATABASE);
    String t = config.getProperty (CONFIGFILE_THREADS);
    articles = loadArticles (databaseFilename, t);
    // load in memoryLimit, diskLimit here
    // load in inbound connection ACL here
    // print config to console
    System.out.println ("FORUM SERVER CONFIGURATION\n");
    System.out.println ("Discussion threads:\n");
    Enumeration en = articles.keys();
    while (en.hasMoreElements())
      System.out.println (en.nextElement());
    System.out.println ("");
    System.out.println ("Database file: " + databaseFilename + "\n");
    // System.out.println ("Maximum disk usage: ");
    // System.out.println ("IP restrictions: ");
    // System.out.println ("Maximum memory usage: ");
    System.out.println ("Client connection listing:\n");
  }
Listing 10: ForumServer's setup() method

The setup() method performs all the server setup functions. As shown in Listing 10, the method first attempts to read from a config file. If this fails, setup() throws an exception. If the file exists, setup() calls getConfig() to get a Properties object containing the configuration parameters from the file.

If all of these operations complete successfully, setup() prints the server's configuration to the system console. If they fail, an IOException will be thrown and handled by run().

  Properties getConfig (File configFile) throws IOException {
    Properties props = new Properties();
    FileInputStream f = new FileInputStream (configFile);
    DataInputStream dIn = null;
    try {
      dIn = new DataInputStream (f);
      Properties props = new Properties();
      String line;
      int lineNo = 0;
      while ((line = dIn.readLine()) != null) {
        ++ lineNo;
        line = line.trim();
        if ((line.length() > 0) && (!line.startsWith ("#"))) {
          int idx = line.indexOf ("=");
          if (idx >= 0)
            props.put (line.substring (0, idx).trim(), line.substring 
                       (idx + 1).trim());
          else
            throw new IOException ("line " + lineNo + " not understood: 
                                   " + line + "\n");
        }
      }
    } catch (IOException ex) {
      throw new IOException ("Error reading " + configFile + ": " 
                              + ex.getMessage());
    } finally {
      if (dIn != null)
    dIn.close();
    }
    return props;
  }
Listing 11: ForumServer's getConfig() method

The getConfig() method lexes the file passed to it as a parameter, throwing exceptions if it encounters problems. This method is very similar to the load() method of Properties, except that it allows for a more natural config file format.

  boolean aclCheck (Socket c) {
    // unimplemented
    return true;
  }
  boolean diskLimitCheck() {
    // unimplemented
    return true;
  }
Listing 12: ForumServer's aclCheck() and diskLimitCheck() methods

The two unimplemented methods shown in Listing 12 check a connection to see whether it is permitted and check disk limitations before writing the article database to disk, respectively.

  Hashtable loadArticles (String dbfn, String t) throws IOException {
    Hashtable returnArticles = new Hashtable();
    if (t == null || t.equals("")) {
      throw new RuntimeException ("You must define at least one discussion 
                                   thread in the configuration file.\n");
    }
    else {
      StringTokenizer strtok = new StringTokenizer (t, ",");
      while (strtok.hasMoreTokens()) 
    returnArticles.put (strtok.nextToken(), new Vector());
    }
Listing 13: ForumServer's loadArticles() method

The loadArticles() method, which is used by setup() to load the article database, creates a Hashtable called returnArticles that will be returned when the method exits.

returnArticles is first loaded with the keys derived by tokenizing the discussion threads listing string t. Each discussion thread is put in as the key for an empty Vector.

    if (dbfn != null && !dbfn.equals("")) {
      File db = new File (dbfn);
      System.out.println ("Threads loaded from filesystem:\n");
      try {
    FileInputStream f = new FileInputStream (db);
    DataInputStream dIn = new DataInputStream (f);
    int numThreads = 0;
      numThreads = dIn.readInt();
      for (int i = 1; i <= numThreads; i++) {
        String thread = dIn.readUTF();
        Vector v = new Vector();
        int numArts = 0;
        numArts = dIn.readInt();
        for (int j = 1; j <= numArts; j++) {
          String art = dIn.readUTF();
          v.addElement (art);
        }
        if (returnArticles.get (thread) != null) {
          returnArticles.put (thread, v);
          System.out.println ("Thread '" + thread + "' picked 
                                   up from database file.\n");     
        }
        else 
          System.out.println ("Thread '" + thread + "' ignored.\n");
      }
      } catch (IOException ex) {
    System.out.println ("Exception '" + ex + "' encountered while 
                             attempting to load articles from 
                             database " + dbfn + ".\n");
      }
    }
    return returnArticles;
  }
Listing 14: ForumServer's loadArticles() method

If an article database file name has been passed in as a parameter, the method next tries to deserialize an articles database from the file, as shown in Listing 14. A correctly formatted database file begins with an int that tells the number of threads it contains. Each thread is then listed, followed by the number of articles that it has, followed by the articles themselves. Any IOException that arises during the operation is caught -- throwing it would cause the run() method to exit.

The for loop loads each thread from the file system database. When the thread has been loaded, it is checked against returnArticles, which already has the correct list of threads taken from the config file. Threads read in from the file system that are not defined in the config file are ignored.

  void dumpArticlesToDatabase (String dbfn) throws IOException {
    if (articlesIsEmpty (articles)) {
      System.out.println ("No articles to save to database file.\n");
      return;
    }
Listing 15: ForumServer's dumpArticlesToDatabase() method

Not surprisingly, the dumpArticlesToDatabase() method, shown in Listing 15, is more or less the mirror image of the loadArticles() method. I won't go into detail here.

    int numThreads = articles.size();
    Enumeration threads = articles.keys();
    File db = new File (dbfn);
    DataOutputStream dOut = null;
    try {
      FileOutputStream f = new FileOutputStream (db);
      dOut = new DataOutputStream (f);
     // record the number of threads
      dOut.writeInt (numThreads);
      // put each thread in, followed by its articles
      while (threads.hasMoreElements()) {
        String curThread = (String) threads.nextElement();
        dOut.writeUTF (curThread);
    Vector curThreadArts = (Vector) articles.get (curThread);
        dOut.writeInt (curThreadArts.size());
    Enumeration allArts = curThreadArts.elements();
    while (allArts.hasMoreElements()) {
      String art = (String) allArts.nextElement();
      dOut.writeUTF (art);
    }
      }
      System.out.println ("Article database written to " + dbfn + ".\n");
    }
    catch (IOException ex) {
      throw new IOException ("\nException " + ex + " encountered while 
                              attempting to write to file " + dbfn + ".\n");
    }
    finally {
      try {
      if (dOut != null)
    dOut.close();
      } catch (IOException ex) {
        throw new IOException ("Exception " + ex + " was encountered while 
                                attempting to close file " + dbfn + ".\n");
      }
    }
  }
Listing 16: Saving threads to a file

The code in Listing 16 simply loops through the threads listed in the articles database and attempts to save them to the file dbfn in the correct format.

  boolean articlesIsEmpty (Hashtable a) {
    boolean result = true;
    if (a == null || a.size() == 0) 
       return true;
    Enumeration en = a.keys();
    while (en.hasMoreElements()) {
      Vector threadArts = (Vector) a.get (en.nextElement());
      if (threadArts.size() > 0) {
    result = false;
    break;
      }
    }
    return result;
  }
Listing 17: Checking for articles

This last method, articlesIsEmpty(), is simply a handy check to see if an article database has any articles in any of its threads. The Hashtable size() method won't do for this because it counts only the number of keys.

Conclusion

Pat yourself on the back. You've just created a killer, albeit simple, discussion forum. The

ForumComm

,

ForumConnectionHandler

, and

ForumServer

classes we examined today provide a basic networking and server package for the Interchange discussion system. Of course, you don't need to stop here. Have some fun with this project. Spruce it up and make it your own. You can easily extend these basic classes to provide more functionality. For example, you can code

ForumComm

to be multithreaded so that posts will not block, which means that the client has to wait (meaning that the user also has to wait) on the completion of a post attempt to the server. Along these same lines, a more advanced enhancement would be to make the Forum client itself multithreaded so that I/O wouldn't block the client. You can further extend the system to process more types of requests by adding new

case

clauses to both

ForumComm

and

ForumConnectionHandler

. This approach will help you to overcome the biggest limitation of our little system -- the massive download of all of a thread's articles when the thread is selected.

Michael ShoffnerShoffner is VP of strategic development at Prominence Dot Com. He is co-author of Java Network Programming (Manning), and Java Applets and Channels without Programming, due out from Manning Publications in March of this year. In his free time, Michael likes to go to theaters to see Star Wars, picking up where he left off 17 years ago when it finally stopped showing the first time. Java Step By Step is a monthly column devoted to Java development. Each month, the authors of Step by Step will guide you through the development of a real-world Java application. Step By Step is co-authored by Michael Shoffner, Maria Winslow, and Merlin Hughes, founders of Prominence Dot Com, a Java development firm in Chapel Hill, NC.

Learn more about this topic

Previous Step By Step articles

Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more