- Special Edition Using Java, 2nd Edition -

Chapter 23

TCP Sockets


by David W. Baker

Sockets are a programming abstraction that isolates your code from the low-level implementations of the TCP/IP protocol stack. TCP sockets enable you to quickly develop your own custom client-server applications. While the URL class described in Chapter 22 is very useful with well-established protocols, sockets allow you to develop your own modes of communication.

TCP Socket Basics

Sockets were originally developed at the University of California at Berkeley as a tool to easily accomplish network programming. Originally part of UNIX operating systems, the concept of sockets has been incorporated into a wide variety of operating environments, including Java.

What is a Socket?

A socket is a handle to a communications link over the network with another application. A TCP socket is one that utilizes the TCP protocol, inheriting the behavior of that transport protocol. Four pieces of information are needed to create a TCP socket:

Sockets are often used in client-server applications: A centralized service waits for various remote machines to request specific resources, handling each request as it arrives. In order for clients to know how to communicate with the server, standard application protocols are assigned well-known ports. On UNIX operating systems, ports below 1024 can only be bound by applications with super-user (for example, root) privileges, and thus for control, these well-known ports lie within this range, by convention. Some well known ports are shown in table 23.1.

Table 23.1 Well-known TCP ports and services

Port Service
21 FTP
23 Telnet
25 SMTP (Internet Mail Transfer)
79 Finger
80 HTTP

For many application protocols, you can merely use the Telnet application to connect to the service port and then manually emulate a client. This may help you understand how client-server communications work.
 

Client applications must also obtain, or bind, a port to establish a socket connection. Because the client initiates the communication with the server, such a port number could conveniently be assigned at runtime. Client applications are usually run by normal, unprivileged users on UNIX systems, and thus these ports are allocated from the range above 1024. This convention has held when migrated to other operating systems, and client applications are generally given a dynamically-allocated port above 1024.

Because no two applications can bind the same port on the same machine simultaneously, a socket uniquely identifies a communications link. Realize that a server may respond to two clients on the same port, since the clients will be on different systems and/or different ports; the uniqueness of the link's characteristics are preserved. Figure 23.1 illustrates this concept.


FIG. 23.1

Many clients can connect to a single server through separate sockets..

Figure 23.1 shows a server application responding to three sockets through port 80, the well-known port for HTTP. Two sockets are communicating with the same remote machine, while the third is to a separate system. Note the unique combination of the four TCP socket characteristics.

Java TCP Socket Classes

Java has a number of classes which allow you to create socket-based network applications. The two classes you use include java.net.Socket and java.net.ServerSocket.

The Socket class is used for normal two-way socket communications and has four constructors:

public Socket(String host, int port)
throws UnknownHostException, IOException;
public Socket(InetAddress address, int port)
throws IOException;
public Socket(String host, int port, boolean stream)
throws IOException;
public Socket(InetAddress address, int port,
boolean stream) throws IOException;

The first constructor allows you to create a socket by just specifying the domain name of the remote machine within a String instance and the remote port. The second enables you to create a socket with an InetAddress object. The third and fourth constructors are similar to the first two, except that they allow you to pass a boolean value indicating if a stream-based protocol such as TCP should be used to implement the socket. By default, TCP is used, but if passed a value of false, an unreliable datagram protocol, such as UDP, will be used.

See “Java UDP Classes,” Chapter 24 for more information.
 

An InetAddress is an object that stores an IP address of a remote system. It has no public constructor methods, but does have a number of static methods which return instances of InetAddress. Thus, InetAddress objects can be created through static method invocations:

try {
InetAddress remoteOP =
InetAddress.getByName("www.microsoft.com");
InetAddress[] allRemoteIPs =
InetAddress.getAllByName("www.microsoft.com");
InetAddress myIP = InetAddress.getLocalHost();
} catch(UnknownHostException excpt) {
System.err.println("Unknown host: " + excpt);
}

The first method returns an InetAddress object with an IP address for www.microsoft.com. The second obtains an array of InetAddress objects, one for each IP address mapped to www.microsoft.com. (Recall from Chapter 22 that the same domain name can correspond to several IP addresses.) The last InetAddress method creates an instance with the IP address of the local machine. All of these methods throw an UnknownHostException, which is caught in the previous example.

See “Internet Protocol (IP),” Chapter 22 for more information.
 

The Socket class has methods which allow you to read and write through the socket, the getInputStream() and getOutputStream() methods. To make applications simpler to design, the streams these methods return are usually decorated by another stream object, such as DataInputStream and PrintStream, respectively. Both getInputStream() and getOutputStream() throw an IOException, which should be caught.

try {
Socket netspace = new Socket("www.netspace.org",7);
DataInputStream input =
new DataInputStream(netspace.getInputStream());
PrintStream output =
new PrintStream(netspace.getOutputStream());
} catch(UnknownHostException expt) {
System.err.println("Unknown host: " + excpt);
System.exit(1);
} catch(IOException excpt) {
System.err.println("Failed I/O: " + excpt);
System.exit(1);
}

Now, in order to write a one line message and then read a one line response, you only need use the decorated stream:

output.println("test");
String testResponse = input.readLine();

Once you have completed communicating through the socket, you must first close the InputStream and OutputStream instances, and then close the socket.

output.close();
input.close();
netspace.close();

To create a TCP server, it is necessary to understand a new class, ServerSocket. ServerSocket allows you to bind a port and wait for clients to connect, setting up a complete Socket object at that time. ServerSocket has two constructors:

public ServerSocket(int port) throws IOException;
public ServerSocket(int port, int count)
throws IOException;

The first creates a listening socket at the port specified, allowing for the default number of 50 clients waiting in the connection queue. The second constructor enables you to change the length of the connection queue, allowing more or less clients to wait to be processed by the server.

After creating a ServerSocket, the accept() method can be used to wait for a client to connect. accept() blocks until a client connects, and then returns a Socket instance for communicating to the client. Blocking is a programming term which means that a routine enters an internal loop indefinitely, returning only when a specific condition occurs. The program's thread of execution does not proceed past the blocking routine until it returns, that is, when the specific condition happens.

The following code creates a ServerSocket at port 2222, accepts a connection, and then opens streams through which communication can take place once a client connects:

try {
ServerSocket server = new ServerSocket(2222);
Socket clientConn = server.accept();
DataInputStream input =
new DataInputStream(clientConn.getInputStream());
PrintStream output =
new PrintStream(clientConn.getOutputStream());
} catch(IOException excpt) {
System.err.println("Failed I/O: " + excpt);
System.exit(1);
}

After communications are complete with the client, the server must close the streams and then close the Socket instance, as previously described.

Creating a TCP Client-Server Application

Having understood the building blocks of TCP socket programming, the next challenge is to develop a practical application. To demonstrate this process, we will be creating a stock quote server and client: The client will contact the server and request stock information for a set of stock identifiers. The server will read data from a file, periodically checking to see if the file has been updated, and send the requested data to the client.

Designing an Application Protocol

Given the needs of our system, our protocol has six basic steps:

Client connects to server.

Server responds to client with a message indicating the currentness of the data.

The client requests data for a stock identifier.

The server responds.

Repeat steps 3–4 until the client ends the dialog.

Terminate the connection.

Implementing this design, we come up with a more detailed protocol. The server waits for the client on port 1701. When the client first connects, the server responds with:

+HELLO time-string

time-string indicates when the stock data to be returned was last updated. Next, the client sends a request for information. The server follows this by a response providing the data.

STOCK: stock-id
+stock-id stock-data

stock-id is a stock identifier consisting of a series of capital letters. stock-data is a string of characters detailing the performance of the particular stock. The client can request information on other stocks by repeating this request sequence.

Should the client send a request for information regarding a stock of which the server is unaware, the server responds with:

-ERR UNKNOWN STOCK ID

Should the client send an invalid command, the server responds with:

-ERR UNKNOWN COMMAND

When the client is done requesting information, it ends the communication and the server confirms the end of the session:

QUIT
+BYE

The below demonstrates a conversation using the below protocol. All server responses should be preceded by a "+" or "-" character, while the client requests should not. In this example, the client is requesting information on three stocks: ABC, XYZ, and AAM. The server has information only regarding the last two.

+HELLO Tue, Jul 16, 1996 09:15:13 PDT
STOCK: ABC
-ERR UNKNOWN STOCK ID
STOCK: XYZ
+XYZ Last: 20 7/8; Change -0 1/4; Volume 60,400
STOCK: AAM
+AAM Last 35; Change 0; Volume 2,500
QUIT
+BYE

Developing the Stock Client

The client application to implement the above protocol should be fairly simple. The code is shown in listing 23.1.

Listing 23.1 StockQuoteClient.java

import java.io.*; // Import the names of the packages
import java.net.*; // to be used.
/**
* This is an application which obtains stock information
* using our new application protocol.
* @author David W. Baker
* @version 1.1
*/
public class StockQuoteClient {
// The Stock Quote server listens at this port.
private static final int SERVER_PORT = 1701;
private String serverName;
private Socket quoteSocket = null;
private DataInputStream quoteReceive = null;
private PrintStream quoteSend = null;
private String[] stockIDs; // Array of requested IDs.
private String[] stockInfo; // Array of returned data.
private String currentAsOf = null; // Timestamp of data.
/**
* Start the application running, first checking the
* arguments, then instantiating a StockQuoteClient, and
* finally telling the instance to print out its data.
* @param args Arguments which should be <server> <stock ids>
*/
public static void main(String[] args) {
if (args.length < 2) {
System.out.println(
"Usage: StockQuoteClient <server> <stock ids>");
System.exit(1);
}
StockQuoteClient client = new StockQuoteClient(args);
client.printQuotes(System.out);
System.exit(0);
}
/**
* This constructor manages the retrieval of the
* stock information.
*/
public StockQuoteClient(String[] args) {
String serverInfo;
// Server name is the first argument.
serverName = args[0];
// Create arrays as long as arguments - 1.
stockIDs = new String[args.length-1];
stockInfo = new String[args.length-1];
// Copy in the arguments into are stockIDs[] array.
for (int index = 1; index < args.length; index++) {
stockIDs[index-1] = args[index];
}
// Contact the server and return the HELLO message.
serverInfo = contactServer();
// Parse our the timestamp.
if (serverInfo != null) {
currentAsOf = serverInfo.substring(
serverInfo.indexOf(" ")+1);
}
getQuotes(); // Go get the quotes.
quitServer(); // Close the communication.
}
/**
* Open the initial connection to the server.
* @return The initial connection response.
*/
protected String contactServer() {
String serverWelcome = null;
try {
// Open a socket to the server.
quoteSocket = new Socket(serverName,SERVER_PORT);
// Obtain decorated I/O streams.
quoteReceive = new DataInputStream(
quoteSocket.getInputStream());
quoteSend = new PrintStream(
quoteSocket.getOutputStream());
// Read the HELLO message.
serverWelcome = quoteReceive.readLine();
} catch (UnknownHostException excpt) {
System.err.println("Unknown host " + serverName +
": " + excpt);
} catch (IOException excpt) {
System.err.println("Failed I/O to " + serverName +
": " + excpt);
}
return serverWelcome; // Return the HELLO message.
}
/**
* This method asks for all of the stock info.
*/
protected void getQuotes() {
String response; // Hold the response to stock query.
// If the connection is still up.
if (connectOK()) {
try {
// Iterate through all of the stocks.
for (int index = 0; index < stockIDs.length;
index++) {
// Send query.
quoteSend.println("STOCK: "+stockIDs[index]);
// Read response.
response = quoteReceive.readLine();
// Parse out data.
stockInfo[index] = response.substring(
response.indexOf(" ")+1);
}
} catch (IOException excpt) {
System.err.println("Failed I/O to " + serverName
+ ": " + excpt);
}
}
}
/**
* This method disconnects from the server.
* @return The final message from the server.
*/
protected String quitServer() {
String serverBye = null; // BYE message.
try {
// If the connection is up, send a QUIT message
// and receive the BYE response.
if (connectOK()) {
quoteSend.println("QUIT");
serverBye = quoteReceive.readLine();
}
// Close the streams and the socket if the
// references are not null.
if (quoteSend != null) quoteSend.close();
if (quoteReceive != null) quoteReceive.close();
if (quoteSocket != null) quoteSocket.close();
} catch (IOException excpt) {
System.err.println("Failed I/O to server " +
serverName + ": " + excpt);
}
return serverBye; // The BYE message.
}
/**
* This method prints out a report on the various
* requested stocks.
* @param sendOutput Where to send output.
*/
public void printQuotes(PrintStream sendOutput) {
// Provided that we actually received a HELLO message:
if (currentAsOf != null) {
sendOutput.print("INFORMATION ON REQUESTED QUOTES"
+ "\n\tCurrent As Of: " + currentAsOf + "\n\n");
// Iterate through the array of stocks.
for (int index = 0; index < stockIDs.length;
index++) {
sendOutput.print(stockIDs[index] + ":");
if (stockInfo[index] != null)
sendOutput.println(" " + stockInfo[index]);
else sendOutput.println();
}
}
}

/**
* Conveniently determine if the socket and streams are
* not null.
* @return If the connection is OK.
*/
protected boolean connectOK() {
return (quoteSend != null && quoteReceive != null &&
quoteSocket != null);
}
}

The main() Method: Starting the Client

The main() method first checks to see that the application has been invoked with appropriate command line arguments, quitting if this is not the case. It then instanciates a StockQuoteClient with the args array reference and runs the printQuotes() method, telling the client to send its data to standard output.

The StockQuoteClient Constructor

The goal of the constructor is to initialize the data structures, connect to the server, load the stock data from the server, and terminate the connection. The constructor creates two arrays, one into which it copies the stock IDs and the other which remains uninitialized to hold the data for each stock.

It uses the contactServer() method to open communications with the server, returning the opening string. Provided that the connection opened properly, this string contains a timestamp indicating the currentness of the stock data. The constructor parses this string to isolate that timestamp, gets the stock data with the getQuotes() method, and then closes the connection with quitServer().

The contactServer() Method: Starting the Communication

Like the examples seen previously in this chapter, this method opens a socket to the server. It then creates two streams to communicate with the server. Finally, it receives the opening line from the server (for example, "+HELLO time-string") and returns that as a String.

The getQuotes() Method: Obtaining the Stock Data

This method performs the queries on each stock ID with which the application is invoked, now stored within the stockIDs array. First it calls a short method, connectOK(), which merely ensures that the Socket and streams are not null. It iterates through the stockIDs array, sending each in a request to the server. It reads each response, parsing out the stock data from the line returned. It stores the stock data as a separate element in the stockInfo array. Once it has requested information on each stock, the getQuotes() method returns.

The quitServer() Method: Ending the Connection

This method ends the communication with the server, first sending a QUIT message if the connection is still valid. Then it performs the essential steps when terminating a socket communication: close the streams and then close the Socket.

The printQuotes() Method: Displaying the Stock Quotes

Given a PrintStream object, such as System.out, the method prints the stock data. It iterates through the array of stock identifiers, stockIDs, and then prints the value in the corresponding stockInfo array.

Developing the Stock Quote Server

The server application is a bit more complex than the client that requests its services. It actually consists of two classes. The first loads the stock data and waits for incoming client connections. When a client does connect, it creates an instance of another class that implements the Runnable interface, passing it the newly created Socket to the client.

This secondary object, a handler, is run in its own thread of execution. This allows the server to loop back and accept more clients, rather than performing the communications with clients one at a time. The handler is the object which performs the actual communication with the client.

This is a common network server design—using a multi-threaded server to allow many client connects to be handled simultaneously. The code for this application is shown in listing 23.2.

Listing 23.2 StockQuoteServer.java

import java.io.*; // Import the package names to be
import java.net.*; // used by this application.
import java.util.*;
/**
* This is an application which implements our stock
* quote application protocol to provide stock quotes.
* @author David W. Baker
* @version 1.1
*/
public class StockQuoteServer {
// The port on which the server should listen.
private static final int SERVER_PORT = 1701;
// Queue length of incoming connections.
private static final int MAX_CLIENTS = 50;
// File that contains the stock data of format:
// <stock-id> <stock information>
private static final File STOCK_QUOTES_FILE =
new File("stockquotes.txt");
private ServerSocket listenSocket = null;
private String[] stockInfo;
private Date stockInfoTime;
private long stockFileMod;
// A boolean used to keep the server looping until
// interrupted.
private boolean keepRunning = true;
/**
* Starts up the application.
* @param args Ignored command line arguments.
*/
public static void main(String[] args) {
StockQuoteServer server = new StockQuoteServer();
server.serveQuotes();
}
/**
* The constructor creates an instance of this class,
* loads the stock data, and then our server listens
* for incoming clients.
*/
public StockQuoteServer() {
// Load the quotes and exit if it is unable to do so.
if (!loadQuotes()) System.exit(1);
try {
// Create a listening socket.
listenSocket =
new ServerSocket(SERVER_PORT,MAX_CLIENTS);
} catch(IOException excpt) {
System.err.println("Unable to listen on port " +
SERVER_PORT + ": " + excpt);
System.exit(1);
}
}
/**
* This method loads in the stock data from a file.
*/
protected boolean loadQuotes() {
String fileLine;
StringBuffer inputBuffer = new StringBuffer();
int numStocks = 0;

try {
// Create a decorated stream to the data file.
DataInputStream stockInput = new DataInputStream(
new FileInputStream(STOCK_QUOTES_FILE));
// Read in each line.
while ((fileLine = stockInput.readLine()) != null) {
// Put line into a buffer.
inputBuffer.append(fileLine + "\n");
numStocks++; // Increment the counter.
}
stockInput.close();
// Store the last modified timestamp.
stockFileMod = STOCK_QUOTES_FILE.lastModified();
} catch(FileNotFoundException excpt) {
System.err.println("Unable to find file: " + excpt);
return false;
} catch(IOException excpt) {
System.err.println("Failed I/O: " + excpt);
return false;
}
// Create an array of strings for each data file line.
stockInfo = new String[numStocks];
String inputString = inputBuffer.toString();
// Pointers for creating substrings.
int stringStart = 0,stringEnd = 0;
for (int index = 0; index < numStocks; index ++) {
// Find the end of line.
stringEnd = inputString.indexOf("\n",stringStart);
// If there is no more \n, then take the rest
// of inputString.
if (stringEnd == -1) {
stockInfo[index] =
inputString.substring(stringStart);
// Otherwise, take from the start to the \n
} else {
stockInfo[index] =
inputString.substring(stringStart,stringEnd);
}
// Increment the start of the substring.
stringStart = stringEnd + 1;
}
stockInfoTime = new Date(); // Store the time loaded.
return true;
}
/**
* This method waits to accept incoming client
* connections.
*/
public void serveQuotes() {
Socket clientSocket = null;

try {
while(keepRunning) {
// Accept a new client.
clientSocket = listenSocket.accept();
// Ensure that the data file hasn't changed; if
// so, reload it.
if (stockFileMod !=
STOCK_QUOTES_FILE.lastModified()) {
loadQuotes();
}
// Create a new handler.
StockQuoteHandler newHandler = new
StockQuoteHandler(clientSocket,stockInfo,
stockInfoTime);
Thread newHandlerThread = new Thread(newHandler);
newHandlerThread.start();
}
listenSocket.close();
} catch(IOException excpt) {
System.err.println("Failed I/O: "+ excpt);
}
}
/**
* This method allows the server to be stopped.
*/
protected void stop() {
if (keepRunning) {
keepRunning = false;
}
}
}
/**
* This class use used to manage a connection to
* a specific client.
*/
class StockQuoteHandler implements Runnable {
private Socket mySocket = null;
private PrintStream clientSend = null;
private DataInputStream clientReceive = null;
private String[] stockInfo;
private Date stockInfoTime;
/**
* The constructor sets up the necessary instance
* variables.
* @param newSocket Socket to the incoming client.
* @param info The stock data.
* @param time The time when the data was loaded.
*/
public StockQuoteHandler(Socket newSocket,
String[] info, Date time) {
mySocket = newSocket;
stockInfo = info;
stockInfoTime = time;
}
/**
* This is the thread of execution which implements
* the communication.
*/
public void run() {
String nextLine;
String quoteID;
String quoteResponse;
try {
clientSend =
new PrintStream(mySocket.getOutputStream());
clientReceive =
new DataInputStream(mySocket.getInputStream());
clientSend.println("+HELLO "+ stockInfoTime);
clientSend.flush();
// Read in a line from the client and respond.
while((nextLine = clientReceive.readLine())
!= null) {
nextLine = nextLine.toUpperCase();
// QUIT command.
if (nextLine.indexOf("QUIT") == 0) break;
// STOCK command.
else if (nextLine.indexOf("STOCK: ") == 0) {
quoteID =
nextLine.substring("STOCK: ".length());
quoteResponse = getQuote(quoteID);
clientSend.println(quoteResponse);
clientSend.flush();
}
// Unknown command.
else {
clientSend.println("-ERR UNKNOWN COMMAND");
clientSend.flush();
}
}
clientSend.println("+BYE");
clientSend.flush();
} catch(IOException excpt) {
System.err.println("Failed I/O: " + excpt);
// Finally close the streams and socket.
} finally {
try {
if (clientSend != null) clientSend.close();
if (clientReceive != null) clientReceive.close();
if (mySocket != null) mySocket.close();
} catch(IOException excpt) {
System.err.println("Failed I/O: " + excpt);
}
}
}
/**
* This method matches a stock ID to relevant information.
* @param quoteID The stock ID to look up.
* @return The releveant data.
*/
protected String getQuote(String quoteID) {
for(int index = 0; index < stockInfo.length; index++) {
// If there's a match, return the data.
if(stockInfo[index].indexOf(quoteID) == 0)
return "+" + stockInfo[index];
}
// Otherwise, this is an unknown ID.
return "-ERR UNKNOWN STOCK ID";
}
}

Starting the Server

The main() method allows the server to be started as an application and instanciates a new StockQuoteServer object. It then uses the serveQuotes() method to begin accepting client connections.

The constructor first calls the loadQuotes() method to load in the stock data. The constructor ensures that this process succeeds, and if not, quits the application. Otherwise, it creates a ServerSocket at port 1701. Now the server is waiting for incoming clients.

The loadQuotes() Method: Read in the Stock Data

This method uses a java.io.File object to obtain a DataInputStream, reading in from the data file called "stockquotes.txt". loadQuotes() goes through each line of the file, expecting that each line corresponds to a new stock with a format of:

stock-ID stock-data

The method counts the number of lines and places the data into a StringBuffer. It stores the file's modification time with the lastModified() method of the File class, so that the server can detect when the data has been updated. It creates an array large enough to hold the lines of the data file within a separate String element, and then parses the StringBuffer to place each line into a separate element of the array. It stores the current date using the java.util.Date class, so that it can tell connecting clients when the stock information was loaded.

In a more ideal design, this method would read data from the actual source of the stock information. Since we don't have an Internet stock quotes service available, a static file will do for now.

The serveQuotes() Method: Respond to Incoming Clients

This method runs in an infinite loop, setting up connections to clients as they come in. It blocks at the accept() method of the ServerSocket, waiting for a client to connect. When this occurs, it checks to see if the file in which the stock data resides has a different modification time since when it was last loaded. If this is the case, it calls the loadQuotes() method to reload the data.

The serveQuotes() method then creates a StockQuoteHandler instance, passing it the Socket created when the client connected and the array of stock data. It places this handler within a Thread object and starts that thread's execution. Once this has been done, the serveQuotes() method loops back again to wait for a new client to connect.

Creating the StockQuotesHandler

This class implements the Runnable interface so that it can run within its own thread of execution. The constructor merely sets some instance variables to refer to the Socket and stock data passed to it.

The run() Method: Implementing the Communication

This method opens two streams to read from and write to the client. It sends the opening message to the client and then reads each request from the client. Because all requests should be in uppercase, it ensures this by translating the request to uppercase and then tries to match it with one of the two supported commands, STOCK: and QUIT.

If the request is a STOCK: command, it assumes everything after STOCK: is the stock identifier. Parsing out that data with the substring() method, it passes the identifier to the getQuote() method to obtain the appropriate data. getQuote() is a simple method which iterates through the data obtained from the data file, trying to find a match. If one is found, it returns the line. Otherwise, it returns an error message. The run() method sends this information to the client.

If the request is a QUIT command, the server sends the +BYE response and breaks from the loop. It then terminates the communication by closing the streams and the Socket. The run() method ends, allowing for the thread in which this object executes to terminate.

Should the request be neither of these two commands, the server sends back an error message, waiting for the client to respond with a valid command.

Running the Client and Server

Compile the two applications with javac. Then make sure you've created the stock quote data file tockquotes.txt, as specified within the server code, in the proper format. Run the server with the Java interpreter, and it will run until interrupted by the system.

Finally, run the client to see how your server responds. Try running the client with one or more of the stock identifiers you placed into the data file. Then, update the data file and try your queries again; the client should show that the data has changed.


Previous Page TOC Next Page

| Previous Chapter | Next Chapter |

|Table of Contents | Book Home Page |

| Que Home Page | Digital Bookshelf | Disclaimer |


To order books from QUE, call us at 800-716-0044 or 317-361-5400.

For comments or technical support for our books and software, select Talk to Us.

© 1996, QUE Corporation, an imprint of Macmillan Publishing USA, a Simon and Schuster Company.