CS551 Calculator Lab Assignment Deadline: Oct 3 (F) Midnight Submission: email a zipped package (containing source code and report) to both the instructor (leeyu@umkc.edu) and the TA (copdk4@umkc.edu) before the deadline. This documentation has three parts: Lab 2-A, Lab 2-B, and Report. 1. Lab 2-A : A Socket-Based Calculator Distribution The Simple Socket-Based Cookie Server There is no actual programming involved in this exercise but it will first teach the basics of Java socket communication. Take a copy of the CookieServer directory from the Lab2-A directory.: BetterSocket.java - the buffering socket class SimpleClient.java – the client applet SimpleServer.java – the cookie server – delivers short messages hello.txt – messages to deliver when client says “hello” bye.txt – messages to deliver when client says “bye” Inspect these files and notice the following points //BetterSocket.java import java.net.*; import java.io.*; The actual socket The buffered reader & writer public class BetterSocket{ Socket theSocket; BufferedReader theReader; PrintWriter theWriter; public BetterSocket( String host, int port ) throws UnknownHostException, IOException { this( new Socket( host, port )); } public BetterSocket( Socket s ) throws UnknownHostException, IOException { theSocket = s; theReader = new BufferedReader( new InputStreamReader( new BetterSocket( host, port ) will call BetterSocket( socket ) with a plain socket for host & port Use the input and output stream of the Socket for our buffered reader & writer theSocket.getInputStream())); theWriter = new PrintWriter( theSocket.getOutputStream(), true ); } public Socket getSocket() { return( theSocket ); } public void close() throws IOException { theSocket.close(); } public String readLine() throws IOException { return( theReader.readLine()); } public void println( String s ) { theWriter.println(s); In case we need direct access to the socket readLine and println for receiving and sending data } } SimpleClient is an Applet //SimpleClient.java import java.applet.*; import java.awt.*; import java.awt.event.*; import java.net.*; import java.io.*; public class SimpleClient extends Applet { public static final int PORT=1234; Button theHelloButton = new Button( "Hello" ); Button theByeButton = new Button( "Bye" ); TextArea theResponse = new TextArea(5,64); This is the port we will use Two buttons & a text area - simple UI! BetterSocket theSocket; public void init(){ init() sets out the setLayout( new BorderLayout()); screen and assigns Panel p = new Panel(); listeners to the two add( p, BorderLayout.NORTH ); buttons add( theResponse, BorderLayout.SOUTH ); p.setLayout( new GridLayout(1,0)); p.add( theHelloButton ); p.add( theByeButton ); theResponse.setEditable(false); theHelloButton.addActionListener( new HelloHandler()); theByeButton.addActionListener( new ByeHandler()); } Simple error handler public void fatalError( Exception ex ){ that closes the socket ex.printStackTrace(); as well try{ theSocket.close(); } catch( Exception ex1 ){;} } Hello button pressed so: class HelloHandler implements ActionListener{ Make a socket ( talking to public void actionPerformed( ActionEvent evt ){ ourselves ), say "Hello" try{ and then get three cookies theResponse.setText(""); theSocket = new BetterSocket( getCodeBase().getHost(), PORT ); theSocket.println( "Hello" ); theResponse.append( theSocket.readLine() + "\n" ); theResponse.append( theSocket.readLine() + "\n" ); Bye button pressed so: theResponse.append( theSocket.readLine() + "\n" ); Make a socket ( talking to theSocket.close(); ourselves ), say "Bye" and } then get three cookies catch( Exception ex ){ fatalError(ex); } } } class ByeHandler implements ActionListener{ public void actionPerformed( ActionEvent evt ){ try{ theResponse.setText(""); theSocket = new BetterSocket( getCodeBase().getHost(), PORT ); theSocket.println( "Bye" ); theResponse.append( theSocket.readLine() + "\n" ); theResponse.append( theSocket.readLine() + "\n" ); theResponse.append( theSocket.readLine() + "\n" ); theSocket.close(); } catch( Exception ex ){ fatalError(ex); } } } } //SimpleServer.java import java.net.*; import java.io.*; import java.util.*; public class SimpleServer { public static final int PORT=1234; ServerSocket theSocket; BetterSocket theClient; Vector theHello = new Vector(); Vector theBye = new Vector(); Server will wait on this port ( the one the client will call on! ) The ServerSocket waits for a request and the BetterSocket wraps the ServerSocket for buffered I/O public static void main( String [] args ){ init() starts by reading the new SimpleServer().init(); hello & goodbye messages } from the two text files into public void init(){ try{ two Vectors BufferedReader in; String line; in = new BufferedReader( The server is new InputStreamReader( an application new FileInputStream( "hello.txt" ))); - not an while(( line = in.readLine()) != null ) applet. theHello.addElement( line ); in.close(); in = new BufferedReader( new InputStreamReader( This is the new FileInputStream( "bye.txt" ))); ServerSocket that will while(( line = in.readLine()) != null ) wait for requests theBye.addElement( line ); in.close(); theSocket = new ServerSocket(PORT); while(true){ System.err.println( "Server is ready" ); theClient = new BetterSocket(theSocket.accept()); System.err.println( "Server processing request" ); Use accept() to tell the String s = theClient.readLine(); ServerSocket to start System.err.println( "Read " + s + " from client" ); waiting and when a if( s.equalsIgnoreCase( "hello" )) message arrives wrap sendGreeting( theHello ); else if ( s.equalsIgnoreCase( "bye" )) the bare ServerSocket sendGreeting( theBye ); with a BetterSocket else System.err.println( "Invalid request: " + s ); theClient.close(); System.err.println( "Processing complete" ); Check the request, send the appropriate response } } catch( Exception ex ){ ex.printStackTrace(); } } public void sendGreeting( Vector v ) throws IOException{ int size = v.size(); for( int I=0; I<3; I++ ){ int n = (int)(Math.random() * size ); theClient.println((String) v.elementAt(n) ); } } } Select a random response from the vector and send it Running the CookieServer Make the CookieServer directory your current directory In a DOS box compile the files: javac *.java In a second DOS box start the server: java SimpleServer If the server is working you will see a “Server is ready” message. Create an HTML file containing the Applet tag which you will find as a comment at the start of the SimpleClient.java file. SimpleClient is written as an Applet which you can run using an applet viewer. Running SimpleClient in the Sun appletviewer: appletviewer SimpleClient.html Note that the Sun appletviewer expects an HTML file as its parameter. Shut down the server with <ctrl>C The Socket Calculator Repeat all the steps for adding a distribution to your Calculator: Create a directory for your new package ( “socket” ) (create this in the calc folder) Put a copy of BetterSocket.java in the directory – modify this file so that it is in the socket package. Compile this file Put a copy of calc.PlainCalculatorImpl in the directory. Modify this file so that it will at least compile: Rename the package Set a title that you like Delete the line that reads local=true. This statement suppresses the dialog that asks the address of the remote server. You now want to be asked that question! Compile this file Add a distribution type constant to calc.CalculatorImpl - remember that the string you define must exactly match the name of the package directory Compile this file Add a menu entry to calc.DistMenu MenuItem declaration Place MenuItem in menu Add an actionPerformed section Compile this file Modify calc.CalculatorImplFactory: Add a case to DistAllowed – the socket distribution will work in either VM. Make sure that all the checks are linked together with else if statements otherwise only the last of these statements will take effect! This much should give you a Socket Calculator entry in the Distribution menu which brings up a separate Calculator – still a local one though! (make sure you have a classpath pointing to calc folder) Writing your Calculator server This involves the following steps: Change SimpleServer into a first try at a CalcServer Use SimpleServer.java as the starting point for your server. Give the class and the file an appropriate name ( “CalcServer” ). Change the constructor call appropriately. Make it part of the socket package Modify the main method to refer to CalcServer rather than SimpleServer Get rid of the Vectors containing the cookies Get rid of the code that reads the text files into the Vectors Get rid of the sendGreeting() method To begin with replace the calls to sendGreeting() with System.out.println calls that will indicate that a call has been made. You should now have the bare bones of your first Calculator server but at the moment it still responds to “hello” and “goodbye” – the command that the SimpleClient sends. Start your CalcServer in a separate DOS box, current directory at the Calc.java level using the command: java socket.CalcServer Try your server with the SimpleClient – nothing much will happen at the client but you should see the extra lines of print out on the server. The next task is to decide on the format of the instructions that your PlainCalculatorImpl (looks like the client ) will send to your CalcServer. There are various possibilities and they will all need some form of Java string manipulation: - “plus 1 2” - “+ 1 2” The Java class that will be most useful for manipulating these command strings when they arrive at the server is the StringTokenizer class. The following fragment from the Java Documentation shows how to use this class: StringTokenizer st = new StringTokenizer("this is a test"); while (st.hasMoreTokens()) { System.out.println(st.nextToken()); } This will print out the four words “this”, “is”, “a” & “test” Modify your CalcServer so that the incoming string is used to create a StringTokenizer and then use the nextToken() method of the StringTokenizer to extract the command ( first token ) from the incoming string. Modify the behaviour of the server so that it simply prints out this command. The SimpleServer that we used as a starting point shuts down the socket after each request which is exactly what SimpleClient expects. You will need to modify your CalcServer to avoid this: Change the way that the while() loop handles the opening and closing of connections: In SimpleServer a new connection was established for each request and these two lines are inside the while() loop: | System.err.println("Server is ready."); theClient = new BetterSocket( theSocket.accept()); These lines need to be placed before the while() loop in order that the original connection is used throughout the session. The closing of the socket should be removed altogether. Compile this class Use calc.PlainCalculatorImpl to begin socket.PlainCalculatorImpl You are now ready to start work on your socket.PlainCalculatorImpl class. This class needs to implement the two abstract methods newConnection() and reuseConnection() that are defined in calc.CalculatorImpl. The purpose of these methods is to ensure that a socket is opened the first time you use the calculator but that it is then reused for each new calculator that you bring up using the menus. The newConnection() method needs to open a connection to the server using the hostname or IP address that the user has entered and that has been passed in via the host parameter Declare a variable of type BetterSocket at the head of the class file and set it to null. By putting it at the head of the file you make sure that it will stick around. Define a PORT constant exactly the way that your CalcServer does. If the port values are not the same there will be no communication. These are both defined at the head of the file Use a try/catch to give variable a value try{ theSocket = new BetterSocket(host, PORT); return true; } catch( Exception uh ){ System.out.println( uh ); return false; } You are forced to use a try/catch because there might be an error on the network and returning true makes sure that you will not be called again. Note that the try/catch is used to cause the correct true/false value to be returned. The above code is put within the newConnection() function The reuseConnection() simply retrieves the BetterSocket from the previous calcualtor and assigns this to the variable theSocket. The parameter to this method is declared as being of type calc.CalculatorImpl but what is being passed in will be an instance of a sub-class ( eg socket.PlainCalculatorImpl ). We will need to cast the parameter to be of the more restrictive type in order that we can retrieve a BetterSocket from it: theSocket = //we are using the passed-in value ((PlainCalculatorImpl) impl) //cast impl .theSocket; //so we can retrieve socket The action for add() will look something like what is below. We are not trying to return a result from the server yet – simply to send the command so we are returning 42 to remind you that there is still work to do! : public double add( double a, double b ){ try{ theSocket.println("plus " + a + “ “ + b ); } catch( Exception uh ){ System.out.println( uh ); } return( 42); } Notice the spaces to separate the command and the parameters. Change this action for add, subtract, divide and multiply, in socket.PlainCalculatorImpl.java. If you fire up a Calculator now and select the Socket Distribution you should see the commands printed out on the server screen. CalcServer is the server, Calc.java launches the client interface in the form of a GUI calculator. So run the server first and then run the client. Fix up the server so that it returns the result of a calculation What remains for you to do is to cause the server to return a value to the PlainCalculatorImpl. The actual returning of the value requires a BetterSocket readLine() call but the value needs to be passed as a String. The hard part about this is keeping track of numeric values as they move between the basic double type and the Java String class. The key to this is the Java Double class: If you have a String and you want a double you need to create a Double object from the String and get the value with the doubleValue() method: double x = (new Double( xAsString)).doubleValue(); To go in the opposite direction you need to create a Double object from the double and then use the toString() method: String xAsString = (new Double(x)).toString(); Using this approach you will need to modify your CalcServer and PlainCalculatorImpl so that the calculation is actually performed remotely. 2. Lab 2-B: Advanced - Developing the Socket-Based Calculator server Adding Threading behaviour to your Server The Socket-based calculator server that you developed last week has a major draw-back – only one use can access the server at a time. The first exercise this week is to make this server multithreaded To accomplish this you need to become really clear about the series of states that the server moves through whilst it is servicing clients, and the relation of these states to the code you are executing. Your server will also need to be re-defined, as described in the lecture, as a class that implements Runnable. This is a pseudo code description of a multi-threaded server along with the Java code that corresponds to each step. Forever Accept a new client Socket s = theSocket.accept(); Start a new thread for that client (new CalcServer(s)).start(); Until client quits run() method in CalcServer Do calculations End Until Kill Client Thread End Forever You will need to make the following ( general ) changes to your socket.CalcServer: Make the class implement the Runnable interface: Add the implements statement Add a public void run() method. This method needs to include all the existing code that processes a single calculation. Broadly speaking this will begin with the readLine() request, through to the "Processing complete” message, along with the while(true) construct so that your server will keep on doing calculations. You may find it useful to print out the name of the thread that is currently processing a request. You can do this by adding the following expression to the “Processing request…” message: Thread.currentThread() Get init() organised What you did above will have reduced your init() method to just a few lines. init() is called when you first fire up your CalcServer and its behaviour can be seen in the pseudo-code above – namely to sit waiting for clients to arrive and to fire up a new thread for each client: Create a new ServerSocket and say that the server is ready In a while(true) loop accept new client connections and fire up a new thread for each one: Socket s = theSocket.accept(); new Thread( new CalcServer(s) ).start(); This will leave a couple of lines of the original code that need to be dealt with: The original accept() call. This is now being done in the line above so we can delete this line. Write a couple of constructors for the CalcServer class: One is the constructor that is being called in the new Thread call above. This constructor is being told how to talk to the new client via the Socket parameter that is being passed in. The constructor needs to create the new BetterSocket that will be used to communicate with the client. public CalcServer( Socket s ){ try{ theClient = new BetterSocket(s);…. Because we have defined this one constructor we are also forced by the compiler to define the default constructor – this is used once in the CalcServer main(). public void CalcServer(){. . . Handle client shutdown You have probably already noticed that when the client Calculator exits the server generates an exception. The exception is a SocketException and occurs because the remote end of an open connection simply “disappeared”. This behaviour, which shuts the server down, is clearly unacceptable in a multi-threaded, possibly multi-user, server. The server needs to catch that specific exception and then kill the specific thread that is handling that connection. To gracefully kill a thread you need a boolean that the main while() statement keeps checking. ( instead of while(true)). Changing the value of this variable will cause the while() loop to exit and bring this thread to the end of its run() method. At this point the JVM will gracefully clean up. The response to catching the SocketException clearly needs to be to set this boolean appropriately. A Socket-Based Statistical Calculator The issue here is to use Object Serialisation to pass the Vector of data items to the server. Of course there are other ways to do this – such as maintaining the Vector at the server but we are setting out with two goals in mind: Making our server based solution to Statistical calculations have the same interface as the local one we have already implemented Learning to use Object serialisation! The Statistical server Take a new copy of BetterSocket.java from the Lab2-B directory. This new copy has the readObj() and writeObj() methods implemented in it that were discussed in lectures. Make sure that you put the new copy in your socket directory. Compile this file. Inspect BetterSocket.java and note that readLine() and println() now also use the underlying socket as an Object stream. It is not possible to mix Objects and plain data on a Java Socket. Modify your CalcServer.java so that it recognises the two new statistical commands “sum” and “average”. These commands will be passed to the server as Strings, just as before, but they will be followed by the Vector object rather than by the extra Strings that were used by the PlainCalculator. Depending on how you wrote your server you will need to take account of the different way in which these two sets of commands work. It is no good calling nextToken() when the next thing is a serialised object. This method call, on the modified BetterSocket, will read in the Vector that is being passed: Vector v = (Vector)theClient.readObj(); Arrange that the server performs the necessary calculations with this Vector. You can copy the code fragments for this from calc.StatCalcualtorImpl You have a choice as to whether you send the result back to the client as before ( as a String ) or to use your new ability to serialise objects by sending a Double. The Statistical Impl Save a copy of calc.StatCalculatorImpl in your socket directory and begin to modify it: Change the package Make it extend the PlainCalculatorImpl class that you wrote in the last lab. Make sure that implements calc.StatCalculatorInterface Whenever we select an interface from the menu ( eg Statistical ) we will already have made a particular distribution choice. This means that the Statistical calculator will never need to make a new connection to the server – only reuse the one that was there. Hence we only need implement resueConnection – we can borrow the code from our PlainCalcualtorImpl. Your methods will now send the Vector to the server – you can delete the code for summing and averaging the vector – this is now being done at the server! To send an object you use the writeObj( Object )of the new BetterSocket. 3. Report (2-3 pages): 1. Describe the Code: Describe your observations on socket based distribution (What it is? How to achieve it? etc) in a report and document all modifications made in the code. 2. Reverse Engineering: Generate the class diagram of the codes (Lab2-A, Lab2-B) using the Rational Rose and include the screenshots in this report. Describe the strength and the weakness of the implementation design. 3. Improvement: Identify and describe new interesting features to be added to the implementation design (Lab2-A, Lab2-B).