Solution for
Programming Exercise 10.4
THIS PAGE DISCUSSES ONE POSSIBLE SOLUTION to the following exercise from this on-line Java textbook.
Exercise 10.4: 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 <file>", where <file> 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 Section 10.2 for help with the problem of getting the list of files in the directory.
Discussion
The sample program DateServe.java 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 the 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. Nested classes were covered in Section 5.6. It's not essential in this example that the class be nested. It could easily be a separate class.)
The Solution
/* 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. This program uses the non-standard class, TextReader. */ import java.net.*; import java.io.*; public class FileServer { static final int LISTENING_PORT = 3210; public static void main(String[] args) { File directory; // The directory from which the // 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() static class ConnectionHandler extends Thread { // An object of this class is a thread that will // process the connection with one client. The // thread starts itself in the constructor. File directory; // The directory from which files are served Socket connection; // A connection to the client. TextReader incoming; // For reading data from the client. PrintWriter outgoing; // For transmitting data to the client. ConnectionHandler(File dir, Socket conn) { // Constructor. Record the connection and // the directory and start the thread running. directory = dir; connection = conn; start(); } void sendIndex() throws Exception { // This is called by the run() method in response // to an "index" command. Send the list of files // in the directory. 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."); } void sendFile(String fileName) throws Exception { // This is called by the run() command in response // to "get <fileName>" command. If the file doesn't // exist, send the message "error". Otherwise, // send the message "ok" followed by the contents // of the file. 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"); TextReader fileIn = new TextReader( new FileReader(file) ); while (fileIn.peek() != '\0') { // Read and send lines from the file until // an end-of-file is encountered. String line = fileIn.getln(); outgoing.println(line); } } outgoing.flush(); outgoing.close(); if (outgoing.checkError()) throw new Exception("Error while transmitting data."); } public void run() { // 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. String command = "Command not read"; try { incoming = new TextReader( connection.getInputStream() ); outgoing = new PrintWriter( connection.getOutputStream() ); command = incoming.getln(); 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 ]