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

1 2 3 Page 2
Page 2 of 3

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
1 2 3 Page 2
Page 2 of 3