cnet v4.0.4  

home topology files command‑line options the core API FAQ download and install

A complete stopandwait Data-Link Layer protocol

At the risk of stealing the thunder of other professors or instructors, we finally present a complete example of the stopandwait Data-Link Layer protocol. No effort is made to describe cnet features that we've seen above, nor how the protocol works - please consult a good undergraduate textbook on data communications and computer networking. The example is imaginatively named stopandwait.c, supported by STOPANDWAIT.

This implementation is based on Tanenbaum's `protocol 4', 2nd edition, p227 (or his 3rd edition, p205). This protocol employs only data and acknowledgement frames - piggybacking and negative acknowledgements are not supported.

We first define some global types, data structures, and variables for this protocol. It is important to understand that each of these is unique to each of the nodes in the simulation. Although each of the nodes will typically use the same source code file, each node has its own local copy of its variables. It is not possible for one node to modify the variables in another node. The only way for the nodes to communicate is via the Physical Layer.

#include <cnet.h> #include <stdlib.h> #include <string.h> typedef enum { DL_DATA, DL_ACK } FRAMEKIND; typedef struct { char data[MAX_MESSAGE_SIZE]; } MSG; typedef struct { FRAMEKIND kind; // only ever DL_DATA or DL_ACK size_t len; // the length of the msg field only int checksum; // checksum of the whole frame int seq; // only ever 0 or 1 MSG msg; } FRAME; #define FRAME_HEADER_SIZE (sizeof(FRAME) - sizeof(MSG)) #define FRAME_SIZE(f) (FRAME_HEADER_SIZE + f.len) int ackexpected = 0; int nextdatatosend = 0; int dataexpected = 0; MSG lastmsg; size_t lastlength = 0; CnetTimerID lasttimer = NULLTIMER;

We first declare a type, MSG, to receive a message from the node's Application Layer. We do not know, nor care, what will be in these messages, and so it is reasonable to declare them as an array of bytes of some maximum length, MAX_MESSAGE_SIZE. We also declare a type, FRAME, to carry the messages across the Physical Layer. Each instance of FRAME consists of two parts - firstly, the frame header consisting of four fields, and then the frame body or payload which is actually a message. We define two txtual constants in C, FRAME_HEADER_SIZE and FRAME_SIZE(f) to simplify the coding of our protocol. We finally define six global variables - three to keep a copy of, and remember atttributes of the last message received from the Application Layer, and three integers to track the sequence numbers of the stopandwait protocol. Note that the variables lastmsg, lastlength, lasttimer, nextdatatosend, and ackexpected are all required by the sender of the protocol, and that only dataexpected is required by the receiver. However, because each node executes its own copy of the compiled code, using its own variables, and at any time takes on the role of either sender or receiver, the approach of defining all variables together is considered reasonable.

We next look at the mandatory reboot_node() function, and the simple handler of EV_DEBUG1 which simply prints the runtime state of the stopandwait protocol.

EVENT_HANDLER(reboot_node) { if(nodeinfo.nodenumber > 1) { fprintf(stderr,"This is not a 2-node network!\n"); exit(1); } CHECK(CNET_set_handler( EV_APPLICATIONREADY, application_ready, 0)); CHECK(CNET_set_handler( EV_PHYSICALREADY, physical_ready, 0)); CHECK(CNET_set_handler( EV_DRAWFRAME, draw_frame, 0)); CHECK(CNET_set_handler( EV_TIMER1, timeouts, 0)); CHECK(CNET_set_handler( EV_DEBUG1, showstate, 0)); CHECK(CNET_set_debug_string( EV_DEBUG1, "State")); if(nodeinfo.nodenumber == 1) CNET_enable_application(ALLNODES); } EVENT_HANDLER(show_state) { printf( "\tackexpected\t= %d\n\tnextdatatosend\t= %d\n\tdataexpected\t= %d\n", ackexpected, nextdatatosend, dataexpected); }

As this file only provides a Data-Link layer protocol, we first ensure that this is only a network of 2 nodes. Note that if any node calls the C function exit(), that the whole simulation will terminate. There is little else new here other than handlers for the EV_APPLICATIONREADY and EV_DRAWFRAME events.

cnet provides the ability for our interior protocols to control the rate of new messages coming ''down'' from the Application Layer. We do this by enabling and disabling the Application Layer or, more particularly, by enabling and disabling the generation of messages for certain remote hosts. We need to call CNET_enable_application() in at least one node for anything further to happen. This protocol is written so that only one node (number 0) will generate and transmit messages and the other (number 1) will receive them. This self-imposed restriction makes it easier to understand early protocols. The restriction can easily be removed by removing the line

    if(nodeinfo.nodenumber == 1)

in reboot_node(). Both nodes will then transmit and receive (why?). The meaning and handling of the EV_DRAWFRAME event is fully described elsewhere - see Drawing data frames in cnet.

The first thing of interest that will occur after each node has rebooted is that one node's Application Layer will generate and announce a new message for delivery. We handle the EV_APPLICATIONREADY event with our application_ready() function:

EVENT_HANDLER(application_ready) { CnetAddr destaddr; lastlength = sizeof(MSG); CHECK(CNET_read_application(&destaddr, (char *)&lastmsg, &lastlength)); CNET_disable_application(ALLNODES); printf("down from application, seq=%d\n",nextdatatosend); transmit_frame(&lastmsg, DL_DATA, lastlength, nextdatatosend); nextdatatosend = 1-nextdatatosend; }

We first determine the maximum sized message that our protocol can handle and pass that, by reference, to the CNET_read_application() function. Asssuming that we have provided enough buffer space, on return CNET_read_application() informs our interior protocol of the intended network destination of the new message's destination, copies the actual message into our variable lastmsg, and modifies lastlength to tell us how long the message is. We next call CNET_disable_application() to tell our node's Application Layer to stop generating messages (for any node). We finally pass the new message to our function, transmit_frame() (shown shortly), along with parameters in support of our stopandwait protocol.

Our transmit_frame() function performs the final actions before something is transmitted across the Physical Layer. Parameters provide the message to be transmitted, an indication as to whether it is data or an acknowledgment, its length, and its sequence number as part of the stopandwait protocol.

void transmit_frame(MSG *msg, FRAMEKIND kind, size_t msglen, int seqno) { FRAME f; f.kind = kind; f.seq = seqno; f.checksum = 0; f.len = msglen; switch(kind) { case DL_ACK : printf("ACK transmitted, seq=%d\n",seqno); break: case DL_DATA : { CnetTime timeout; memcpy(&f.msg, (char *)msg, (int)msglen); printf(" DL_DATA transmitted, seq=%d\n",seqno); timeout = FRAME_SIZE(f)*(CnetTime)8000000 / linkinfo[1].bandwidth) + linkinfo[1].propagationdelay; lasttimer = CNET_start_timer(EV_TIMER1, timeout, 0); break: } } msglen = FRAME_SIZE(f); f.checksum = CNET_ccitt((unsigned char *)&f, (int)msglen); CHECK(CNET_write_physical(1, (char *)&f, &msglen)); }

We initialize the header fields of a frame, of type FRAME, and, if data, embed the message into the frame, by copying the bytes of the message into the corresponding field of the frame. Again, if the message is data, we need to estimate the amount of time that it will take for the message to travel to its destination, be processed by the remote node, and for its acknowledgment frame to return. It is important that we keep the units of our calculation correct - a link's propagation delay is measured in microseconds, the frame's size in bytes, and a link's bandwidth in bits per second. We multiply the whole calculation by 3 for a reasonable estimate of the upper bound for the complete round-trip time. We call CNET_start_timer() to have the EV_TIMER1 event raised sometime after we expect the acknowledgment to return.

We finally calculate a checksum of the frame to be transmitted, embed the value of the checksum in the frame itself(!), and call CNET_write_physical() to transmit the frame on link 1.

Next, we provide the most complex handler of the EV_PHYSICALREADY event, which is invoked when a frame arrives, via a link, at the Physical layer. We first call CNET_read_physical() to read in the frame, first telling it how much buffer space we are providing to receive the frame. On return, the function tells us on which link the frame arrived, copies the frame to our provided buffer space, and tells us how long (in bytes) the frame is. We again use CHECK() to automatically detect any errors.

We next use one of the provided checksum functions to determine if the frame has been corrupted in its travel. Although not demanded by cnet, is is necessary to use the same checksum function on both sender and receiver. We extract the expected checksum, as calculated by the sender, from the frame itself(!) and compare it with the locally calculated value. It is unwise to attempt to make any sense of any of the contents of a corrupted frame. If we detect corruption, we simply ignore the newly arrived frame, confident that it will be retransmitted in the future.

If the frame is an expected acknowledgment, we know that its corresponding data frame has arrived safely, and so we stop the retransmission timer. Gloating with pride, we call CNET_enable_application() so that the Application Layer may generate the next message for delivery.

If the frame is expected data, we write a copy of the frame's embedded message (only) to our Application Layer with CNET_write_application(). We again use CHECK() to automatically detect if our protocol has let through any errors. Finally, if the frame was a data frame, the expected one or not, we reply with an acknowledgment frame using transmit_frame(), described earlier.

EVENT_HANDLER(physical_ready) { FRAME f; size_t len; int link, checksum; len = sizeof(FRAME); CHECK(CNET_read_physical(&link, (char *)&f, &len)); checksum = f.checksum; f.checksum = 0; if(CNET_ccitt((unsigned char *)&f, (int)len) != checksum) { printf("\t\t\t\tBAD checksum - frame ignored\n"); return; // bad checksum, ignore frame } switch(f.kind) { case DL_ACK : { if(f.seq == ackexpected) { printf("\t\t\t\tACK received, seq=%d\n", f.seq); CNET_stop_timer(lasttimer); ackexpected = 1-ackexpected; CNET_enable_application(ALLNODES); } break: } case DL_DATA : { printf("\t\t\t\tDATA received, seq=%d, ", f.seq); if(f.seq == dataexpected) { printf("up to application\n"); len = f.len; CHECK(CNET_write_application((char *)&f.msg, &len)); dataexpected = 1-dataexpected; } else printf("ignored\n"); transmit_frame((MSG *)NULL, DL_ACK, 0, f.seq); break: } } }

If the topology file sets either of the probframecorrupt or probframeloss link attributes, and the data frame is corrupt or lost, then the standard stopandwait protocol will not send an acknowledgment from the receiver back to the sender. Moreover, even if the data frame arrives safely, the acknowledgment frame itself may be corrupt or lost on its return. We thus require a ''timeout'' function to cause the retransmission of a frame if the sender has not seen a valid acknowledgment frame after some period of time. Our timeouts() function handles the EV_TIMER1 event for the timer that was initially started in transmit_frame(). Note that we do not need to explicitly stop a timer if its handler is invoked - it is stopped implicitly by cnet.

EVENT_HANDLER(timeouts) { if(timer == lasttimer) { printf("timeout, seq=%d\n", ackexpected); transmit_frame(&lastmsg, DL_DATA, lastlength, ackexpected); } }

Done. A real Data-Link Layer protocol providing reliable delivery in the presence of frame corruption and loss.

Lessons learnt

Although cnet tries not to dictate how you write your interior protocols, there are obviously a number of common ideas that appear in these example protocols. Some are necessary to enable correct interaction between the cnet event scheduler and your event handlers, but others may simply be considered as good style to both minimize errors and to make your protocols readable by others:

  1. Each protocol source file must include the <cnet.h> header file.
  2. All nodes actually execute the same (read-only) copy of the compiled protocol code, but each node has its own copy of any variables in the code - after all, each node is a simulating a distinct computer, and they don't share their RAM. Nodes may only communicate using the Physical Layer.
  3. Each node's protocol code must have one function to receive the initial EV_REBOOT event. By default, this is named reboot_node() but may be renamed with the -R command-line option.
  4. All event handlers are invoked with three parameters providing the event (the reason for being invoked), a timer (often NULLTIMER if there is no meaningful timer), and a user provided data value.
  5. The reboot_node() handler should indicate which future events are of interest with CNET_set_handler() and place labels on the debug buttons with CNET_set_debug_string().
  6. Event handlers should simply perform their responsibilities as quickly as possible, and then return to enable other handlers to be invoked. They should not loop indefinitely, nor attempt to sleep, wait or poll for other events.
  7. Calls to some functions, such as CNET_read_application() and CNET_read_physical(), must first indicate, using a parameter passed by reference, how much buffer space they are providing to receive some data. On return, these functions modify the same parameter to report how much buffer space was actually required and used.
  8. Variables and function parameters used to hold the lengths of messages and frames should be of type size_t.
  9. If a frame appears to have been corrupted in its travel (as determined by one of the provided checksum functions), it is unwise to attempt to make any sense of any of the contents of the frame.
  10. Most functions provided by cnet return 0 on success and 1 on failure (with the obvious exception of the CNET_start_timer() function). Most function calls may be ''wrapped'' with the CHECK() function to automatically detect and report most errors. It indicates a serious error in your protocols if one of the cnet functions reports an error and your protocol ignores it.
  11. We do not need to explicitly stop a timer if its handler is invoked - it is stopped by cnet. However, it is not really a fatal error to stop a timer that has already expired.

Good luck.

cnet v4.0.4 - [email protected]