So far in Part IV, "Networking with Java," we have discovered how Java's URL, datagram, and socket classes can be used to perform some simple networking tasks. This chapter discusses the larger world of client/server computing, how Java fits into this world, and the future of Java related to the client/server model. We will also cover one last Java networking class, ServerSocket, that can be used to create sophisticated and robust server processes.
During the last few years, the term client/server has taken the computing industry by storm. In companies around the world, technical management has latched onto client/server as a revolutionary concept that will propel their businesses into the next century. The truth is, however, that the client/server model has existed in one form or another in computer science for several decades. It exists in the monolithic mainframe systems that have been criticized for not delivering functionality that they were never designed to provide-as well as in the applications and operating systems running on your desktop. Boiled down to its most basic definition, the term client/server refers to one process (a client) requesting a service from another process (a server). The client and server processes could be running within the same address space on the same system or on separate systems separated by thousands of miles.
The recent trend has been to distribute computing resources and responsibility across several platforms, each specializing in providing a certain service. Although computing systems are better leveraged and can take advantage of parallel processing in this model, it does not come without a price. Specifically, the expense and administrative effort involved in supporting these systems is immense. System architects and administrators are faced with several daunting challenges including interoperability, compatibility, and configuration management.
Java brings a refreshing option to the table. Because Java is platform-independent, many of the compatibility issues that have plagued traditional client applications are solved. Developers no longer have to maintain and compile separate versions of the client application, and users are not forced to use a standardized client configuration. In addition, all or part of Java's executable components can be distributed. Because Java bytecodes can be centrally located but still executed in the client's address space, administrators only have to update the bytecodes in one location instead of deploying a newly compiled client application to every workstation. Of course, traditionally compiled client applications can also be loaded from a server but at the cost of slower load-time and increased network traffic due to the size of their executable files. Java's bytecode files (.class files) are designed to be safely transported across a network, especially a wide area network where bandwidth may be limited. Chapter 20, "Keeping Out the Riff-Raff: JavaSecurity," discusses the security features of Java in detail.
Another advantage of using Java for client/server development is its capability to be seamlessly integrated with the World Wide Web. This opens up the opportunity to quickly leverage the multimedia features of the Web with the dynamic and active nature of Java.
Finally, there have been some recent initiatives that will allow Java to become a serious player in significant client/server development in the future. These developments, as well as plans for the future, are discussed at the end of this chapter.
In Chapter 18, "Networking with Datagrams and Sockets," you learned how to use the DatagramSocket class to originate and accept datagram-based network connections. You also saw how the Socket class can be used to initiate a socket or streaming connection. However, the Socket class cannot be used to listen for or accept connections originating from another host. That is the job of the ServerSocket class.
The role of ServerSocket is to listen for connection
requests on a specific port from other hosts on the network. Once
a connection is established, the accept() method of ServerSocket
will create or spin off a Socket object to interact with
the remote host. So, ServerSocket is a doorman of sorts.
It waits for people to knock on a particular door, opens the door,
and lets them in. As shown in the Internet dictionary example
to follow, this design is the key to developing a robust socket
server. Table 19.1 lists several of the public methods of the
ServerSocket class.
| Method | Description |
| ServerSocket(int) | Creates a server socket on the specified port. A default backlog of 50 is used. |
| ServerSocket(int, int) | Creates a server socket on the specified port with the specified backlog limit. |
| InetAddress getInetAddress() | Returns an InetAddress object representing the host to which this server socket is connected. |
| int getLocalPort() | Returns the port number on the local host for this server socket. |
| Socket accept() | Accepts a connection on the local port and returns a Socket object that can be used to communicate over that connection. |
| close() | Closes the server socket. |
| setSocketFactory, (SocketImplFactory) | Sets the system's socket factory that will be used to create all sockets. |
In order for a ServerSocket object to be created, it must be told what port to monitor. You can bind the server socket to an anonymous port by specifying a port of 0. Both public constructors require the port number as a parameter but the second constructor will also accept an integer representing the number of queued connection requests that the server socket will maintain before denying access. To extend the doorman analogy used above, this parameter is functionally equivalent to limiting the number of people that are allowed to stand in line waiting for the doorman to let them in. Once this backlog limit is reached, the server socket will cause the client socket to throw an IOException indicating that the connection request was rejected. The default connection backlog size is 50 requests.
When designing server programs, one of the primary goals is to process incoming requests as quickly and efficiently as possible. The last thing you want to do is force clients to wait in line or deny their requests because your server is not robust enough. At the same time, you do not want to limit the functionality of your server just because you cannot turn requests around fast enough. The following code fragment illustrates the challenges just described:
try
{
ServerSocket listen = new ServerSocket(1234); // Create server socket on
åport 1234.
while (true)
{
Socket socket = listen.accept(); // Wait here for the next connect
årequest.
// Client has connected so get stream objects to communicate with.
DataInputStream in = new DataInputStream(socket.getInputStream());
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
// Do some useful processing... meanwhile other clients are being queued.
}
}
catch (IOException e)
{
// Exception handling logic.
}
Once the server socket has been created, the logic enters a loop where it waits for a client to connect so it can service its request. The accept() method will not return until a client connects. When the server is done working with the client, the loop wraps around and waits for the next connection. If one or more clients are waiting in the queue, accept() will grab the next one in line and return a socket to it immediately. So it is clear that if the tasks that the server must perform for each client are significant and the server is repeatedly used by several clients, each client will spend a lot of time waiting for the server to accept each connection. In addition, if the nature of the connection to the client is interactive or the duration is controlled by the client, the server is essentially reduced to serving one client at a time. By making the server multithreaded, the following example shows how to design a robust server that solves these problems and is capable of handling multiple connections quickly and efficiently.
To illustrate a multithreaded Java server program, we will develop a simple client/server example that implements an Internet dictionary. The client can be run as an applet or application, as shown in Figures 19.1 and 19.2.
Figure 19.1 : Internet Dictionary client as an applet.
Figure 19.2 : Internet Dictionary client as an application.
The server program is a Java application that must be run on the same host as the client class files (as specified by the CODEBASE HTML tag) if the client is run as an applet. Because applications are not bound by the security constraints imposed on applets, the server can be run on any host if the client is run as an application.
A portion of the source code for the client is shown in Listing 19.1. The user interface classes created by the Java Resource Wizard are not shown here, but are included on the accompanying CD-ROM.
Listing 19.1. EX19A.java.
import java.applet.*;
import java.awt.*;
import java.net.*;
import java.io.*;
public class EX19A extends Applet
{
MainWinRes mainWin = null;
Socket socket = null;
static final int DICTIONARY_PORT = 2000;
String host;
public static void main(String args[])
{ // Program being run as an application.
EX19A applet = new EX19A();
// Setup application frame for main window and start applet.
Ex19aApplicationFrame frame = new Ex19aApplicationFrame(applet);
applet.mainWin = frame.resource;
applet.host = "199.42.65.2";
applet.start();
}
public void init()
{ // Program being run as applet.
mainWin = new MainWinRes(this);
mainWin.CreateControls();
mainWin.IDC_TITLE.setFont(new Font("Helvetica", Font.BOLD, 18));
// Use host that applet was served from.
host = getCodeBase().getHost();
}
public void start()
{ // Initialize screen controls.
mainWin.IDC_STATUS.setText("");
mainWin.IDC_RESULTS.setEditable(false);
}
public void stop()
{ // Close down socket if it's active.
if (socket != null)
{
try
socket.close();
catch (IOException e) {}
}
}
public boolean handleEvent(Event event)
{
boolean retval = false;
if (event.target == mainWin.IDC_LOOKUP)
{ // Make sure the user has entered something.
String word = mainWin.IDC_WORD.getText();
if (word.length() == 0)
mainWin.IDC_STATUS.setText("Please enter a word to lookup.");
else
lookupWord(word);
retval = true;
}
return retval;
}
// This method does the actual lookup of the word by sending a request
// across the network to the dictionary server.
protected void lookupWord(String word)
{
mainWin.IDC_RESULTS.setText("");
mainWin.IDC_STATUS.setText("Performing lookup...");
try
{ // Create socket and setup buffered input/output streams to communicate.
socket = new Socket(host, DICTIONARY_PORT);
DataInputStream in = new DataInputStream(new
åBufferedInputStream(socket.getInputStream()));
PrintStream out = new PrintStream(new
åBufferedOutputStream(socket.getOutputStream()), true);
mainWin.IDC_STATUS.setText("Sending word...");
out.println("LOOKUP=" + word); // Send lookup command to server.
String line;
mainWin.IDC_STATUS.setText("Waiting for response...");
line = in.readLine();
if (line.startsWith("DEFINITION="))
{ // A definition was returned.
// Strip off "DEFINITION=" response from first line of definition.
line = line.substring(line.indexOf('=') + 1);
// Read each line of definition from server.
do
mainWin.IDC_RESULTS.appendText(line + "\n");
while ((line = in.readLine()) != null);
mainWin.IDC_STATUS.setText("Response received. Closing
åconnection.");
}
else if (line.startsWith("ERROR="))
{ // An error occurred at the server.
// Strip off "ERROR=" response from first line of response.
String error = line.substring(line.indexOf('=') + 1);
if (error.equalsIgnoreCase("WordNotFound"))
mainWin.IDC_STATUS.setText("The word '" + word + "' not found.
åTry again.");
else if (error.equalsIgnoreCase("DictionaryFileError"))
mainWin.IDC_STATUS.setText("Dictionary not available. Try again
ålater.");
else
mainWin.IDC_STATUS.setText("Error looking up word.");
}
in.close();
out.close();
socket.close();
}
catch (UnknownHostException e1)
mainWin.IDC_STATUS.setText("Unable to resolve dictionary server's name.");
catch (IOException e2)
{
mainWin.IDC_STATUS.setText("Error opening connection to dictionary server.
Server may not be running.");
if (socket != null)
{ // Close down socket.
try
socket.close();
catch (IOException e) {}
}
}
socket = null;
}
}
// Frame used to wrap up main window when program is run as an application.
class Ex19aApplicationFrame extends Frame
{
MainWinRes resource;
MainMenuRes menu;
Applet owner;
public Ex19aApplicationFrame(Applet owner)
{
super("Internet Dictionary Client Application");
this.owner = owner;
// Since it's an application, give it a menu to allow user to exit.
menu = new MainMenuRes(this);
menu.CreateMenu();
setFont(new Font("Dialog", Font.PLAIN, 8));
resource = new MainWinRes(this);
resource.CreateControls();
resource.IDC_TITLE.setFont(new Font("Helvetica", Font.BOLD, 18));
setResizable(false); // Do not allow frame to be resized.
show();
}
public boolean handleEvent(Event event)
{ // Intercept frame messages and pass the others along to the applet.
if (event.target == menu.ID_FILEEXIT || event.id == Event.WINDOW_DESTROY)
{
dispose();
System.exit(0);
return true;
}
else
return owner.handleEvent(event);
}
}
When the user enters a word to look up and presses the Lookup Word... button, the handleEvent() method receives the event notification and calls the lookupWord() method. This is where all of the interaction with the server occurs.
Once the socket to the server has been opened, buffered input and output streams are created and the lookup request is sent to the server. The format of the lookup request is LOOKUP=word to lookup. Then the client waits for a response from the server. The response can take on two forms: DEFINITION=definition of word or ERROR=type of error. Because the definition can be made up of multiple lines, the program keeps reading lines from the input stream until the entire definition has been received. Each line is added to the results text area control as it is received.
On the other side of the connection, the server application shown in Figure 19.3 is responsible for looking up and returning definitions to all of the clients.
Figure 19.3 : Internet Dictionary server application.
The server is composed of two parts. The application itself contains the main loop that implements a server socket. As connections are accepted in the run() method, a Lookup object is created and is handed the socket that was returned by ServerSocket.accept(). Because the Lookup class runs in its own thread, the server thread is allowed to immediately return to accepting the next connection. With this design, clients are serviced as fast as possible regardless of the complexity of the service being provided by the server.
The source code for the server is shown in Listing 19.2. Once again, the user interface classes created by the Java Resource Wizard are not shown here but are included on the accompanying CD-ROM.
Listing 19.2. EX19B.java.
import java.awt.*;
import java.io.*;
import java.net.*;
public class EX19B extends Frame implements Runnable, LookupInterface
{
static final int DICTIONARY_PORT = 2000;
// Counts used to track server statistics.
int currentConnections = 0;
int totalConnections = 0;
int wordsFound = 0;
int wordsNotFound = 0;
// Interface objects.
MainMenuRes menu;
MainWindowRes mainWin;
// Server's primary thread and socket.
Thread running = null;
ServerSocket listen = null;
public static void main(String args[])
{
new EX19B("Internet Dictionary Server");
}
public EX19B(String caption)
{
super(caption);
// Create main menu built using Resource Wizard.
menu = new MainMenuRes(this);
menu.CreateMenu();
// Setup font to use.
setFont(new Font("Dialog", Font.PLAIN, 8));
mainWin = new MainWindowRes(this);
mainWin.CreateControls();
// Initialize main window fields.
mainWin.IDC_CURRENTCONNECTIONS.setText("0");
mainWin.IDC_TOTALCONNECTIONS.setText("0");
mainWin.IDC_WORDSFOUND.setText("0");
mainWin.IDC_WORDSNOTFOUND.setText("0");
setResizable(false); // Do not allow frame to be resized.
show();
// Start up thread to listen for clients.
running = new Thread(this);
running.start();
}
public boolean handleEvent(Event event)
{
boolean retval = false;
if (event.target == menu.ID_FILE_EXIT || event.id == Event.WINDOW_DESTROY)
{ // Kill server thread if running.
if (running != null)
running.stop();
dispose();
System.exit(0);
retval = true;
}
return retval;
}
public void run()
{
mainWin.IDC_ACTIVITY.addItem("Starting server socket...");
try
{ // Create primary thread.
listen = new ServerSocket(DICTIONARY_PORT);
while (true)
{ // Wait for connection requests and spin off Lookup threads
// to service each request.
Socket socket = listen.accept();
new Lookup(this, socket);
}
}
catch (IOException e)
mainWin.IDC_ACTIVITY.addItem("Server socket error: " + e);
// Close down the socket.
try
listen.close();
catch (IOException e) {}
listen = null;
}
// Called by each lookup thread to update server.
public synchronized void connectionUpdate(String event)
{
mainWin.IDC_ACTIVITY.addItem(event);
}
// Called by each lookup thread to indicate it has started.
public synchronized void connectionStart()
{
mainWin.IDC_CURRENTCONNECTIONS.setText(String.valueOf(++currentConnections));
mainWin.IDC_TOTALCONNECTIONS.setText(String.valueOf(++totalConnections));
}
// Called by each lookup thread to indicate it has stopped.
public synchronized void connectionStop()
{
mainWin.IDC_CURRENTCONNECTIONS.setText(String.valueOf(--currentConnections));
}
// Called by each lookup thread to indicate that the requested word has been found.
public synchronized void wordFound()
{
mainWin.IDC_WORDSFOUND.setText(String.valueOf(++wordsFound));
}
// Called by each lookup thread to indicate that the requested word has not been found.
public synchronized void wordNotFound()
{
mainWin.IDC_WORDSNOTFOUND.setText(String.valueOf(++wordsNotFound));
}
}
As shown in Listing 19.3, the Lookup class extends the Thread class to gain the capability to run in its own thread. The constructor simply saves references to the interface of the object that wants to be notified of lookup activity and the socket accepted by the server, and then starts the thread.
Listing 19.3. Lookup.java.
import java.io.*;
import java.net.*;
public class Lookup extends Thread
{
static final String DICTIONARY_FILE = "dictionary.dat";
// Interface used to udpate server with lookup events.
LookupInterface lookupUser;
// Socket accepted from server to communicate with client.
Socket socket;
// Input/output streams connected to socket.
DataInputStream in;
PrintStream out;
public Lookup(LookupInterface lookupUser, Socket socket)
{
this.lookupUser = lookupUser;
this.socket = socket;
start(); // Kick-off thread.
}
public void run()
{
lookupUser.connectionStart();
try
{ // Setup buffered streams to read/write over socket.
in = new DataInputStream(new
åBufferedInputStream(socket.getInputStream()));
out = new PrintStream(new BufferedOutputStream(socket.getOutputStream()),
åtrue);
String request;
while ((request = in.readLine()) != null)
{ // Read until a LOOKUP command is received.
if (request.startsWith("LOOKUP="))
{ // Lookup word and then drop out of thread.
wordSearch(request.substring(7));
break;
}
}
}
catch (IOException e)
lookupUser.connectionUpdate("I/O Error on socket: " + e);
// Close down the socket.
try
socket.close();
catch (IOException e) {}
lookupUser.connectionStop();
}
protected void wordSearch(String word)
{
try
{ // Create a buffered input stream to search dictionary file for word.
DataInputStream dis = new DataInputStream(new BufferedInputStream(new
åFileInputStream(DICTIONARY_FILE)));
String line;
boolean found = false;
while (!found && (line = dis.readLine()) != null)
{ // Keep reading until word is found or end of file is reached.
int idx;
if (line.startsWith("^") && (idx = line.indexOf('^', 1)) > 0)
{ // Format is "^Word^Definition...". Since definitions can occupy
// multiple lines in file, "^" indicator is used to start each
åword
// and definition sequence.
String toMatch = line.substring(1, idx);
if (word.equalsIgnoreCase(toMatch) && word.length() ==
åtoMatch.length())
{ // Build "DEFINITION" response to be sent back to client.
line = "DEFINITION=" + line.substring(idx + 1);
// Keep reading and sending lines until the end of file is
åreached
// or the next word in the file is reached.
do
out.println(line);
while ((line = dis.readLine()) != null &&
å!line.startsWith("^"));
found = true;
lookupUser.wordFound();
}
}
}
if (!found)
{ // Send "ERROR" response back to client indicating word was not
åfound.
out.println("ERROR=WordNotFound");
lookupUser.wordNotFound();
}
}
catch (FileNotFoundException fe)
{ // Update client and server.
out.println("ERROR=DictionaryFileError");
lookupUser.connectionUpdate("Dictionary file not found.");
}
catch (IOException e)
{ // Update client and server.
out.println("ERROR=DictionaryFileError");
lookupUser.connectionUpdate("I/O Error reading dictionary file.");
}
}
}
The run() method creates buffered input and output streams from the socket and waits for the LOOKUP command to arrive. This method could easily be enhanced to process lookup requests for multiple words and probably should employ a timer to drop the connection after some period of inactivity.
Once the lookup command is received, the wordSearch() method is called to actually look up the definition for the word. To keep the example simple, the dictionary is stored in a text file with just a few words that are sequentially searched by each Lookup object. Of course, if we were searching a real dictionary, this method would benefit from searching against some sort of indexed database of words and definitions.
As shown in Figure 19.3, the server displays statistics covering server activity. Because the bulk of the activity occurs in the Lookup class, the LookupInterface interface is used to communicate between each of the Lookup objects and the server. Using an interface as opposed to direct calls to the server de-couples the Lookup class from the object that wants to know about lookup activity. For example, the LookupInterface could have been implemented by a different class that wrote all activity to a logfile, without modifying the Lookup class. The LookupInterface interface is shown in Listing 19.4.
Listing 19.4. LookupInterface.java.
// Interface used allow lookup threads to update server of events.
public interface LookupInterface
{
public void connectionUpdate(String event);
public void connectionStart();
public void connectionStop();
public void wordFound();
public void wordNotFound();
}
Although the Internet dictionary example is simple in function, it illustrates how to design and develop robust server processes in Java. By taking advantage of Java's multithreading capability, the server is very fast, flexible, and efficient. However, programming client/server solutions at the socket level still requires a great deal of effort. Application-level protocols and states must be maintained and synchronized between processes, which defeats many of the advantages of the object-oriented aspects of Java. Fortunately, recent initiatives in the world of Java will allow Java programmers to take the next step in client/server development.
Since its initial release, Java has certainly had a significant impact on the Internet. Java applets are being deployed on more and more Web sites every day. The Internet phenomenon has also reached into private corporate intranets where Web servers and Java applets are being used not only for disseminating company information but also to assist in tasks such as administering and supporting networks and corporate applications. However, in order for Java to move from being used as a utility to supplement Web pages to the primary language used to create mission critical applications, it must garner the support of client/server platform vendors as well as be supplemented by class libraries and bridging technologies that have catapulted languages like C++ to the forefront of client/server development. Although there has never been a shortage of vendors jumping on the Java bandwagon, the tools and add-ons to Java are beginning to reach the market and make Java a player in significant client/server development. In addition, Sun has created a business unit called JavaSoft that is dedicated to developing applications, tools, and platforms to enhance Java as a programming language. A few of these enhancements are discussed below.
| NOTE |
Although the 1.0 release of Visual J++ does not support these initiatives out of the box, Microsoft has stated that it is committed to keeping Visual J++ current with the Java Developers Kit. Forthcoming compatibility with JDK 1.1 will then enable you to take advantage of these features. They are covered here to give you an idea of where Java client/server development is headed and hopefully to provide a preview of a subsequent release of Visual J++. |
One of Java's strengths is its capability to network. Combined with the fact that Java programs can be run on several platforms without source code modifications, it is no wonder that Java is rapidly becoming a popular choice for client/server development. The flexibility of Java is continuing to be enhanced. Whether you need to delve into socket-based programming or step up to interacting with distributed objects, Java provides the necessary solutions.