NetProg 2002

Homework 1: Layered Half-Duplex Communication Software
Due Date: Friday, Jan 25th (by 11:59PM)

Submit to netprog-submit@cs.rpi.edu with the subject line "1"
Complete Submission instructions are here


This homework involves the 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 and 4 (described below). You do not need to provide a layer 1 implementation - I've included a sample along with a sample main program. 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!

.

Layer 1: read/write a single byte.

IMPORTANT: Layer 1 does maintain ordering, so that the receiver will receive the first byte sent, before the second byte sent. You can (and 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 write this layer - just use it. 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 message

Layer 2 provides transmission/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 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 message sent or received (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 no longer than 999 bytes. The content of messages can be anything at all, there is no restriction on the values of the indiviual bytes that make up a message.

int l2_write(char *b,int n);

Sends a message that consists of the sequence of bytes starting at the address specified as the first parameter (b) and having length n. Returns n 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 n, and that you check the return value each time you call l1_write.

int l2_read(char *b,int max);

Reads a message and stores the incoming message starting at the address specified by the pointer. b. No more than max bytes will be put in to 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 b) is returned.

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


Layer 3: read/write a message with with error detection

Layer 3 adds simple error detection to the services provided by layer 2. The function prototypes (which define the interface protocols) looks the same as the layer 3 prototypes, the only difference is that the layer 3 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 l3_write(char *b,int n);

Sends a message of length n. The bytes sent are specified by the pointer b. Returns the number of bytes sent on success, -1 means an error.

int l3_read(char *b,int max);

Reads a message in to memory starting at the address specified by the pointer b. 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 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 will be returned. Upon successful reception of a message, the size of the message (the number of bytes stored in b) is returned.


Layer 4: read/write a named value

Layer 4 will provide higher layers 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 a named value).

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

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

Sends the named value to the receiver. Returns a 1 on success, -1 means an error.

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

l4_read uses l3_read to read a named value and on success returns the value 1, and puts the name of the received value at name and the value at value. The pointers namelenptr and valuelenptr are reference the sizes of the two buffers when l4_read is initially called, you must use these values to make sure you don't overflow the buffers. Upon successful return, *namelenptr should indicate the number of bytes in the name received and *valuelenptr should indicate the number of bytes in the value received. l4_read must return a -1 on error.


Sample Code and Testing

Included below is a sample main program that should give you an idea of how to test your code. The code is also available here: hw1.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, 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 test code below might be used as part of an application that requires that a username and password be sent from a client to a server. The username is sent followed by the password. The following main program calls layer 4 to send or receive these messages depending on whether it finds any command line arguments present. If there are command line arguments the program assumes it is a sender and sends argv[1] as a username, followed by argv[2] as a password. If no command line arguments are detected, the program first attempts to read a username using layer 4, then reads an password string, and prints them both out 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 username password | ./hw1

The code is also available here: hw1.c

// Netprog 2002 HW1 test program.
//
// This code can be used to test your layer 4 implementation
//   (which in turn should use your layer 3 implementation, etc.)
//
// Included is a set of functions that can be used as a layer 1
//   implementation, and a main program.
//
// Question, suggestions, etc. should be sent to netprog@cs.rpi.edu

#include <stdio.h>

//------------------------------------------------------------------
// Sample layer 1 implementation - this can be used to
// provide half-duplex communication by using the shell to
// create a pipe between a sender process and a receiver process.

// sample l1_read just calls read on stdin
int l1_read( char *b) {
  return(read(0,b,1));
}

// sample l1_write just calls write to stdout
int l1_write(char b) {
  return(write(1,&b,1));
}

//------------------------------------------------------------------
// layer 4 prototypes

int l4_write(char *, int , char *, int );
int l4_read(char *, int *, char *, int *);


//------------------------------------------------------------------
// Main program. This program can be a sender or a receiver,
//  it depends on how many command line arguments are supplied
//  when the program is run. 

int main(int argc,char **argv) {

  char namebuf[101];
  char valuebuf[101];
  int namelen;
  int valuelen;

  // If there are 2 command line arguments then this program will send
  //   the first (argv[1]) as the username and the second as the password
  //   It uses l4_write to send each as a named value...
  // If there are not 2 command line arguments, the program assumes it should
  //   be a reader, so it calls l4_read to get the named values.

  if (argc!=3) {

    // I'm a reader - read the first named value

    // IMPORTANT: l4_read requires that namelen and valuelen have values that
    //    indicate how large the buffers namebuf and valuebuf are. They must
    //    be initialized before calling l4_read.

    namelen = 100;
    valuelen = 100;

    if (l4_read(namebuf,&namelen,valuebuf,&valuelen)==-1) {
      fprintf(stderr,"Reading error\n");
      exit(1);
    } 

    // we expect C strings (with null termination), but make sure each is null terminated
    // (what if a bad guy sent the message?) before using as
    // a C string

    namebuf[namelen]='\0';
    valuebuf[valuelen]='\0';

    // Our application level protocol expects the first value to
    // be the username - make sure the name sent matches this.

    if (strcmp(namebuf,"username")!=0) {
      fprintf(stderr,"Error - first value received was not named \"username\"\n");
      exit(1);
    }

    // We got something named "username", print it out...

    printf("Username: %s\n",valuebuf);

    // now get the password

    // namelen and valuelen need to be re-initialized, otherwise they
    // will have the values that reflect what happened the last time we called
    // l4_read
    namelen = 100;
    valuelen = 100;

    if (l4_read(namebuf,&namelen,valuebuf,&valuelen)==-1) {
      fprintf(stderr,"Reading error\n");
      exit(1);
    } 

    // we expect a C strings - make sure each is null terminated
    // (what if a bad guy sent the message?) before using as
    // a C string

    namebuf[namelen]='\0';
    valuebuf[valuelen]='\0';

    // Our application level protocol expects this value to
    // be the password - make sure the name sent matches this.

    if (strcmp(namebuf,"password")!=0) {
      fprintf(stderr,"Error - secondt value received was not named \"password\"\n");
      exit(1);
    }


    // OK - We got something named "password", print it out...

    printf("Password: %s\n",valuebuf);

  } else {


    // I'm a writer (argv == 3)
    //  the first command line argument is the username, so send it
    // using l4_write. Make sure the username is not more than 100 bytes
    // (this is part of the application level protocol - usernames can't
    // be more than 100 bytes).

    if (strlen(argv[1])>100) {
      fprintf(stderr,"Error - username is too long\n");
      exit(1);
    }

    // length is OK - send it
    // NOTE: why add one to the lengths? This way the receiver is sent the
    // terminating null.
    if (l4_write("username",strlen("username")+1,argv[1],strlen(argv[1])+1)==-1) {
      // something went wrong when sending
      fprintf(stderr,"Error sending username\n");
      exit(1);
    }

    //  the second command line argument is the password, so send it
    // using l4_write. Make sure the password is not more than 100 bytes
    // (this is part of the application level protocol - passwords can't
    // be more than 100 bytes).

    if (strlen(argv[2])>100) {
      fprintf(stderr,"Error - password is too long\n");
      exit(1);
    }

    // length is OK - send it
    if (l4_write("password",strlen("password")+1,argv[2],strlen(argv[2])+1)==-1) {
      // something went wrong when sending
      fprintf(stderr,"Error sending password\n");
      exit(1);
    }
  }
  return(0);
}

Deliverables

You must provide us with one file for each of the 3 layers you will write. The layer 2 code must be in a file named l2.c, the layer 3 code in l3.c and the layer 4 code in l4.c. 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 hw1.c,you can use gcc to build the program like this:

> gcc -o hw1 hw1.c l2.c l3.c l4.c

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

> ./hw1 uname passwd

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.

Grading

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 15%
layer 3 25%
layer 4 35%
Style/Code structure, etc. 25%

NOTE: 25% of your homework grade depends on the how well we can understand your code.

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 email, the general idea is to send an email message with all your files as attachments. There is an automated email submission system that will respond to your submission right away, so you will have a record that we got your files.

All projects must be submitted via email to netprog-submit@cs.rpi.edu. The subject line of the submission message should contain a single number indicating the project number (1 for HW1, 2 for HW2, etc.). You must include your files as attachments, feel free to send a zip-file or a tar file.

Don't send compiled code!

You can expect a return email indicating receipt of your project submission immediately. This receipt will include a list of all the files that were successfully extracted by the submission script - please look over the receipt carefully to make sure your submission worked.

Multiple Submissions: You can resubmit up to 10 times for each project, we will always grade the last submission received unless you tell us otherwise.