Layered Half-Duplex Communication Software

This project involves the development of code to support a half-duplex layered communication system. Recall that half-duplex divides communication into one-way channels, so in this project you will be dealing with a single sender and a single receiver. Below, you are provided with the service interface for each of the layers in our layered communication system, specified as C function prototypes. Your job is to implement all but the bottom-most layer. Note that this implementation will involve the development of the peer-to-peer protocols. You do not need to provide a layer 1 implementation. I have included a sample layer 1 along with a sample main program. Note, however, that when I test your submission, I will use my own layer 1 that will be different from the one included here. ... so make sure your code does not depend on any properties of the layer 1 implementation beyond what is promised at the service interface description.

Service Interface Description

Layer 1: Single Byte Write/Read

Layer 1 provides ordered single byte communication from the sender to the receiver. By ordered, I mean that the receiver will receive the first byte sent before the second byte sent, and so forth. This is true for the provided example layer 1, and will also be true for my submission test layer 1.

Note that the service protocol is specified here, but you do not need to write this layer -- just use it. If you look at the provided layer 1 implementation, you will see that if provides ordered half-duplex communication by using a Unix pipe. You do not need to know anything about how that works to use the provided layer.

Calls

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: Message Write/Read

Layer 2 moves the granularity of transmission to a higher level by providing transmission and reception of a message. A message is simply a sequence of bytes. The important issue here is that there must be some agreement between the sender and the receiver as to what constitutes a message (how long is a message?, where is the end?). You should have encountered these issues in your first project, and this layer 2 is not fundamentally different -- we're just enforcing structure and placing it within the layered communication system. You must design/define a peer-to-peer protocol and implement the protocol here. The only way for the layer 2 functions to send or receive is by using the layer 1 functions, l1_write and l1_read.

When designing your peer-to-peer protocol, keep in mind:

This is a general purpose protocol, and the definition of a message is correspondingly general (just a sequence of bytes, and not necessarily ASCII bytes), so you don't want to preclude some particular byte from being able to appear in a message.

Recall the fundamental network stack concept of encapsulation, wherein we create our peer-to-peer protocol by using headers to convey protocol information. ... remember the envelope analogy

You should be able to draw a picture of the format of the information exchange between peers, as well as describe how it is to interpret that format.

The return value of each layer 2 function indicates teh length of the message sent or recevied (in bytes) upon success, or the value -1 to indicate failure. The only assumption you can make about messages is that the length is no longer than 999 bytes. As alluded to above, the content of messages can be anything at all, including all values in the byte type range of 0 to 255.

Calls

int l2_write(char *buf, int n);

Sends a message that consists of the sequence of bytes starting at the address specified as the first parameter (buf) and having length n. Returns n on success, -1 means an error occurred. You need to handle all errors that can be detected here, including checking the validity of arguments and also checking the return value for any invocation of l1_write.

int l2_read(char * buf, int max);

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

Note: make sure that your l2_read does not allow the sender to overflow the buffer buf! It is not enough to recognize when this has happened and return an error; you must not store anything beyond the max location of buf. The caller of l2_read is telling you how large the buffer buf is, and you cannot assume it is any larger than max. If you don't understand why this is so important, come and speak with me.

Layer 3: Message Write/Read with Error Detection

Layer 3 adds simple error detection to the services provided by layer 2. The service interface for layer 3 looks the same as the layer 2 service interface, the only difference being that the layer 3 read should also return a -1 (error) if it detects an error in the received message. The errors we are looking for here involve transmission errors, wherein one or more bytes might have gotten corrupted. We want to make sure that the message received is the same as the message that was sent.

Error Detection: To accomplish this, we need some form of 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 (treating 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 of computing the checksum and then comparing the received checksum to the computed checksum. ... if they do not agree, there was an error.

Calls

int l3_write(char *buf, int n);

Sends a message that consists of the sequence of bytes starting at the address specified as the first parameter (buf) and having length n. Returns n on success, -1 means an error occurred.

int l3_read(char * buf, int max);

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

Layer 4: Name/Value Pair Write/Read

Layer 4 will provide higher level software with a mechanism for sending and receiving values that have an associated name. The idea is that we could build an application-level protocol that uses layer 4 to send named values between peer processes. For example, a telephone directory service could be built by having clients send a request that could be either a person's name, or phone number; the server could do the appropriate lookup and send back the appropriate result as one of these name/value pairs. (Note that this is simply an illustrative example. I am not asking for any higher level software, not am I looking for anything that turns this one-way communication into something capable of both directions.)

Your layer 4 implementation should use layer 3 for sending and receiving messages. This means you cannot go directly to any of the lower layers. Once again, you need to come up with a peer-to-peer protocol so that your layer 4 functions on the receiver and sender know what to expect and can work together. You have the freedom to design your peer-to-peer protocol as you see fit, as long as your l4_write works with your l4_read.

Calls

int l4_write(char *name, int namelen, char * value, int valuelen);

Sends the name and value pair to the receiver, where namelen specifies the number of bytes in name, and valuelen specifies the number of bytes in value. Returns 1 on success, -1 means an error occurred.

int l4_read(char * name, int * namelenptr, char * value, int * valuelenptr);

The l4_read function reads a name/value pair into the buffers pointed to by name and value. The namelenptr and valuelenptr parameters are in/out parameters that give the address of an integer variable. On input to the function call, the integers pointed to should contain the maximum size of the name buffer and the value buffer, respectively. On successful return from the function call, the integer pointed to holds the actual byte count received for name and value. The return value of l4_read should be a 1 on success and a -1 on error, including the case where the name buffer or the value buffer would overflow.

 

Sample Code

At the following link, you will find a sample C program that should give you an idea of how to test your code. The file is named proj2.c. Note that although the code below only accesses the layer 4 functions, it is necessary that your layers are independent, and can work with any other implementation of other layers. For example, after submission, I may use a different (my own) layer 2 implementation with your layer 3 and layer 4 code. ... and everything should work correctly. Also keep in mind that when I test your code, I will generate error conditions, which must be handled properly (including some simulated transmission errors by providing a bit-level unreliable layer 1).

The comments within the provided sample code should be sufficient to understand and use the sample. However, I would recommend that you do not try and do everything at once. Start by developing your layer 2. You will need your own way of testing that layer 2. Then move on and develop your layer 3, and finally develop your layer 4.

 

Deliverables and Grading

You must provide one file for each of the 3 layers you will write. The layer 2 code must be in a file name l2.c, the layer 3 code in l3.c, and the layer 4 code in l4.c. Each of these files must have their corresponding read and write functions name exactly as specified above and with exactly the signature as given above.

Now would be the time to create a makefile so that you can easily provide the separate compilation units for the l2.c l3.c l4.c proj2.c (or whatever combination and test main program you wish to write), as we demonstrated in class. For purpose of example, I include a link to the Makefile I used for the TCP Echo server and client. If you need help, don't let this be a roadblock ... come and see me.

When you submit this project, you are going to give me the three source files along with a README that includes your name and a brief description of your submission. If there are any special instructions for building your code, include that as well (such as header files or anything else required as compile or link options). You do not need to provide any code for layer 1 or for a main program, as I will supply my own when testing your code.

Each of your three layers will be tested independently, including putting it through its paces with respect to error conditions. There will be a portion of the grade for how well your code passes the set of unit tests designed for each layer, and a portion of style/code structure/readability as follows:

Style/Code structure 25%
Layer 2 30%
Layer 3 30%
Layer 4 15%
The first category will be based on whatever code you turn in, so if you run short on time and cannot complete layer 4, you can still achieve up to 85%. The comments for each layer should include complete information on the design of your peer-to-peer protocol. Your goal on the clarity of such comments should be that, if I were writing a layer x function to receive data being sent by your layer x function, I would be able to do so with no additional information.


All rights reserved, Thomas C. Bressoud and Denison University.
For problems or questions regarding this web contact bressoud@denison.edu.
Last updated: 09/14/06.