CPE 401/601 Computer Communication Networks

Spring 2009

Network Lab 1 : Layered, Half-Duplex Communication

Due on Tuesday, Feb 17 at 12:00 pm

This assignment involves development of C code to support a half-duplex layered communication system. You are provided with the interface protocols (C function prototypes) for each of the layers, your job is to implement these layers - part of this will involve the development of peer-to-peer protocols.

You are responsible for providing the C functions that provide layers 2,3, 4 and 5 (described below). You do not need to provide a layer 1 implementation - a sample along with a sample main program is provided. When testing your submission, we will use our own layer 1 that may be very different from the one included here - make sure your code does not depend on any specific layer 1 implementation!

Acknowledgement: The assignment is modified from Dave Hollinger.

Layer 1: read/write a single byte (do not implement!)

IMPORTANT: Layer 1 does maintain ordering, so that the receiver will receive the first byte sent, before the second byte sent. You should assume that any layer 1 implementation maintains the order of individual bytes.

The layer1 interface protocol is specified here, but you don't need to implement this layer. A sample implementation is provided that will support testing of your code - the sample layer will support half-duplex communication using a pipe.

The layer1 API is shown below - each function returns an int that indicates the number of bytes read/written, in this case the return value should be 1 if everything goes well, -1 indicates some kind of error.

int l1_write(char b);

Writes the byte specified by b. Returns 1 on success or -1 on error.

int l1_read(char *b);

Reads one byte and copies the byte to the address specified by b. Returns 1 on success, -1 on error.

Layer 2: read/write a chunk

Layer 2 provides transmission/reception of a chunk. A chunk is defined as a sequence of bytes whose length is no greater than 16 bytes. Each of the bytes in a chunk can have any value, the only restriction is that there are no more than 16 bytes. It is valid to send/receive a chunk of size 0!

Note: The term "chunk" is just being used so we have a formal name for the payload of layer 2, there is no such (well-defined) use of the term "chunk" in the real world.

The important issue in layer 2 is that there must be some agreement between the sender and the receiver as to what constitutes the chunk (how long is the chunk?, where is the end?). You must come up with a peer-to-peer protocol and implement the protocol here. The only way for l2_write to send anything is by using l1_write, and the only way l2_read can retreive a byte from the sender is by calling l1_read.

The return value of each layer 2 function indicates the length of the chunk sent or received (in bytes) upon success, or the value -1 to indicate failure. This return value should reflect the number of bytes in the original chunk, it doesn't count any extra bytes your layer 2 might use as part of the peer-to-peer protocol.

IMPORTANT: The service provided by layer 2 is the transmission of a chunk. You can use layer as many times as you want to send/receive a chunk (can be more than 16), the length restriction is on the payload only. The content of a chunk can be anything at all, there is no restriction on the values of the indiviual bytes that make up a chunk.

int l2_write(char *chunk, int len);

Sends a chunk that consists of the sequence of bytes starting at the address specified as the first parameter (chunk) and having length len. Returns len on success, -1 means an error occurred. You need to handle all errors that can be detected here, this means you need to check for valid values of len, and that you check the return value each time you call l1_write.

int l2_read(char *chunk, int max);

Reads a chunk and stores the incoming byes starting at the address specified by the pointer chunk. No more than max bytes will be put in to memory, so max limits the size of the chunk read. If a chunk is received by l2_read that would require more than max bytes, l2_read should return -1 (error). Upon successful reception of a chunk, the size of the chunk (the number of bytes stored in chunk) is returned.

NOTE: Make sure that your l2_read does not allow the sender to overflow the buffer chunk! It's not enough to recognize when this has happened and return an error, you must not store anything beyond the maxth location of chunk (the caller of l2_read is telling you how large the buffer chunk is, you can't assume it's any larger than max).

Layer 3: read/write a message

Layer 3 provides the capability to send and receive messages, where a message is defined as any sequence of bytes. There is no length limitation at layer 3, so a sender can ask layer 3 to send any size message. The general idea is that your layer 3 implementation needs to use layer 2 to send and receive small chunks, perhaps many times in order to transmit/receive a complete message.

The main issue at layer 3 is creation of a peer-to-peer protocol that uses chunks to communicate, and the receiver needs to know when it has reached the end of a message. Your l3_write can only call l2_write to send data (not l1_write), and your l3_read can only call l2_read.

int l3_write(char *msg, int len);

Sends a message of length len. The bytes sent are specified by the pointer msg. Returns the number of bytes of the message sent on success (should be len), -1 means an error.

int l3_read(char *msg, int max);

Reads a message and stores in memory starting at the address specified by the pointer msg. No more than max bytes will be put in to memory, so max must limit the size of the message read. Return value is the size of the message received or -1 on error. If a message is received by l3_read that would require more than max bytes, l3_read should return -1 (error).

Layer 4: read/write a message with error detection

Layer 4 adds simple error detection to the services provided by layer 3. The function prototypes (which define the interface protocols) looks the same as the layer 3 prototypes, the only difference is that the layer 4 read function should return a -1 (error) if it detects an error in the received message. The errors we are looking for here involve transmission errors, we want to try to make sure that the message received is the same as the message that was sent.

Error Detection: The simplest approach is to use a checksum. To use a checksum you simply add together all the bytes of the original message (just treat each byte as a number) and the checksum is the sum modulo 256 (to fit in a single byte). The result is a single byte that can be sent along with the message data, and the receiving end can go through the same steps (computing the checksum) and then compare the received checksum to the computed checksum. If they don't agree - there was an error. If the checksums do agree, it is likely there was no error (but it's not a certainty). Many network protocols use checksums to detect errors, although in general they use checksums larger than a single byte to improve the accuracy of the error detection. Feel free to use whatever you want to detect errors, a single byte checksum is just the easiest to implement. When testing your code we will use a buggy layer 1 that does introduce errors, so make sure your error detection works!

int l4_write(char *msg, int len);

Sends a message of length len. The bytes sent are specified by the pointer msg. Returns the number of bytes of the message sent on success (should be len), -1 means an error.

int l4_read(char *msg, int max);

Reads a message in to memory starting at the address specified by the pointer msg. No more than max bytes will be put in to memory, so max must limit the size of the message read. If a message is received by l4_read that would require more than max bytes, l4_read should return -1 (error). Some error detection mechanism is used to detect transmission errors. If such an error is detected by l4_read, a -1 will be returned. Upon successful reception of a message, the size of the message (the number of bytes stored in msg) is returned.

Layer 5: read/write a student struct

Layer 5 will provide higher layers with a mechanism for sending and receiving a simple C struct (defined below). The idea is that the sending function (l5_write) is given the address of a struct to be sent, and the corresponding call to l5_read in the peer will create an indential struct. Both peer processes have the definition of the struct ahead of time, and the functions are written with this knowledge.

Your layer 5 implementation should use layer 4 for sending and receiving messages (nothing else!). Once again, you need to come up with a peer-to-peer protocol so that your layer 5 functions know what to expect and can work together. It doesn't matter what your peer-to-peer protocol is, as long as your l5_write works with your l5_read.

Here is the definition of the C struct that layer 5 deals with:

typedef struct {
   char *firstname;
   char *lastname;
   int id;
} student_rec;

Note that firstname and lastname are pointers to null terminated C strings. Both of these strings can be up to 80 characters (plus a null terminator, so the maximum memory used by either is 81). id is a 4 byte integer.

int l5_write(student_rec *stu);

Sends the structure pointed to by stu to the receiver. Returns a 1 on success, -1 means an error.

int l5_read(student_rec *stu);

l5_read uses l4_read to read (somehow...) a student_rec struct. and puts the received field values (firstname, lastname and id) in the student_rec pointed to by stu. l5_read assumes that stu points to memory that has been allocated and is large enough to store the two pointers and the int. l5_read will allocate memory of the appropriate size for the firstname and lastname, it is the responsibility of the caller to free this memory if doing so is important. The return value will be 1 if everything went OK, otherwise l5_read returns -1 (error).

Sample Code and Testing

Included is a program that should give you an idea of how to test your code. The program hw1test.c can be used to test your layer 5 code (which should use your layer 4 code, which should use your layer 3 code, ... ).

IMPORTANT: Although the sample code only accesses the layer 5 functions directly, it is necessary that your layers are independent and can work with any other implementation of other layers. For example, we might use a different layer 2 implementation with your layer 3 and 4 code - everything should work correctly! Also keep in mind that when we test your code we will generate error conditions (including introducing some simulated transmission errors by providing a buggy layer 1).

The main program calls layer 5 to send or receive a student record depending on whether it finds command line arguments present. If there are 3 command line arguments the program assumes it is a sender and sends argv[1] as a firstname, argv[2] as the lastname and argv[3] as the id. If no command line arguments are detected, the program tries to read a student_rec using l5_read. If everything works, the reader program will print the record values to STDOUT. Using the sample layer 1 code included, you can run the program as a sender and pipe the output of the program to another copy of the program running as a receiver:

> ./hw1 Joe Student 12345 | ./hw1
Name: Joe Student
ID: 12345

The code is available here: hw1test.c

There is also a test program that can be used to test layer 3 directly (and layer 2 indirectly). This might give you more ideas about how to test each layer (don't rely on the hw1test.c program alone!). Both of the test programs and a sample Makefile are available here: sample code.


You must provide us with one file for each of the 4 layers you will write. The layer 2 code must be in a file named l2.c, the layer 3 code in l3.c, and so on. Note that each of these files should have both a read and write function (l2.c should have l2_read() and l2_write(), ...).

NOTE:If you've never split a C program among multiple files before - here is how you would build the program. Assuming you save the sample main/layer 1 code in a file named hw1test.c,you can use gcc to build the program like this:

> gcc -o hw1test hw1test.c l2.c l3.c l4.c l5.c

The executable program produced will be hw1test, so you could then test the sender with something like this:

> ./hw1test joe
smith 1234

You must also include in your submission a file named README that includes your name and a brief description of your submission, including the name of each file submitted along with a one line description of what is in the file. You should also mention what Operating System you used to develop the code, and any special instructions for building your code, problems you had or anything else you think might be helpful to us. You do not need to provide any code for layer1 or a main program, we will supply our own when testing your code. You must also have your name in each file that you submit including source code files.


Your project will be tested to make sure it works properly - a large part of this testing will make sure that your functions generate errors (return -1) when appropriate. Here are the specifics of the grading:

layer 2 20%
layer 3 20%
layer 4 20%
layer 5 20%
Style/Code structure, etc. 20%

NOTE: 20% of your homework grade depends on the how well we can understand your code. For this assignment, it would be hard to create crummy/sloppy code (since you are forced to provide 8 individual functions). Feel free to write more than the 8 required functions, but keep in mind that we will test each of your layers in isolation (so each file needs to be a complete implementation of a layer).

The tests for each of your layers will be run independently, so errors in your layer 2 code will not propogate to your layer 3 code...

Submitting your files

Submission of your homework is via WebCT. You must submit all the required files in a single tar or zip file containing all the files for your submission.