Lab 11

Building a Fileserver with Sockets

Today we are going to use Sockets to build both Server and Client applications. We will use them to transfer files across a network. This code will provide functionality similar to scp or an ftp server.

The intent of this lab is to give you some experience manipulating Sockets. What is the best way to learn how to implement new functionality? Start with a working sample. Copy the two files below to your Lab11 directory. Save and compile them.

UCaseServer.java

import java.io.IOException;
import java.net.Socket;
import java.net.ServerSocket;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.Scanner;

public class UCaseServer {

    public void listen(int port) {
        try{
            // (1) Create a ServerSocket to listen on a port
            ServerSocket svr = new ServerSocket(port);

            //while(true) {
                // (2) Wait for a client to connect
                Socket client = svr.accept();

                // (3) Create an object to write data to the socket
                PrintWriter out = new PrintWriter(client.getOutputStream(), true);            

                // (3) Create a Scanner to read from the socket
                Scanner in = new Scanner(client.getInputStream());

                // (4) Listen for a message over the socket
                String datain = in.nextLine();

                // (5) Modify the message
                String dataout = datain.toUpperCase();
                System.out.println("Received:    " + datain);
                System.out.println("Transmitted: " + dataout);
                System.out.println();

                // (6) Send message back to client
                out.println(dataout);

                // (7) Close client and wait for next connection
                client.close();
            //}
            svr.close();
			
        } catch (IOException e) {
            System.out.println("Could not listen on port " + port);
            System.exit(-1);
        }
    }

    public static void main(String[] args) {
        UCaseServer us = new UCaseServer();
        us.listen(10558);
        System.out.println("Server is done.");
    }
}

UCaseClient.java

import java.io.IOException;
import java.net.Socket;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.Scanner;

public class UCaseClient {

    public void connect(int port, String data) {
        try{
            // (1) Open Socket to server
            Socket client = new Socket("127.0.0.1", port);

            // (2) Create an object to write data to the socket
            PrintWriter out = new PrintWriter(client.getOutputStream(), true);            

            // (3) Create a Scanner to read from the socket
            Scanner in = new Scanner(client.getInputStream());

            // (4) Send out data to the server
            out.println(data);

            // (5) Read modified data from server
            String newdata = in.nextLine();
            
            // (6) Print original and new data
            System.out.println("Transmitted: " + data);
            System.out.println("Received:    " + newdata);

        } catch (IOException e) {
            System.out.println("Could not connect on port " + port);
            System.exit(-1);
        }
    }

    public static void main(String[] args) {
        UCaseClient uc = new UCaseClient();
        uc.connect(10558, "This is a test");
        System.out.println("Client is done.");
    }
}

A walk through the code

So what are these programs doing? To test them, open two terminal windows on your machine.

Run this command in the first terminal:

javac UCaseServer.java && java UCaseServer

Run this command in the second terminal:

javac UCaseClient.java && java UCaseClient

Take a look at comments (1) and (2) in UCaseServer. It opens a ServerSocket and waits for a connection. A ServerSocket is not a socket that we communicate through - it is a listener. When another process connects, the ServerSocket.accept() method returns a new Socket. This is the object that we need for communication. The accept() method blocks until a connection is made. This means that the program stops at this line and waits for input.

Now take a look at comment (1) in UCaseClient. This program connects with the server as soon as it starts. It then creates a PrintWriter object to write to the Socket, and a Scanner to read from it.

The UCaseServer creates a similar set of PrintWriter and Scanner objects. Each process can both read and write to the Socket.

The biggest problem with communicating over a socket is coordinating between applications. While one is writing, the other has to be reading. If both were attempting to write at the same time, messages would be lost. If both were attempting to read, they would block on Scanner.readNext() indefinitely.

How do we keep this sort of problem from happening?

Protocols to the rescue

Whenever you have communication between two processes, you need to agree to a protocol. This means file formats, special strings, etc. In the case of UCase, it includes an agreement on a "handshake". According to the (unwritten) protocol: immediately after the connection is made, UCaseClient sends a one-line string to the UCaseServer. So the server needs to enter reading mode as soon as the connection is made, and the client needs to write. As soon as the message is delivered, they swap roles - the client listens and the server returns a string.

As simple as this may sound, it is a good idea to diagram your protocols when you develop networked applications. If both apps were listening first, for example, they would lock up indefinitely. That might be complicated to debug.

Making a server stick around

Did you notice that the server closes after serving a single client? This would be annoying if we needed to rely on it to always be available.

Comment out the while() look in the server. This closes the Socket connection with the client, but does not close the SocketServer. As long as the SocketServer is around, we can continue to establish new connections. With these lines uncommented, you should have to start the server only once and be able to keep running the client over and over.

That is enough practice with a working server/client pair. Now we are going to begin developing today's lab.

The Assignment

You are going to modify the UCaseServer and UCaseClient classes in order to build a file transfer utility.

Setup

You need to run this lab in a directory named "Lab11".

Copy your test files above to "FileServer.java" and "FileClient.java". Make any necessary changes to get these two new files to compile.

Create a subdirectory in Lab11 named "docs".

Copy these files to your docs subdir: alice.txt declaration.txt fox.txt

Requirements

Here is what your application needs to do:

  1. The FileServer class runs in the Lab11 directory. It can listen on any port that the user requests. The user must pass the port number of the server on the command-line. The port number may not be hard-coded. The user should be able to start the server with a command like this:
    java FileServer 12345
  2. You must have proper error-checking code in place to ensure that the user provides one integer as an argument. The program should fail gracefully with a helpful error message if not.
  3. The FileClient class runs from any directory. It should even be able to run from a separate machine. For testing purposes, it can run on the same machine from the Lab11 directory. The user must pass the IP address and port number of the server on the command-line when starting the client. They may not be hard-coded. The user should be able to start the client with a command like this:
    java FileClient 127.0.0.1 12345
  4. You must have proper error-checking code in place to ensure that the user provides two arguments, and that the second one is an integer.
  5. You must have proper error-checking code in place to allow a graceful exit if the user supplies an IP address that does not exist.
  6. As soon as the two processes connect, the FileServer sends the client a list of the names of all the files in the 'docs' directory. You have not seen how to do this yet. Do an online search for "java list files in directory" to learn how. You can send the list to the client as a string. The important this is to make sure that the list of files is human-readable.
  7. As soon as the server transits the list of filenames, it will go into listen mode and wait for the client to send the server a single filename back.
  8. As soon as the two processes connect, the FileClient begins listening for a list of files. It prints them to the screen (along with a helpful message) so the user knows what files are available.
  9. Once the client has printed the list of files, it opens a Scanner to read input from the terminal. It waits for the user to type a filename and press Enter.
  10. The user decides which of the available files he wishes to download to his local directory. He types the filename in exactly as it appears and presses enter.
  11. Once the client reads a filename from the user, it sends to filename to the server, and then begins waiting for a reply from the server.
  12. The server reads the filename from the client. It then checks whether the filename exists. (It is possible that the file has been moved ore renamed, or that the user mistyped a name.)
  13. If the file exists, the server opens a Scanner to read the file from disk.
  14. We now have to deal with a new protocol issue. What if the file does not exist? We need to tell the client whether the file was found or not, and return the contents if it was. We will use the first character of the string sent to the client to deliver this message.
    • If the file was found, create a return string that begins with a '1', and then contains the entire contents of the file.
    • If the file was not found, create a return string that is just a '0'.
  15. The server sends the complete string (with leading 0 or 1) to the client.
  16. The client reads the incoming string from the server. It checks the first character to see whether the file was found.
    • If the first char is a 0, the client prints a helpful message to the user that the file was not found.
    • If the first char is a 1, the client strips the first char from the string and saves the rest of the file to the local directory, under its original file name.

    Updated Hint - You are now going to have to deal with a multi-line message to move files.
    This is harder than just handling a single-line string.
    The Scanner has no way of knowing when a file transfer is complete.
    It needs to be told explicitly when to stop reading.

    Scanner.nextLine() reads an entire line of text, ending at the "\n". This method will block if there is no text to read.
    Scanner.hasNext() is not a big help. It will also block if there is no text to read.
    We can update our protocol to recognize multi-line text:
    Line numberData
    first line(0 or 1) indicates whether the file was found or not
    multiple linesdata from file
    last lineToken to represent end of file, e.g. "<ENDTOKEN>"

    So you could use code like this to interpret the incoming socket data:
        String controlToken = in.nextLine();  // Strips the first line
        String tmp = "";
        final String ENDTOKEN = "<ENDTOKEN>";
        if(controlToken.equals("1") {
            boolean moredata = true;
            while(moredata) {
                tmp = in.nextLine();
                if(tmp.equals(ENDTOKEN)) {
                    moredata = false;
                }
                else {
                    System.out.println(tmp); { // Prints every line from the file
                }
            }
        }
     				
  17. The client closes its Socket and exits.
  18. The server closes its Socket and continues goes back to waiting for a new connection on the SocketServer.

Javadoc is not required for this lab.

What to hand in

At least two java files:

All of the above must be in a directory named 'Lab11'

Delete any *.class or other unneeded files prior to submitting

Delete all of your javadoc files.

When you run the submit script, the directory and its contents get zipped and sent to your instructor. Ask for help early if you are having problems running the submit script.

Make sure that you run the correct submit script for your instructor: