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.
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:
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.
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;
}
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;
}
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;
}
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;
}
}
}
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.
The server we're constructing will include the following features:
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;
}
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.