[ Exercises | Chapter Index | Main Index ]

Solution for Programmming Exercise 11.3


This page contains a sample solution to one of the exercises from Introduction to Programming Using Java.


Exercise 11.3:

For this exercise, you will write a network server program. The program is a simple file server that makes a collection of files available for transmission to clients. When the server starts up, it needs to know the name of the directory that contains the collection of files. This information can be provided as a command-line argument. You can assume that the directory contains only regular files (that is, it does not contain any sub-directories). You can also assume that all the files are text files.

When a client connects to the server, the server first reads a one-line command from the client. The command can be the string "index". In this case, the server responds by sending a list of names of all the files that are available on the server. Or the command can be of the form "get filename", where filename is a file name. The server checks whether the requested file actually exists. If so, it first sends the word "ok" as a message to the client. Then it sends the contents of the file and closes the connection. Otherwise, it sends the word "error" to the client and closes the connection.

Ideally, your server should start a separate thread to handle each connection request. However, if you don't want to deal with threads you can just call a subroutine to handle the request. See the DirectoryList example in Subsection 11.2.2 for help with the problem of getting the list of files in the directory.


Discussion

The sample program DateServer.java, from Subsection 11.4.4, shows the typical outline of a server program. For this exercise, the processing of each connection request is more complicated, but the basic outline of the program is the same.

My program begins by getting the directory name from the command line. An object of type File is created which can be used to check whether the specified directory exists and is in fact a directory rather than a regular file. This allows for some basic error checking before the server starts running:

if (args.length == 0) {
   System.out.println("Usage:  java FileServer <directory>");
   return;
}
directory = new File(args[0]);
if ( ! directory.exists() ) {
   System.out.println("Specified directory does not exist.");
   return;
}
if (! directory.isDirectory() ) {
   System.out.println("The specified file is not a directory.");
   return;
}

The File object is needed later, in any case, for reading the list of files in the directory.

The rest of the main() routine takes the typical form for a server program. A listening socket is established. Then the server accepts and processes connection requests in an infinite loop. In this case, the processing is done by creating a thread belonging to a class ConnectionHandler which is a subclass of the standard class, Thread. The actual communication with the client is done in the run() method of this thread, which is executed in parallel with the main() routine and with any other ConnectionHandler threads that are created. This allows the server to process more than one client simultaneously. Since it can potentially take a long time to transmit a file, multithreading is very desirable in this example.

The run() method of the thread implements the communication protocol of the server in a straightforward way: It reads a line from the client. If the line is "index", it sends a list of available files. If the line starts with the string "get", it sends the requested file. Finally, it closes the connection. (In fact, the connection is closed in the finally clause of a try statement to make sure that the connection is closed before the thread terminates.) You should be able to follow all this in the solution that follows.

My ConnectionHandler class is a static nested class inside the FileServer class. It's not essential in this example that the class be nested. It could easily be a separate class, and that might be better style.

(One minor point: Suppose that the last line in a file is not terminated by an end-of-line marker. That is, there is no end-of-line directly preceding the end-of-file. When my program transmits the file, it will nevertheless send an end-of-line marker after the last line of the file. So, the file that is transmitted is not identical to the file on the computer's hard disk. Actually, things are even worse than that because the end-of-line markers used when the file is transmited are not necessarily the same as the ones that are used in the file as stored on the disk.)


The Solution

import java.net.*;
import java.io.*;

/**
 * This program is a very simple network file server.  The 
 * server has a list of available text files that can be
 * downloaded by the client.  The client can also download
 * the list of files.  When the connection is opened, the
 * client sends one of two possible commands to the server:
 * "index" or "get <file-name>".  The server replies to
 * the first command by sending the list of available files.
 * It responds to the second with a one-line message,
 * either "ok" or "error".  If the message is "ok", it is
 * followed by the contents of the file with the specified
 * name.  The "error" message indicates that the specified
 * file does not exist on the server. (The server can also
 * respond with the message "unknown command" if the command
 * it reads is not one of the two possible legal commands.)
 * 
 * The server program requires a command-line parameter
 * that specifies the directory that contains the files
 * that the server can serve.  The files should all be
 * text files, but this is not checked.  Also, the server
 * must have permission to read all the files.
 */
 public class FileServer {

   static final int LISTENING_PORT = 3210;


   public static void main(String[] args) {
   
      File directory;        // The directory from which the server
                             //    gets the files that it serves.
   
      ServerSocket listener; // Listens for connection requests.
   
      Socket connection;     // A socket for communicating with a client.
      

      /* Check that there is a command-line argument.
         If not, print a usage message and end. */
      
      if (args.length == 0) {
         System.out.println("Usage:  java FileServer <directory>");
         return;
      }
      
      /* Get the directory name from the command line, and make
         it into a file object.  Check that the file exists and
         is in fact a directory. */
         
      directory = new File(args[0]);
      if ( ! directory.exists() ) {
         System.out.println("Specified directory does not exist.");
         return;
      }
      if (! directory.isDirectory() ) {
         System.out.println("The specified file is not a directory.");
         return;
      }
      
      /* Listen for connection requests from clients.  For
         each connection, create a separate Thread of type
         ConnectionHandler to process it.  The ConnectionHandler
         class is defined below.  The server runs until the
         program is terminated, for example by a CONTROL-C. */
            
      try {
         listener = new ServerSocket(LISTENING_PORT);
         System.out.println("Listening on port " + LISTENING_PORT);
         while (true) {
            connection = listener.accept();
            new ConnectionHandler(directory,connection);
         }
      }
      catch (Exception e) {
         System.out.println("Server shut down unexpectedly.");
         System.out.println("Error:  " + e);
         return;
      }
      
   } // end main()
   

   /**
    * An object of this class is a thread that will process the connection with
    * one client.  The  thread starts itself in the constructor.
    */
   private static class ConnectionHandler extends Thread {
   
      File directory;           // The directory from which files are served.
      Socket connection;        // A connection to the client.
      BufferedReader incoming;  // For reading data from the client.
      PrintWriter outgoing;     // For transmitting data to the client.
      
      /**
       * Constructor.  Record the connection and the directory, and
       * start the thread running.
       * @param dir the directory containing the files served by the server
       * @param conn an already-connected socket for communicating with the client
       */
      ConnectionHandler(File dir, Socket conn) {
         directory = dir;
         connection = conn;
         start();
      }
      
      /**
       * This is called by the run() method in response to an "index" command
       * from the client.  Send the list of files in the server's directory.
       */
      void sendIndex() throws Exception {
         String[] fileList = directory.list();
         for (int i = 0; i < fileList.length; i++)
            outgoing.println(fileList[i]);
         outgoing.flush();
         outgoing.close();
         if (outgoing.checkError())
            throw new Exception("Error while transmitting data.");
      }
      
      /**
       * This is called by the run() command in response to "get <fileName>" 
       * command from the client.  If the file doesn't exist, send the message "error".
       * Otherwise, send the message "ok" followed by the contents of the file.
       */
      void sendFile(String fileName) throws Exception {
         File file = new File(directory,fileName);
         if ( (! file.exists()) || file.isDirectory() ) {
              // (Note:  Don't try to send a directory, which
              // shouldn't be there anyway.)
            outgoing.println("error");
         }
         else {
            outgoing.println("ok");
            BufferedReader fileIn = new BufferedReader( new FileReader(file) );
            while (true) {
                  // Read and send lines from the file until
                  // an end-of-file is encountered.
               String line = fileIn.readLine();
               if (line == null)
                   break;
               outgoing.println(line);
            }
         }
         outgoing.flush(); 
         outgoing.close();
         if (outgoing.checkError())
            throw new Exception("Error while transmitting data.");
      }
      
      /**
       * This is the method that is executed by the thread.
       * It creates streams for communicating with the client,
       * reads a command from the client, and carries out that
       * command.  The connection is logged to standard output.
       * An output beginning with ERROR indicates that a network
       * error occurred.  A line beginning with OK means that
       * there was no network error, but does not imply that the
       * command from the client was a legal command.
       */
      public void run() {
         String command = "Command not read";
         try {
            incoming = new BufferedReader( 
                            new InputStreamReader(connection.getInputStream()) );
            outgoing = new PrintWriter( connection.getOutputStream() );
            command = incoming.readLine();
            if (command.equals("index")) {
               sendIndex();
            }
            else if (command.startsWith("get")){
               String fileName = command.substring(3).trim();
               sendFile(fileName);
            }
            else {
               outgoing.println("unknown command");
               outgoing.flush();
            }
            System.out.println("OK    " + connection.getInetAddress()
                                        + " " + command);
         }
         catch (Exception e) {
            System.out.println("ERROR " + connection.getInetAddress()
                                     + " " + command + " " + e);
         }
         finally {
            try {
               connection.close();
            }
            catch (IOException e) {
            }
         }
      }
   
   }  // end nested class ConnectionHandler
  

} //end class FileServer

[ Exercises | Chapter Index | Main Index ]