Chapter 24
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 23 is very useful with well-established protocols, sockets allow you to develop your own modes of communication.

TCP Socket Basics

Sockets, as a programming interface, 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 uses the TCP protocol, inheriting the behavior of that transport protocol. Four pieces of information are needed to create a TCP socket:


TIP: The original TCP specification, RFC 793, used the term socket to mean the combination of a system's IP address and port number. A pair of sockets identified a unique end-to-end TCP connection. In this discussion, the term socket is used at a higher level, and a socket is your interface to a single network connection. RFC 793 is available at:

ftp://ftp.internic.net/rfc/rfc793.txt




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 24.1.


The Internet Assigned Numbers Authority (IANA) assigns well known ports to application protocols. At the time of this writing, the current listing of the well-known ports is within RFC 1700, available from:

ftp://ftp.internic.net/rfc/rfc1700.txt


Table 24.1 Well-Known TCP Ports and Services
Port Service
21 FTP
23 Telnet
25 SMTP (Internet Mail Transfer)
79 Finger
80 HTTP


TIP: 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 or ephemeral 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 24.1 illustrates this concept.

Figure 24.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.

Figure 24.1 also shows a simplified view of a client-server connection. Many machines are configured with multiple IP interfaces--they have more than one IP address. These distinct IP addresses allow for separate connections to be maintained. Thus, a server may have an application accept connections on port 80 for one IP address while a different application handles connections to port 80 for another IP address. These connections are distinct. The Java socket classes, described within the section "Java TCP Socket Classes," allow you to select a specific local interface for the connection.

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

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 commonly used constructors:

public Socket(String host, int port) 
   throws UnknownHostException, IOException;
public Socket(InetAddress address, int port) 
   throws IOException;
public Socket(String host, int port, InetAddress localAddr,
   int localPort) throws UnknownHostException, IOException;
public Socket(InetAddress address, int port,
   InetAddress localAddr, int localPort) 
   throws UnknownHostException, 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 are similar to the first two, except that they allow you to choose the local interface and port number for the connection. If your machine has multiple IP addresses, you can use these constructors to choose a specific interface to use.

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 23 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.


TIP: The DNS, described in Chapter 23, is a distributed database whose information changes over time. The InetAddress class, however, is written so that it only performs DNS resolution once for each hostname over the life of the Java runtime. All subsequent InetAddress objects created for a particular hostname will be returned from a persistent cache.

Thus, if you have a long-running Java application, the IP address contained within an InetAddress object may become inappropriate. The comments within the JDK code indicate that this was done for security reasons. In your programming, this may become an important fact to be aware of.


See "Internet Protocol (IP)," Chapter 23

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 java.io object, such as BufferedReaderandPrintWriter, respectively. Both getInputStream() and getOutputStream() throw an IOException, which should be caught.

try {
   Socket netspace = new Socket("www.netspace.org",7);
   BufferedReader input = new BufferedReader(
      new InputStreamReader(netspace.getInputStream()));
   PrintWriter output = new PrintWriter(
      netspace.getOutputStream(), true);
} 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 need only 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 three constructors:

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

The first constructor 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. The final constructor allows you to specify a local interface to listen for connections. If your machine has multiple IP addresses, this constructor allows you to provide services to specific IP addresses. Should you use the first two constructors on such a machine, the ServerSocket will accept connections to any of the machine's IP addresses.

After creating a ServerSocket, the accept() method can be used to wait for a client to connect. The accept() method 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();

   BufferedReader input = new BufferedReader(
      new InputStreamReader(clientConn.getInputStream()));   PrintWriter output = new PrintWriter(
      clientConn.getInputStream(), true);
} 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.


NOTE: The socket classes in the Java API provide a convenient stream interface by using your hosts TCP implementation. Within the JDK, a subclass of the abstract class SocketImpl performs the interaction with your machine's TCP. It is possible to define a new SocketImpl which could use a different transport layer than plain TCP. You can change this transport layer implementation by creating your own subclass of SocketImpl and defining your own SocketImplFactory. However, in this chapter, it is assumed that you are using the JDK's socket implementation, which uses TCP.


Customizing Socket Behavior

The JDK allows you to specify certain parameters which affect how your TCP sockets will behave. These parameters mimic the behavior of some of the options available within the Berkeley sockets API, and thus are often referred to by the names within that API: SO_TIMEOUT, SO_LINGER, and TCP_NODELAY.

The Socket class has a method setSoTimeout() which allows you to specify a timeout in milliseconds. Any subsequent attempts to read from the InputStream of this socket will wait for data only until this timeout expires. If the timeout expires, an InterruptedIOException is thrown. By default, the timeout is 0, indicating that a read() call should block forever or until an IOException is thrown.

The ServerSocket class also has a setSoTimeout() method, but the timeout applies to the accept() method. If you set a timeout, a subsequent call to accept() will wait only the specified number of milliseconds for a client to connect. When the timeout expires, an InterruptedIOException is thrown. For most applications, it is appropriate for a server to wait indefinitely for clients to connect, but in certain instances, the ability to timeout is valuable. When the exception is thrown, the ServerSocket instance is still valid, and if you wish, you can call accept() again.

The setSoLinger() method of the Socket class allows you to modify how the close() method behaves. Normally, when you close() the socket, any data which is queued by your machine's TCP or has yet to be acknowledged by the recipient is dealt with in the background; the close() function does not block until TCP has completed the transmission. If you enable the setSoLinger() option, this changes. When the option is enabled with a timeout of 0, a close() causes any data queued for transmission to be discarded and the recipient is sent a reset segment. To wit, the connection abruptly aborts. When the option is enabled with a positive timeout, close() will block until all of the data has been sent and acknowledged by the recipient, or the timeout expires, in which case an IOException is thrown. This option allows your application to become more aware how successful TCP is at sending all of the data to the recipient.

The last socket option which Java TCP sockets support is set within the setTcpNoDelay() option of the Socket class. This allows you to disable the use of the Nagle algorithm in your machine's TCP implementation. The Nagle algorithm instructs a TCP implementation to limit the number of unacknowledged small segments to one. When sending data a small piece at a time, TCP using the Nagle algorithm will wait until each piece is acknowledged before sending another. This greatly reduces congestion on networks, and in most instances, it is to your advantage to leave the Nagle algorithm enabled. However, there are times when an application must have small messages transmitted without delay, and the setTcpNoDelay() method allows you to disable the standard behaviors.


NOTE: The Nagle algorithm was originally proposed within RFC 896, available from:

ftp://ftp.internic.net/rfc/rfc896.txt


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, you will create 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:

  1. Client connects to server.
  2. Server responds to client with a message indicating the currentness of the data.
  3. The client requests data for a stock identifier.
  4. The server responds.
  5. Repeat steps 3-4 until the client ends the dialog.
  6. Terminate the connection.

Implementing this design, you 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

If the client sends a command requesting information about a stock, but omits the stock ID, the server will send:

-ERR MALFORMED COMMAND

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 example 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 24.1.

Listing 24.1StockQuoteClient.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.2
 */
public class StockQuoteClient {
   // The Stock Quote server listens at this port.
   private static final int SERVER_PORT = 1701;
   // Should your quoteSend PrintWriter autoflush?
   private static final boolean AUTOFLUSH = true;
   private String serverName;
   private Socket quoteSocket = null;
   private BufferedReader quoteReceive = null;
   private PrintWriter 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.
    * @param args The server followed by the stock IDs.
    */
   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 the rest of the elements of the args array
      // into the 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 out the timestamp, which is everything after
      // the first space.
      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 BufferedReader(
                          new InputStreamReader(
                             quoteSocket.getInputStream()));
         quoteSend = new PrintWriter(
                           quoteSocket.getOutputStream(),
                           AUTOFLUSH);
         // 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 you 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 instantiates 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 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, this 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 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. When a server handles requests one after the other, it is said to be iterative, while one that deals with multiple requests at the same time is concurrent. For TCP client/server interactions, which can often last a long time, concurrent operation is often essential. The handler is the object which performs the actual communication with the client, and multiple instances of the handler allow the server to process multiple requests simultaneously.

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 24.2.

Listing 24.2StockQuoteServer.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.2
 */
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 Hashtable 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;
      StringTokenizer tokenize;
      String id;
      StringBuffer value;
    
      try {
         // Create a decorated stream to the data file.
         BufferedReader stockInput = new BufferedReader(
            new FileReader(STOCK_QUOTES_FILE));
         // Create the Hashtable in which to place the data.
         stockInfo = new Hashtable();
         // Read in each line.
         while ((fileLine = stockInput.readLine()) != null) {
           // Break up the line into tokens.
           tokenize = new StringTokenizer(fileLine);
           try {
             id = tokenize.nextToken();
             // Ensure the stock ID is stored in upper case.
             id = id.toUpperCase();
             // Now create a buffer to place the stock value in.
             value = new StringBuffer();
             // Loop through all remaining tokens, placing them
             // into the buffer.
             while(tokenize.hasMoreTokens()) {
               value.append(tokenize.nextToken());
               // If there are more tokens to come, then append
               // a space.
               if (tokenize.hasMoreTokens()) {
                 value.append(" ");
               }
             }
             // Create an entry in our Hashtable.
             stockInfo.put(id,value.toString());
           } catch(NullPointerException excpt) {
             System.err.println("Error creating stock data " +
                                "entry: " + excpt);
           } catch(NoSuchElementException excpt) {
             System.err.println("Invalid stock data record " +
                                "in file: " + excpt);
           }
         }       
         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;
      }
      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 static final boolean AUTOFLUSH = true;
   private Socket mySocket = null;
   private PrintWriter clientSend = null;
   private BufferedReader clientReceive = null;
   private Hashtable 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, 
                            Hashtable info, Date time) {
      mySocket = newSocket;
      stockInfo = info;
      stockInfoTime = time;
   }

   /**
    * This is the thread of execution which implements
    * the communication.
    */
   public void run() {
      String nextLine;
      StringTokenizer tokens;
      String command;
      String quoteID;
      String quoteResponse;

      try {
         clientSend = 
            new PrintWriter(mySocket.getOutputStream(),
                            AUTOFLUSH);
         clientReceive = 
            new BufferedReader(new InputStreamReader(
                                  mySocket.getInputStream()));
         clientSend.println("+HELLO "+ stockInfoTime);
         // Read in a line from the client and respond.
         while((nextLine = clientReceive.readLine()) 
               != null) {
            // Break the line into tokens.
            tokens = new StringTokenizer(nextLine);
            try {
              command = tokens.nextToken();
              // QUIT command.
              if (command.equalsIgnoreCase("QUIT")) break;
              // STOCK command.
              else if (command.equalsIgnoreCase("STOCK:")) {
                 quoteID = tokens.nextToken();
                 quoteResponse = getQuote(quoteID);
                 clientSend.println(quoteResponse);
              }
              // Unknown command.
              else {
                 clientSend.println("-ERR UNKNOWN COMMAND");
              }
           } catch(NoSuchElementException excpt) {
             clientSend.println("-ERR MALFORMED COMMAND");
           }
         }
         clientSend.println("+BYE");
      } 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) {
      String info;

      // Make sure the quote ID is in upper case.
      quoteID = quoteID.toUpperCase();
      // Try to retrieve from out Hashtable.
      info = (String)stockInfo.get(quoteID);
      // If there was such a key in the Hashtable, info will
      // not be null.
      if (info != null) {
        return "+" + quoteID + " " + info;
      }
      else {
        // 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 instantiates 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 parses the line and places the data into a Hashtable instance: the upper-case value of the stock ID is the key while the stock data is the value. It stores the file's modification time with the lastModified() method of the File class, so the server can detect when the data has been updated. It stores the current date using the java.util.Date class, so 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. Because you probably haven't set up such a service within another company yet, 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 Hashtable of stock data. It places this handler within a Thread object and starts that thread's execution. Once this has been performed, the serveQuotes() method loops back again to wait for a new client to connect.

Creating the StockQuotesHandler
This class implements the Runnable interface so 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. The method uses a StringTokenizer to parse the request and tries to match it with one of the two supported commands, STOCK: and QUIT. If the request is a STOCK: command, it assumes the token after STOCK: is the stock identifier and passes the identifier to the getQuote() method to obtain the appropriate data. getQuote() is a simple method which tries to find a match within the stockInfo Hashtable. 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 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 stockquotes.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.