'; zhtm += ''; zhtm += '

' + pPage + ''; zhtm += '
'; window.popUpWin.document.write(zhtm); window.popUpWin.document.close(); // Johnny Jackson 4/28/98 } //--> Java 1.2 Unleashed -- Ch 32 -- Server Programs


Java 1.2 Unleashed

Previous chapterNext chapterContents


- 32 -

Server Programs


In this chapter you'll learn how to write server programs to support Internet client/server applications. You'll also learn about the server programs found on the Internet and how they are written. You'll develop a Web server that implements version 0.9 of the Hypertext Transfer Protocol. This chapter builds on the material presented in Chapters 30, "Network Programming with the java.net Package," and 31, "Client Programs." You might want to review these chapters before continuing with the material presented in this chapter.

Types of Servers

Chapter 30 introduced you to the types of client programs found on the Internet. For every client, there must be a server. Typical servers include email, Web, FTP, Telnet, Netnews, and DNS. Other, less-popular servers such as Echo, Ping, and Finger are also commonly supported.

Email servers move mail from client programs through the Internet to its destination hosts and store it until it is retrieved. The Simple Message Transfer Protocol (SMTP) is used to move mail. The Post Office Protocol (POP) is used to store mail and serve it to destination client programs.

Web servers implement the Hypertext Transfer Protocol (HTTP) in order to serve Web pages over the Internet. The most popular Web server is the Apache Web server. It is publicly available and may be freely downloaded. Commercial Web servers, such as those provided by Netscape and Microsoft, are only a small percentage of those that are in current operation.

FTP servers implement the File Transfer Protocol to make files available over the Internet. The most popular FTP server is a publicly available server developed by Washington University in St. Louis, Missouri.

The domain name system provides the backbone for Internet communication by translating domain names to their IP addresses. The most popular DNS software is the publicly available BIND software developed by the University of California at Berkeley.

Telnet servers are found in UNIX, VMS, and other multiuser operating systems. These servers allow remote login and implement the Telnet protocol covered in Chapter 30.

Server Responsibilities

A server program listens for incoming connections on the well-known port associated with its service protocol. When an incoming connection is initiated by a client, the server accepts the connection and typically spawns a separate thread to service that client. The client sends service requests over the connection. The server performs the service and then returns the results to the client.

A Multithreaded Server Framework

The operation of most servers involves listening for connections, accepting connections, processing requests received over the connections, and terminating connections after all requests have been processed. The handling of multiple connections is generally performed using multiple threads. As such, a general framework for multithreaded servers can be developed. Listing 32.1 provides such a framework for a generic server named GenericServer. Appendix A, "Java Language Summary," summarizes how multithreading is accomplished in Java.

The GenericServer class is the main class of the program. It defines serverPort as the number of the port that the server is to listen on. You would change 1234 to the well-known port of the service that the server is to implement. The main() method creates an instance of GenericServer and invokes the instance's run() method. The run() method creates a new ServerSocket and assigns the socket to the server variable. It then executes an infinite loop where it listens for an incoming connection and creates a Socket instance to service the connection. The Socket instance is assigned to the client variable. A new ServerThread object is created to process client requests and the start() method of this object is invoked to get the thread up and running.

The ServerThread class extends the Thread class. It declares the client field variable to keep track of the client socket. Its run() method creates objects of class ServiceInputStream and ServiceOutputStream to communicate with the client. These streams are buffered to enhance I/O performance. A while statement is used to repeatedly invoke the processRequest() method to process client requests. If processRequest() returns a value of false, the service is completed, the while loop ends, and the client socket is closed. The processRequest() method is a stub for implementing service-specific request processing.

The ServiceInputStream, ServiceOutputStream, and ServiceRequest classes are placeholders for implementing client I/O and request processing.


NOTE: GenericServer handles exceptions by exiting. In real server implementations this may not be the best response. To implement a real server based on GenericServer, you should substitute your own exception handling code.

LISTING 32.1. THE GenericServer CODE.

import java.net.*;
import java.io.*;
import java.util.*;
public class GenericServer {
    // Replace 1234 with the well-known port used by the server.
    int serverPort = 1234; 
    public static void main(String args[]){
        // Create a server object and run it
        GenericServer server = new GenericServer();
        server.run();
    }
    public GenericServer() {
        super();
    }
    public void run() {
        try {
            // Create a server socket on the specified port
            ServerSocket server = new ServerSocket(serverPort);
            do {
                // Loop to accept incoming connections
                Socket client = server.accept();
                // Create a new thread to handle each connection 
                (new ServerThread(client)).start();
            } while(true);
        } catch(IOException ex) {
            System.exit(0);
        }
    }
}
class ServerThread extends Thread {
    Socket client;
    // Store a reference to the socket to which the client is connected
    public ServerThread(Socket client) {
        this.client = client;
    }
    // Thread's entry point
    public void run() {
        try {
            // Create streams for communicating with client
            ServiceOutputStream outStream = new ServiceOutputStream(
                new BufferedOutputStream(client.getOutputStream()));
            ServiceInputStream inStream = 
             new ServiceInputStream(client.getInputStream());
            // Read client's request from input stream
            ServiceRequest request = inStream.getRequest();
            // Process client's request and send output back to client
            while (processRequest(outStream)) {};
        }catch(IOException ex) {
            System.exit(0);
        }
        try {
            client.close();
        }catch(IOException ex) {
            System.exit(0);
        }
    }
    // Stub for request processing
    public boolean processRequest(ServiceOutputStream outStream) {
        return false;
    }
}
// Input stream filter
class ServiceInputStream extends FilterInputStream {
    public ServiceInputStream(InputStream in) {
        super(in);
    }
    // Method for reading client requests from input stream
    public ServiceRequest getRequest() throws IOException {
        ServiceRequest request = new ServiceRequest();
        return request;
    }
}
// Output stream filter
class ServiceOutputStream extends FilterOutputStream {
    public ServiceOutputStream(OutputStream out) {
        super(out);
    }
}
// Class to implement client requests
class ServiceRequest {
}

Writing Your Own Web Server

Web servers implement the Hypertext Transfer Protocol (HTTP) in order to retrieve Web resources identified by URLs. HTTP is an application-level protocol that is designed to be quick and efficient. It is based on the request-response paradigm. Web browsers initiate connections with Web servers and submit service requests. The servers, upon receiving a request, locate the specified resource and perform the requested operation. Typical Web browser requests are to retrieve a designated file or send data to a CGI program. HTTP supports several request types, referred to as methods. These include the GET, HEAD, and POST methods.

The Web server developed in the following section only supports the GET request. It responds to GET requests by returning the requested resource to the browser. The server implements version 0.9 of HTTP. This is the earliest documented version of the protocol. HTTP 0.9 operates as follows:

HTTP 0.9 is documented as part of Request For Comments (RFC) 1945 "Hypertext Transfer Protocol -- HTTP/1.0."

The HTTP09Server Program

The HTTP09Server program illustrates the basic operation of a Web server. (See Listing 32.2.) It is a multithreaded Web server that implements the HTTP 0.9 protocol. Many Web servers are multithreaded, allowing them to simultaneously support multiple browser connections. HTTP09Server tailors the multithreaded server framework covered earlier in this chapter to the HTTP-specific processing. The server is configurable and supports logging of HTTP requests received from clients.

LISTING 32.2. THE SOURCE CODE FOR THE HTTP09Server PROGRAM.

import java.net.*;
import java.io.*;
import java.util.*;
public class HTTP09Server {
    // Version constants
    static final String NAME = "HTTP09Server";
    static final String VERSION = "1.0";
   // Variables for accessing configuration and logging objects
    ServerConfiguration config = new ServerConfiguration();
    Logger logger = new Logger();
    public static void main(String args[]){
        // Create a new server
        HTTP09Server server = new HTTP09Server();
        // Process command line arguments
        if(args.length>0) server.processCommandLine(args);
        // Run the server
        server.run();
    }
    public HTTP09Server() {
        super();
    }
    public void processCommandLine(String[] args) {
        for(int i=0;i<args.length;++i) {
            // Look for a configuration file
            if(args[i].equals("-CONFIG")) {
                if(i+1<args.length)
                    config.processConfigurationFile(args[i+1]);
               else 
               System.out.println("Configuration file argument is                 Âmissing.");
                break;
            }
        }
        String logFile = config.getLogFile();
        if(logFile!="") logger = new          ÂLogger(logFile,config.echoToConsole());
    }
    public void displayVersionInfo(){
        System.out.println("HTTP09Server version "+VERSION);
    }
    public void run() {
        displayVersionInfo();
        // Configure server
        config.display();
        try {
            // Create server socket, listen for and process incoming 
            // connections
            ServerSocket server = new              ÂServerSocket(config.getServerPort());
            int localPort = server.getLocalPort();
            logger.datedLog(NAME+" is listening on port "+localPort+".");
            do {
                Socket client = server.accept();
                // Create a new thread to handle the connection
                (new HTTP09ServerThread(client,config,logger)).start();
            } while(true);
        } catch(IOException ex) {
             logger.datedLog("Unable to listen on "+
             config.getServerPort()+".");
            System.exit(0);
        }
    }
}
class HTTP09ServerThread extends Thread {
    Socket client;
    ServerConfiguration config;
    Logger logger;
    public HTTP09ServerThread(Socket client,
            ServerConfiguration config, Logger logger) {
        this.client = client; 
        this.config = config;
        this.logger = logger;
    }
    public void run() {
        try {
            // Send output to logging device
            describeConnection(client);
            // Set up I/O streams
            HTTPOutputStream outStream = new HTTPOutputStream(
                new BufferedOutputStream(client.getOutputStream()));
                HTTPInputStream inStream = 
             new HTTPInputStream(client.getInputStream());
            // Get and process client requests
            HTTPRequest request = inStream.getRequest();
            request.log(logger);
            if(request.isGetRequest())
                processGetRequest(request,outStream);
            logger.datedLog("Request completed. Closing connection.");
        }catch(IOException ex) {
            logger.datedLog("IOException occured when processing              Ârequest.");
        }
        try {
            client.close();
        }catch(IOException ex) {
            logger.datedLog("IOException occured when closing socket.");
        }
    }
    void describeConnection(Socket client) {
        String destName = client.getInetAddress().getHostName();
        String destAddr = client.getInetAddress().getHostAddress();
        int destPort = client.getPort();
        logger.datedLog("Accepted connection to "+destName+" ("
            +destAddr+")"+" on port "+destPort+".");
    }
    void processGetRequest(HTTPRequest request,HTTPOutputStream outStream)
            throws IOException {
        // What file is the client requesting?
        String fileName = request.getFileName(config);
        File file = new File(fileName); 
        if(file.exists()) {
            // Figure out the file's full path name
            String fullPath = file.getCanonicalPath();
            // Is the file in the server root directory (or              Âsubdirectories)?
            if(inServerRoot(fullPath)) {
                int len = (int) file.length();
                // Send the file to the requesting client
                sendFile(outStream,file);
            }else logger.datedLog("File is not in server root.");
          }else logger.datedLog("File "+file.getCanonicalPath()+
         " does not exist.");
    }
    public boolean inServerRoot(String fileName) {
        String serverRoot = config.getServerRoot();
        int fileLength = fileName.length();
        int rootLength = serverRoot.length();
        if(fileLength<rootLength) return false;
        if(serverRoot.equals(fileName.substring(0,rootLength))) return          Âtrue;
        return false;
    }
    void sendFile(HTTPOutputStream out,File file) {
        try {
             DataInputStream in = 
             new DataInputStream(new FileInputStream(file));
            int len = (int) file.length();
            byte buffer[] = new byte[len];
            in.readFully(buffer);
            in.close();
            for(int i=0;i<len;++i) out.write(buffer[i]);
            out.flush();
            out.close();
            logger.datedLog("File sent: "+file.getCanonicalPath());
            logger.log("Number of bytes: "+len);
        }catch(Exception ex){
            logger.datedLog("Error retrieving "+file);
        }
    }
}
class HTTPInputStream extends FilterInputStream {
    public HTTPInputStream(InputStream in) {
        super(in);
    }
    public String readLine() throws IOException {
        StringBuffer result=new StringBuffer();
        boolean finished = false;
        boolean cr = false;
        do {
            int ch = -1;
            ch = read();
            if(ch==-1) return result.toString();
            result.append((char) ch);
            if(cr && ch==10){
                result.setLength(result.length()-2);
                return result.toString();
            }
            if(ch==13) cr = true;
            else cr=false;
        } while (!finished);
        return result.toString();
    }
    // Read and HTTP request from the input stream
    public HTTPRequest getRequest() throws IOException {
        HTTPRequest request = new HTTPRequest();
        String line;
        do {
            line = readLine();
            if(line.length()>0) request.addLine(line);
            else break;
        }while(true);
        return request;
    }
}
class HTTPOutputStream extends FilterOutputStream {
    public HTTPOutputStream(OutputStream out) {
        super(out);
    }
    public void println() throws IOException {
        write(13);
        write(10);
    }
    public void println(String s) throws IOException {
        for(int i=0;i<s.length();++i) write(s.charAt(i));
        println();
    }
}
// Class for encapsulating an HTTP request
class HTTPRequest {
    Vector lines = new Vector();
    
    public HTTPRequest() {
    }
    public void addLine(String line) {
        lines.addElement(line);
    }
    boolean isGetRequest() {
        if(lines.size() > 0) {
            String firstLine = (String) lines.elementAt(0);
            if(firstLine.length() > 0)
                if(firstLine.substring(0,3).equalsIgnoreCase("GET"))
                    return true;
        }
        return false;
    }
    String getFileName(ServerConfiguration config) {
        if(lines.size()>0) {
            String firstLine = (String) lines.elementAt(0);
            String fileName = firstLine.substring(firstLine.indexOf("              Â")+1);
            int n = fileName.indexOf(" ");
            if(n!=-1) fileName = fileName.substring(0,n);
            try {
                if(fileName.charAt(0) == `/') 
                fileName =  fileName.substring(1);
            } catch(StringIndexOutOfBoundsException ex) {}
            if(fileName.equals("")) fileName = config.getDefaultFile();
            if(fileName.charAt(fileName.length()-1)=='/') 
                fileName+=config.getDefaultFile();
            return config.getServerRoot()+fileName;
        }else return "";
    }
    void log(Logger logger) {
        logger.datedLog("Received the following request:");
        for(int i=0;i<lines.size();++i)
            logger.log((String) lines.elementAt(i));
    }
}
// Class for configuring the server
class ServerConfiguration {
    static final char CONFIG_COMMENT_CHAR = `#';
    int serverPort = 80;
    String serverRoot = "";
    String defaultFile = "index.htm";
    String logFile = "";
    boolean echoLogToConsole = true;
    public ServerConfiguration() {
    }
    public char getCommentChar() {
        return CONFIG_COMMENT_CHAR;
    }
    public int getServerPort() {
        return serverPort;
    }
    public String getServerRoot() {
        return serverRoot;
    }
    public String getDefaultFile() {
        return defaultFile;
    }
    public String getLogFile() {
        return logFile;
    }
    public boolean echoToConsole() {
        return echoLogToConsole;
    }
    public void display() {
        System.out.println("  serverPort: "+serverPort);
        System.out.println("  serverRoot: "+serverRoot);
        System.out.println("  defaultFile: "+defaultFile);
        System.out.println("  logFile: "+logFile);
        System.out.println("  echoLogToConsole: "+echoLogToConsole);
    }
    public void processConfigurationFile(String fname) {
        try {
            File file = new File(fname);
            if(file.exists()) {
                 BufferedReader reader = 
                 new BufferedReader(new FileReader(file));
                String line;
                while((line=reader.readLine())!=null)
                    processConfigurationLine(line);
                reader.close();
            }
        }catch(Exception ex) {
            System.out.println("Unable to process configuration file.");
        }
    }
    public void processConfigurationLine(String line)
            throws NumberFormatException {
        line = removeLeadingWhiteSpace(line);
        if(line.length()==0) return;
        int n;
        int n1 = line.indexOf(` `);
        int n2 = line.indexOf(`\t');
        if(n1 == -1) n = n2;
        else if(n2 == -1) n = n1;
        else if(n1 < n2) n = n1;
        else n = n2;
        if(n==-1 || n==line.length()-1) return;
        String param = line.substring(0,n);
        String value = line.substring(n+1);
        if(param.equals("serverPort"))
            serverPort = (new Integer(value)).intValue();
        else if(param.equals("serverRoot")){
            serverRoot = value;
            if(!serverRoot.equals("")){
                char ch = serverRoot.charAt(serverRoot.length()-1);
                if(ch!='/' && ch!='\\') serverRoot+="/";
            }
        }else if(param.equals("defaultFile"))
            defaultFile = value;
        else if(param.equals("logFile"))
            logFile = value;
        else if(param.equals("echoLogToConsole"))
            echoLogToConsole = (new Boolean(value)).booleanValue();
    }
    String removeLeadingWhiteSpace(String line) {
        boolean finished = false;
        do {
            if(line.length()==0) return "";
            char ch = line.charAt(0);
            if(ch==CONFIG_COMMENT_CHAR) return "";
            if(ch!=' ` && ch!='\t') return line;
            line=line.substring(1);
        } while (!finished);
        return "";
    }
}
// Class for logging information about the server's operation
class Logger {
    public String logFile;
    public boolean echoLogToConsole = true;
    public BufferedWriter writer = null;
    public Logger() {
    }
    public Logger(String fname, boolean echo) {
        logFile = fname;
        echoLogToConsole = echo;
        try {
            writer = new BufferedWriter(new FileWriter(fname,true));
        }catch(IOException ex){}
    }
    void logMsg(String msg) {
        if(writer!=null) {
            try {
                writer.write(msg);
                writer.newLine();
                writer.flush();
            }catch(IOException ex){}
        }
        if(echoLogToConsole) System.out.println(msg);
    }
    public synchronized void log(String msg) {
        logMsg("  "+msg);
    }
    public synchronized void datedLog(String msg) {
        logMsg((new Date()).toString()+" "+msg);
    }
}

When you run HTTP09Server it displays the following output.

HTTP09Server version 1.0
  serverPort: 80
  serverRoot:
  defaultFile: index.htm
  logFile:
  echoLogToConsole: true
Fri Jan 16 19:33:43 PST 1998 HTTP09Server is listening on port 80.

The output identifies the server port as port 80, the server root directory as blank (indicating the directory from which the server was run), the default filename as index.htm, no logging file, and logging being displayed to the console window. You'll learn how to configure these parameters in the section "Configuring HTTP09Server."

I have supplied a default Web page, index.htm, that is retrieved by HTTP09Server. (See Listing 32.3.) I've also included the test.htm file shown in Listing 32.4. You can also retrieve other Web pages by placing them in a path off the server's root directory.

LISTING 32.3. THE CONTENTS OF THE index.htm FILE.

<HTML>
<HEAD>
<TITLE>Index</TITLE>
</HEAD>
<BODY>
<H1>This is index.htm</H1>
</BODY>
</HTML>

LISTING 32.4. THE CONTENTS OF THE test.htm FILE.

<HTML>
<HEAD>
<TITLE>Test</TITLE>
</HEAD>
<BODY>
<H1>This is test.htm</H1>
</BODY>
</HTML>

Because HTTP09Server is a server, you need to use a client program to interact with it. Launch your favorite Web browser and open the URL of your machine. For example, if your host name is my.host.name.com, open the URL http://my.host.name.com/. HTTP09Server responds by identifying the browser connection and sending the index.htm file. You can access other files by appending their names to the URL. For example, to access the test.htm file in the directory where you launched HTTP09Server, use the URL http://my.host.name.com/index.htm. You can also use localhost or 127.0.0.1 if you do not have a host name or know your IP address. Figure 32.1 shows how index.htm is displayed by Netscape Communicator 4.0. Figure 32.2 shows how test.htm is displayed.

When I request index.htm using my browser, the following output is displayed by HTTP09Server on the console window:

Fri Jan 16 19:47:17 PST 1998 Accepted connection to localhost (127.0.0.1) on port 1759.
Fri Jan 16 19:47:17 PST 1998 Received the following request:
  GET / HTTP/1.0
  Connection: Keep-Alive
  User-Agent: Mozilla/4.04  (Win95; U)
  Host: localhost
  Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, image/png, */*
  Accept-Language: en
  Accept-Charset: iso-8859-1,*,utf-8
Fri Jan 16 19:47:18 PST 1998 File sent: D:\jdk1.2beta2\ju\ch32\index.htm
  Number of bytes: 101

Fri Jan 16 19:47:18 PST 1998 Request completed. Closing connection.

You'll receive more or less output depending on the browser you use, its version, and how it is configured. The previous output identifies the date and time at which a connection was accepted, the request received from the browser, the file that was sent in response, and the time the connection was closed. You will see similar, but different, output when you request files from your installation of HTTP09Server.

When you access the URL http://my.host.name.com, HTTP09Server is instructed to return the default-no-name HTML file. It responds by sending index.htm to the Web browser.


NOTE: If you cannot find your host name, you can use localhost instead. For example, the URL http://localhost/ can be used instead of http://my.host.name.com/.

FIGURE 32.1. Web browser displaying the default file (index.htm).

FIGURE 32.2. Web browser displaying test.htm.

Experiment by creating your own HTML files and using your browser to access them using HTTP09Server. Use Ctrl+C to terminate the operation of HTTP09Server.

How HTTP09Server Works

HTTP09Server is implemented using the following seven classes:

These classes are covered in the following subsections.

HTTP09Server

This class is similar in structure to GenericServer, but adds additional field variables and methods. The config variable is assigned an object of the ServerConfiguration class. This object encapsulates all configuration information needed by HTTP09Server. The logger variable is assigned a Logger object that is used to support request logging.

The processCommandLine() method checks to see if a configuration file was included as an argument to HTTP09Server. If so, it invokes the processConfigurationFile() method on the ServerConfiguration object referenced by config. A Logger object is created, initialized based on the server configuration, and assigned to the logger variable.

The displayVersionInfo() method displays version information about HTTP09Server.

The run() method displays version and configuration information, listens on the server port, and creates HTTP09ServerThread objects to service client requests.

HTTP09ServerThread

HTTP09ServerThread follows the structure of ServerThread and adds a few methods. The describeConnection() method uses the logger to display log information about the connection being handled. The processGetRequest() method determines the name of the file requested by the client, checks to see if the file exists and is accessible from the server root, and sends the file to the client. The inServerRoot() method checks a filename to see if it is accessible from the server root. The sendFile() method sends the file requested by the client.

HTTPInputStream and HTTPOutputStream

The HTTPInputStream and HTTPOutputStream classes are analogous to the ServiceInputStream and ServiceOutputStream classes of GenericServer. The readLine() method of HTTPInputStream reads a carriage return, linefeed terminated line. The getRequest() method reads multiline client requests. HTTPOutputStream provides the println() method to send response data to the client.

HTTPRequest

The HTTPRequest class is analogous to the ServiceRequest class of GenericServer. It stores requests using a Vector object. The isGetRequest() method verifies that a client request is an HTTP GET, as opposed to a PUT, POST, or other request. The getFileName() method extracts the file requested by the client from the request Vector object. The log() method is used to log a client request.

ServerConfiguration

The ServerConfiguration class provides the capability to configure HTTP09Server. It maintains the configuration information described in the section "Configuring HTTP09Server" later in this chapter. It provides several methods for accessing this configuration information. The processConfigurationFile() and processConfigurationLine() methods read and parse the configuration file.

Logger

The Logger class implements a logging capability. It displays logging information to a file, the console, or both. The logMsg() method writes data to the log devices. The log() and datedLog() methods are synchronized methods that use logMsg(). The datedLog() method prepends the current date and time to the log data.

Configuring HTTP09Server

You can configure the following parameters of HTTP09Server:

Listing 32.5 presents a sample configuration file. Configuration parameters are placed on separate lines. Each line begins with the name of the parameter to be configured, followed by its value. The configuration parameter names are serverPort, serverRoot, defaultFile, logFile, and echoToConsole.

The pound character (#) indicates a comment in the configuration file when it is placed at the beginning of a line. Lines beginning with the comment character are ignored.

To use a configuration file with HTTP09Server, run HTTP09Server as follows:

java HTTP09Server -CONFIG configurationFileName

Substitute your configuration file for configurationFileName.

LISTING 32.5. EXAMPLE HTTP09Server CONFIGURATION FILE.

# Example HTTP09Server configuration file
# Set the server port to port 8080 instead of the default port 80.
serverPort 8080
# Set the server root to the D:\temp directory.
serverRoot D:\temp\
# Set the default file to default.htm.
defaultFile default.htm
# Set the log file to log.txt.
logFile log.txt
# Disable logging to the console window.
echoLogToConsole false

Summary

In this chapter you learned how to write programs that implement the server end of Internet client/server applications. You learned about the common server programs found on the Internet and how they are structured. You have developed a Web server that implements HTTP version 0.9. In Chapter 33, "Content and Protocol Handlers," you'll learn how to write content and protocol handlers that are used with Web client applications.


Previous chapterNext chapterContents

© Copyright, Macmillan Computer Publishing. All rights reserved.