[ Exercises | Chapter Index | Main Index ]

Solution for Programming 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 a line beginning with the word "ERROR" to the client and closes the connection. (The error response can include an error message on the rest of the line.)

Your program should use a subroutine to handle each request that the server receives. It should not stop after handling one request; it should remain open and continue to accept new requests. 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 calling a handleConnection() method that is defined later in the program. The actual communication with the client is done in this method. (Since it can potentially take a long time to transmit a file, it would be desirable to use threads to handle the connections in this program, instead of running them in the same thread that listens for connection requests. That would allow several requests to be handled simultaneously. Using threads for network communication will be covered in the next chapter.

The handleConnection() method 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 method returns.) You should be able to follow all this in the solution that follows.

(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 transmitted are not necessarily the same as the ones that are used in the file as stored on the disk. It would probably be better to consider the file to be binary data and to send an exact, byte-by-byte copy.)


The Solution

import java.net.*;
import java.io.*;
import java.util.Scanner;

/**
 * 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 "ERROR unsupported command" if the
 * command it reads is not one of the two possible legal commands.)
 * (The commands INDEX and GET are not case-sensitive.)
 * 
 * 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, call the handleConnection() method
         to process it.  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();
            handleConnection(directory,connection);
         }
      }
      catch (Exception e) {
         System.out.println("Server shut down unexpectedly.");
         System.out.println("Error:  " + e);
         return;
      }

   } // end main()


   /**
    * This method processes the connection with one client.
    * It creates streams for communicating with the client,
    * reads a command from the client, and carries out that
    * command.  The connection is also 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.
    */
   private static void handleConnection(File directory, Socket connection) {
      Scanner incoming;       // For reading data from the client.
      PrintWriter outgoing;   // For transmitting data to the client.
      String command = "Command not read";
      try {
         incoming = new Scanner( connection.getInputStream() );
         outgoing = new PrintWriter( connection.getOutputStream() );
         command = incoming.nextLine();
         if (command.equalsIgnoreCase("index")) {
            sendIndex(directory, outgoing);
         }
         else if (command.toLowerCase().startsWith("get")){
            String fileName = command.substring(3).trim();
            sendFile(fileName, directory, outgoing);
         }
         else {
            outgoing.println("ERROR unsupported 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) {
         }
      }
   }

   /**
    * This is called by the handleConnection() method in response to an "INDEX" command
    * from the client.  Send the list of files in the server's directory.
    */
   private static void sendIndex(File directory, PrintWriter outgoing) 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 handleConnection() 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.
    */
   private static void sendFile(String fileName, File directory, PrintWriter outgoing)
                                                                       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.");
   }


} //end class FileServer

[ Exercises | Chapter Index | Main Index ]