Computer Science 375
Computer Networks

Denison

Project 2, Phases 1 & 2: Reliable Transport

Phase 1 Due: Friday, Feb. 21 @ 11:59PM.
Phase 2 Due: Friday, Feb. 28 @ 11:59PM (tentative).

Introduction

Your task is to implement a reliable, stop and wait (Phase 1) and sliding window (Phase 2) transport layer on top of the user datagram protocol (UDP). You will use IP addresses and UDP port numbers to demultiplex traffic, but not otherwise rely on UDP--in particular you should not rely on UDP's checksum to detect bit errors in packets.

The assignment is split into two phases. In the first phase, you just need to support a single direct connection between two UDP ports, one on the server and the other on the client; you can use the stop and wait protocol for this. The picture below shows how the system should look after Phase 1 is implemented:

diagram of Phase1

In Phase 2, you extend the functionality to support demultiplexing of several connections at the server as well as to support a sliding window with sizes > 1. (Recall that stop and wait is equivalent to sliding window with a window size of 1). The picture below shows how the system should look after Phase 2 is implemented:

diagram of finished assignment

In this assignment, you are provided with a library (rlib.h and rlib.c) and you have to implement some functions and data structures for which skeletons are provided (in reliable.c). You will probably find it useful to look through rlib.h and rlib.c, as several useful helper functions have been provided, including abstracting many of the input and output differences between the above two pictures and the I/O details so that you can focus on the learning goals of the reliable transport protocols.

In general your implementation should:

You will implement both the client and server component of a transport layer, both sides available in the same executable, and selected via command line options. The client will read a stream of data in (either from STDIN, in the first phase, or from a reliable TCP connection in Phase 2), break it into fixed-sized packets suitable for UDP transport, prepend a control header to the data, and transmit this packet to the server. The server will read these packets and write the corresponding data, in order, to a reliable stream (STDOUT in Phase 1, and a TCP connection in Phase 2).

One of the simplifications of this assignment over what we study in TCP is that we are implementing a packet-oriented transport, as opposed to a byte stream transport. With not having to keep track of byte offsets, our sequence numbers and acknowledgements refer to

Packet types and fields

There are two kinds of packets, Data packets and Ack-only packets. You can tell the type of a packet by its length. Ack-only packets are 8 bytes, while Data packets vary from 12 to 512 bytes. The packet format is defined in rlib.h:


        struct packet {
          uint16_t cksum; /* Ack and Data */
          uint16_t len;   /* Ack and Data */
          uint32_t ackno; /* Ack and Data */
          uint32_t seqno; /* Data only */
          char data[500]; /* Data only; Not always 500 bytes, can be less */
        };
        typedef struct packet packet_t;

Every Data packet contains a 32-bit sequence number as well as 0 or more bytes of payload. The len, seqno, and ackno fields are always in network byte order (meaning you will have to use htonl/htons to write those fields and ntohl/ntohs to read them). Both Data and Ack packets contain the following fields:

cksum

16-bit IP checksum (you can set the cksum field to 0 and use the cksum(const void *, int) function (i.e. pass it a pointer/address of a packet and a length) to compute the value of the checksum that should be in there). Note that you shouldn't have to call htons on the checksum value produced by the cksum function--it is already in network byte order.

len

16-bit total length of the packet. This value will be 8 for Ack packets, and 12 + payload-size for data packets (since 12 bytes are used for the header). An end-of-file condition is transmitted to the other side of a connection by a data packet containing 0 bytes of payload, and hence a final len of 12. Note: You must examine the length field, and should not assume that the UDP packet you receive is the correct length. The network might truncate or pad packets, resulting in a difference between the UDP length and the length maintained by you in this field.

ackno

32-bit cumulative acknowledgment number. This says that the sender of a packet has received all packets with sequence numbers earlier than ackno, and is waiting for the packet with a seqno of ackno. Note that the ackno is the sequence number you are waiting for, that you have not received yet. The first sequence number in any connection is 1, so if you have not received any packets yet, you should set the ackno field to 1.

The following fields only exist in a data packet:

seqno

Each packet transmitted in a stream of data must be numbered with a seqno. The first packet in a stream has seqno 1. Note that in TCP, sequence numbers indicate bytes. By contrast, this protocol just numbers packets. That means that once a packet is transmitted, it cannot be merged with another packet for retransmission. This should simplify your implementation.

data

Contains (len - 12) bytes of payload data for the application.

To conserve packets, a sender should not send more than one unacknowledged Data frame with less than the maximum number of bytes, 500. (This behavior is somewhat akin to TCP's Nagle algorithm, which we will discuss in lecture.) Note that this is not the same as limiting the window size in general; this applies only when a "small" packet is sent and not yet acknowledged.

Requirements

Your transport layer must support the following:

Implementation Details

There are two modes of operation of the reliable transport protocol:

The first mode is single-connection mode, and connects, through your reliable transport protocol implementation, the standard input and output of the two endpoint processes together. The second is multi-connection mode, in which one endpoint process accepts TCP (or unix-domain) socket connections and relays them over your reliable transport protocol to a server that, for each demux endpoint, connects to a TCP port or unix-domain socket.

You are provided with a library (rlib.h/rlib.c) and your task is to implement the following seven functions: rel_create, rel_destroy, rel_recvpkt, rel_demux (Phase 2), rel_read, rel_output, rel_timer:

Phase 1

While you could develop this project on any of the Linux machines in Olin 219, we want to plan for testability. Since your endpoints for communication run over UDP, we want to be able to control the link (and in particular, its latency, bandwidth, and loss characteristics) over which the UDP packets are transmitted. Your mininet virtual machine, and the virtual network topology it provides, are a perfect match for this requirement. In addition, since you have root access inside the virtual machine, we can add the dmalloc facility, helpful for debugging memory management through malloc().

We will start with a discussion of how to run your built program (or the provided reference program) and then, in the Getting Started section, discuss getting the software base and making sure you have installed some of the packages for effective development. The discussion from here assumes you have downloaded the base software through Mercurial, made the modifications to the file reliable.c for the implementation of the above set of functions possibly adding other .h and .c files for closely related sets of functions, and built the executable using make, resulting in the executable file reliable.

When you are done with Phase 1, two instances of reliable should be able to communicate with one another. An example of the working program is given here.

On your mininet virtual machine, start up mininet:

mininet@mininet-vm:~/test/reliable$ sudo mn -x

This starts up mininet with the default topology and launches an xterm for each of the two hosts, h1 and h2 (as well as for the switch and the controller). This should be done on a terminal where you have ssh'd into the mininit vm with the -X option to ssh to enable "remote" GUI display.

In the host1 xterm, run:

root@mininet-vm:~/test/reliable# ./reliable 6666 10.0.0.2:5555
[listening on UDP port 6666]
Hello I am typing this on host1.

In the host2 xterm, run:

root@mininet-vm:~/test/reliable# ./reliable 5555 10.0.0.1:6666
[listening on UDP port 5555]
Hello I am typing this on host2.

Now anything typed on host1 will show up on host2 and vice versa. Note that you can use the provided reference executable on either (or both) ends of this communcation to test and help debug your implementation. The reference is an x86_64 linux binary suitable for execution on the mininet architecture.

For debugging purposes, you may also find it useful to run ./reliable with the -d command-line option. This option will print all the packets your implementation sends and receives.

Phase 2

For Phase 2, you will extend your solution to Phase 1 to support two additional features:

  1. A sliding send and receive window larger than one packet, and
  2. Connection demultiplexing.

The first feature is relatively straight-forward. When you run the reliable program with the -w argument, it should set the sender and receiver window sizes to be whatever the supplied argument is. For example, the following command should select a window size of 5:

root@mininet-vm:~/test/reliable# ./reliable -w 5 1111 10.0.0.2:2222
[listening on UDP port 1111]

The value specified for the -w argument is stored in the window field of the config_common data structure. You should access it as cc->window in the rel_create function, and store the value somewhere in the reliable_state structure so you have access to it in other functions.

Connection demultiplexing is used when running the reliable program in server mode, which is selected by the -s switch. For example, the following command runs reliable in server mode:

root@mininet-vm:~/test/reliable# ./reliable -s -w 5 1111 10.0.0.1:2222
[listening on UDP port 1111]

Unlike single-connection mode, which you've been using up until this point, in server mode the argument 10.0.0.1:2222 specifies a TCP, rather than UDP port. At this point reliable may accept multiple connections from different clients on different client UDP ports, all sending packets to port 1111 on the server. The reliable program will get all of these packets, but since they are all destined to the same UDP port, the rlib code doesn't know which connection they belong to. Therefore, received packets will be passed to the function rel_demux.

In server mode, the library never calls rel_recvpkt. Instead, you must look up the rel_t structure for a packet based on the client's UDP sockaddr_storage. You will find the addreq function that compares two sockaddr_storage structures for equality useful here.

In server mode, reliable input and output no longer come from standard input and output. Instead, for each new connection set up, the library creates a TCP connection to the TCP port specified (10.0.0.1:2222 in the example above). There is a utility uc that came with the distribution that allows you to listen to a particular TCP port, so that you can test your library. Just run, e.g., ./uc -l 2222 to listen for one connection on a particular TCP port. (You'll have to run it again in a different terminal if you want to accept more than one connection.)

There is also a client mode, selected by -c. You shouldn't need any special support in your software for client mode, as long as you are using the rel_t structure correctly. Client mode allows you to accept TCP connections and relay them to a reliable server on a particular UDP port. For instance:

root@mininet-vm:~/test/reliable# ./reliable -c -w 5 3333 10.0.0.1:1111
[listening on UDP port 3333]

The above command accept connections on TCP port 3333, and for each connection, allocates a new UDP port and uses that port to talk to a reliable server listening on port 1111. The uc command without the -l flag allows you to connect to a TCP port. For instance, to test the above, run ./uc localhost 3333.

Getting Started

To get started, you will want to launch your mininet virtual machine and then use it to access the Mercurial repository with your code base.

The best way to download the assignment source code is to use Mercurial to clone your user-specific repository, by executing the following command:

    hg clone http://140.141.132.4:8001/cs375-login

where login is replaced by your Olin/MathCS login id. Mercurial (hg) is a powerful version control system that will make it easy for you to checkpoint your work and later browse your history to track down problems if you have introduced a bug. Using hg will also make it easy for you to update your source tree should I need to make corrections/bug fixes to the rlib infrastructure of the project assignment. While use of hg is not required for this class, if you invest the time to learn hg now, you will likely benefit far into the future. A quick search of the web will result in a number of high quality tutorials, or you are always welcome and encouraged to talk with me about it..

If an update to the assignment is required, I will lead you through how to merge the changes in the base with the modifications you have made in pursuing your assignment.

Conceptual Questions

Here are some conceptual questions which may help you better understand the assignment and how to go about implementing it, as well as its relationship to TCP. You don't need to answer these questions in your submission: they are purely for your benefit.

  1. One of the requirements of the assignment is to implement basic flow control (i.e. packets must not be acknowledged until they are outputted via conn_output). How would reliable behave if this requirement was relaxed? Would it function incorrectly? Is flow control purely for the sake of reducing internet congestion?

  2. The data segment of each packet_t is only 500 bytes. Though perhaps trickier to implement, a variable sized field could support sending much more data (up to 65535 bytes) per UDP packet. Assess this alternate approach.

  3. Another requirement of this assignment is to ensure that there is no more than one unacknowledged data frame with less than the maximum number of bytes (akin to TCP's Nagle algorithm). Which kind applications or usage would benefit from this, and which would drastically suffer?

  4. This assignment counts and acknowledges entire packets, while TCP counts and acknowledges at the byte level. Assess the merits of this approach versus TCP's.

  5. List three key limitations of Reliable, and outline how, if at all, TCP addresses them.

Grading

75% of your assignment grade will be functionality/execution tests. 25% of your grade will be based on the quality and readability of your code. We understand readability can be a subjective measure, and don't want to enforce particular coding expectations. But generally speaking, we expect your code to:

A programming language is a language which needs to not only communicate behavior to the computer, but also to a human reader. Writing elegant, easy-to-understand code is a critical skill that we want to continue to develop.

Submitting

To submit the assignment, you must do three things:

Since Dr. B is also an owner of all of the individual repositories, he will be able to pull and update his working repository with your changes to test your code.

Collaboration policy

You should direct most questions to Piazzza, but should not post source code there.

You must write all the code you hand in for the programming assignments, except for code that we give you as part of the assignment and system library code. You are not allowed to show your code to anyone else in the class or look at anyone else's solution. You may discuss the assignments with other students, but do not copy each others' code.

F.A.Q.

The following is merely a working list of logistical or anticipated questions.