Programming Assignment 2
CS 33600
Network Programming
Spring, 2024

This assignment makes use of the files contained in this zip file. This assignment is due Monday, February 26.

This assignment simulates a server process communicating with a client process using a "binary message protocol". In the zip file there is a file Server.java that implements the server. Your assignment is to implement the client program, Client.java.

The server program "sends" messages to the client program by writing the messages to standard output. The client program should "receive" messages by reading them from standard input. So the command line

   > java Server 10 | java Client

uses a pipe to simulate the communication channel from the server to the client (the server's command-line argument 10 tells the server to send 10 messages to the client).

Here is a description of the message protocol used by the server. Each message begins with a single byte that is a "message header". There are three kinds of messages, a "numeric message", a "text message", and an "end-of-transmision" message. After a message header will be the "body" of the message (except for the end-of-transmission message which has no message body; it consists of just the message header byte).

The message header for the end-of-transmission message is 0x80 (in hexadecimal). The meaning of this message header is that there are no more messages and the server will close its output stream, and the client should close its input stream.

The message header for a "text message" has its most significant bit set to 1 and its seven least significant bits set to an integer value between 1 and 127. The meaning of this message header is that it will be followed by the given number of printable ASCII characters (hexadecimal 0x20 through 0x7E), which the client should read.

The message header for the "numeric message" has its most significant bit set to 0. The remaining seven bits are a bit field with the following meaning. A 0 bit tells the client to expect an int value to be sent to it by the server. A 1 bit tells the client to expect a double value to be sent to it by the server. The client should read the bits from least significant bit to most significant bit. So, for example, if the message header in hexadecimal is 0x39 (or binary 00111001), then the client should expect to receive, in order, one double, then two ints, then three doubles, followed by one int.

The eight bytes of a double are sent by the server in little-endian byte order. That means that the server sends the least significant byte of a double first, followed by the second-least significant byte, etc. for all eight bytes of the double (for example, see this or this picture). (Java stores double values in big-endian byte order, so the byte order used by the server program is not the same as Java's byte order.)

The four bytes of an int are sent by the server in "weird-endian" byte order. For the purpose of this assignment, the weird-endian byte order means that the server will first send the third-most significant byte of the int, followed by the second-most significant byte, followed by the most significant byte, followed by the int's least significant byte. (Java stores int values in big-endian byte order.)

In order to work with the bytes of a double in little-endian order, or the bytes of an int in weird-endian order, you need to use Java's ByteBuffer class. The ByteBufer class has methods that let you convert an int or double into an array of bytes (in big-endian byte order). And there are methods that let you convert an array of four bytes into an int value and an array of eight bytes into a double value.

The putInt() and putDouble() methods let you convert an int value or a double value into its appropriate byte values in a ByteBuffer object. Then the array() method constructs a byte[] object from the ByteBuffer object. For example, the following line of code creates an array of four bytes holding the bytes that represent the int value 12345.

    byte[] b = ByteBuffer.allocate(Integer.BYTES).putInt(12345).array();

The following line of code creates an array of eight bytes holding the bytes that represent the double value 12345.0.

    byte[] b = ByteBuffer.allocate(Double.BYTES).putDouble(12345.0).array();

For the other direction, from byte array to int or double value, the wrap() method constructs a ByteBuffer object from a byte[] object, and the getInt() and getDouble() methods convert an appropriately sized ByteBuffer into an int or double value. For example, the following line of code returns the int value of the four bytes in the given byte[] object.

    int n = ByteBuffer.wrap(new byte[]{4, 3, 2, 1}).getInt();

The following line of code returns the double value of the eight bytes in the given byte[] object.

    double d = ByteBuffer.wrap(new byte[]{8, 7, 6, 5, 4, 3, 2, 1}).getDouble();

You can experiment with these ByteBuffer methods using the following program in the Java Visualizer.

Also, look at the file Server.java for more example code that uses ByteBuffer and byte[] objects.

Write a program Client.java that implements the receiving end of the above protocol. Besides implementing the above protocol, your Client.java program should do a few other things. The client should keep track of how many bytes it is receiving from the server. After receiving an end-of-transmision message, the client should print out its count of the total number of bytes it received from the server. If the client should detect an end-of-file condition before receiving an end-of-transmission message, the client should print an error message that includes the number of bytes it has received so far from the server. The client should also print to standard output the contents of each text message and each numeric message.

Your client program should read its standard input stream one byte at a time (using the read() method from InputStream). After every byte, your program should check for end-of-file. Your program should not assume that it gets reliable data from the server. The data from the server may unexpectedly end at any point in the input stream. Your program should not crash on an unexpected end-of-file.

To help you debug your Client.java program, each time the server program is run it logs all the messages it sends to the client into a text file called log_file.txt. The log file contains very verbose versions of the messages sent to the client. The log file shows the message headers in binary number notation, it shows the double and integer numbers in both decimal and hexadecimal formats (in the transmitted byte order), and it tells you the length of the text messages in decimal format.

Each time you run the server program, it creates a random set of messages. If you want to test your client program with a repeatable set of messages, run the server one time and capture its (binary) output in a file.

   > java Server 5 > data

Then run your client as many times as you want with the data file as its input.

   > java Client < data

When you do this, be sure to keep a copy of the server's log_file.txt. It will help you determine if your client's output is correct.

You can test that your client program handles an unexpected end-of-file by taking a saved data file and deleting some data from the end of the file. When you feed that truncated data file into your client program, the program should report that it detected an unexpected end-of-file.

In the zip file for this assignment you will find an executable jar file demo program called client_demo.jar that you can use to demo this assignment. You can pipe the output from Server into the demo program with this command-line.

     > java Server 5 | java -jar client_demo.jar

Or you can run the demo program on a saved data file with this command-line.

     > java -jar client_demo.jar < data

Your version of Client.java should behave exactly like the demo version.

Turn in a zip file called CS336Hw2Surname.zip (where Surname is your last name) containing your version of Client.java.

This assignment is due Monday, February 26.