This assignment makes use of the files contained in this zip file. This assignment is due Wednesday, February 19.
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 this client/server pair. 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 float
value to be sent to it by the server. A 1
bit tells the client to expect a long
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 0b00111001
), then the client should expect to receive, in order, one long, then two floats, then three longs, followed by one float.
The eight bytes of a long are sent by the server in little-endian byte order. That means that the server sends the least significant byte of a long first, followed by the second-least significant byte, etc. for all eight bytes of the long (for example, see this or this picture). Java stores long
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 float 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 second-least significant byte of the float, followed by the least significant byte, followed by the most significant byte, followed by the float's second-most significant byte. Java stores float
values in big-endian byte order.
Immediately after the server sends the last byte from the body of a message, the server will send the message header byte for the next message.
In order to work with the bytes of a long
in little-endian order, or the bytes of an float
in weird-endian order, you need to use Java's ByteBuffer class. The ByteBufer
class has methods that let you convert a long
or a float
into an array of bytes (in big-endian byte order). And there are methods that let you convert an array of four bytes into a float
value or an array of eight bytes into a long
value (again, in big-endian byte order).
The putLong() and putFloat() methods let you convert a long
value or a float
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 float
value 123456789.0
.
byte[] b = ByteBuffer.allocate(Float.BYTES).putFloat(123456789.0F).array();
The following line of code creates an array of eight bytes holding the bytes that represent the long
value 1234567890987654321
.
byte[] b = ByteBuffer.allocate(Long.BYTES).putLong(1234567890987654321L).array();
For the other direction, from byte array to long
or float
value, the wrap() method constructs a ByteBuffer
object from a byte[]
object, and the getLong() and getFloat() methods convert an appropriately sized ByteBuffer
into an long
or float
value. For example, the following line of code returns the float
value of the four bytes in the given byte[]
object.
float n = ByteBuffer.wrap(new byte[]{4, 3, 2, 1}).getFloat();
The following line of code returns the long
value of the eight bytes in the given byte[]
object.
long n = ByteBuffer.wrap(new byte[]{8, 7, 6, 5, 4, 3, 2, 1}).getLong();
When you have a long value stored in a byte[] array, then you can move the bytes around in the array to convert from big-endian to little-endian (or vice versa). When you have a float value stored in a byte[] array, then you can move the bytes around in the array to convert from big-endian to weird-endian (or vice versa). The server will be converting from big-endian to either little-endian or weird-endian (look at the Server.java
source code). The client program will be converting from either little-endian or weird-endian to big-endian.
You can experiment with the ByteBuffer API using the following program in the Java Visualizer.
Also, look at the program Server.java
in the zip file 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 NOT read all of its input data into some array or list. That is a very bad strategy, for several reasons. Your program does not know how much data it will receive, so it does not know how much memory is needed to store all the input data (it may be more that what is available to you on the current computer). Your program will not produce any output until all the input data has been received, so your program will appear "dead" and unresponsive. If your program is in the middle of a data pipeline, then your program becomes a bottle neck for the pipeline, blocking all the data that needs to pass through the pipeline. Your program (and almost any filter program) should process its input data as soon as it receives it, and output the processed data right away.
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 long and float 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 (this command captures five messages).
> 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 save a data file from the server, be sure to keep a copy of the server's log_file.txt
along with the data file. The log file will help you determine if your client's output is correct.
In the zip file there is an example data file, its accompanying log file, and the client's results from reading that data. You can use that data as a test case for your client program.
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, your 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 (which sends five messages to the demo program).
> 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.
DO NOT run the server program, the demo program, or your version of the client program, in Microsoft's PowerShell. PowerShell does not like binary streams, and these programs will not run correctly.
If you would like to look at "real world" examples of binary data protocols, here is the binary format for the Internet Protocol Datagram Header and the binary format for the Transmission Control Protocol Header. Notice how they both use a mixture of 1-bit, 4-bit, 8-bit, 16-bit, and 32-bit fields. Here is a third, simpler example, the binary format for the User Datagram Header Format. Here is a more complex example, the binary format of the Network Time Protocol Packet Header
Turn in a zip file called CS336Hw2Surname.zip
(where Surname
is your last name) containing your version of Client.java
.
This assignment is due Wednesday, February 19.