cnet v4.0.4  

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

A protocol walkthrough

Firstly, as seems mandatory when introducing new programming languages, let's look at the traditional ''Hello World'' example written as a cnet protocol. Although cnet protocols are written in ISO-C99, this example looks quite different:

#include <cnet.h> void reboot_node(CnetEvent ev, CnetTimerID timer, CnetData data) { printf("hello world\n"); }

Things to first note are that the source code includes the standard cnet header file, as must all cnet protocol source files, and that there is no traditional main() function, nor a call to exit(), nor a return value. However, this ''protocol'' does run - each node implementing this protocol is rebooted when cnet invokes its event-handler for the EV_REBOOT event. The handler is passed the parameters of ev=EV_REBOOT, timer=NULLTIMER, and data=0, none of which are used by this simplest handler. The handler prints out its message "hello world\n" using the C function printf(), the message appears on the standard output window of each node, and the handler finally returns to whoever called it (in this case cnet's internal scheduler).

In fact, the above protocol is so simple that it doesn't even need the usual cnet topology file. We can commence its execution just using cnet's command-line options:

    shell> cnet -C helloworld.c -r 2

We can even replace the 2, above, with 101 to run the protocol on a much larger, random, network. Once every node in the network has rebooted and printed "hello world\n" the whole simulation keeps running, but doing nothing else because there is nothing left for it to do (strictly speaking, cnet could detect that there are no more significant events to schedule, and just terminate). All noteworthy but pointless.

To make out protocols easier to read, these examples will often use the EVENT_HANDLER C macro, defined in the <cnet.h> header file. Its full definition is:

#define EVENT_HANDLER(name) \ void name(CnetEvent ev, CnetTimerID timer, CnetData data)

This provides greater readability in protocol code, which can simply declare its event handling functions with, for example:

EVENT_HANDLER(timeouts) { .... }

Use of EVENT_HANDLER does, however, introduce some potential confusion. As an consequence of being declared with EVENT_HANDLER, each such function "automatically" has three formal parameters: ev, timer and data. While this improves both readability and consistency, it may introduce confusion as to where the parameters actually "come from".

Introducing timer events

Next, we'll introduce the concept of cnet timers with the protocol source file ticktock.c and its associated topology file TICKTOCK. We'll walkthrough this protocol in a different order to the way it appears in its source file. The C99 requires all identifiers to have been defined, or at least declared, before they are used, but we'll describe the functions in the order they will be invoked. However, we can't avoid first including the standard cnet header file. Firstly, we'll examine the reboot_node() event-handler, always the first function to be called by the cnet scheduler:

#include <cnet.h> EVENT_HANDLER(reboot_node) { // Indicate that we are interested in the EV_TIMER1 event CHECK(CNET_set_handler(EV_TIMER1, timeouts, 0)); // Request that EV_TIMER1 occur in 1sec, ignoring the return value CNET_start_timer(EV_TIMER1, (CnetTime)1000000, 0); }

The first cnet function called is CNET_set_handler(). This instructs cnet to remember that for this node that any time the EV_TIMER1 event occurs, that cnet should call the function timeouts() to handle the event. The third parameter to CNET_set_handler() is actually ignored for this call, but must always be provided. The call to CNET_set_handler() is also ''wrapped'' in the function CHECK(), described in the Frequently Asked Questions, to ''automatically'' detect any error occuring from the call to CNET_set_handler().

Finally, in reboot_node(), the function CNET_start_timer() is called to request that the EV_TIMER1 event be raised (once) in 1 second's time. All times in cnet are measured in microseconds, and so we use 64-bit integers to hold these values (which can grow quite quickly). We use cnet's CnetTime 64-bit integer datatype to simplify the use of these values.

When the EV_TIMER1 event occurs, the third parameter to CNET_start_timer(), here 0, will be faithfully passed to the event handler for EV_TIMER1, which we know to be the timeouts() function. Next, let's look at the timeouts() function, which we know will be called 1 second after the reboot_node() handler is finished:

EVENT_HANDLER(timeouts) { static int which = 0; printf("%3d.\t%s\n", which, (which%2) == 0 ? "tick" : "\ttock"); ++which; // Reschedule EV_TIMER1 to occur again in 1 second CNET_start_timer(EV_TIMER1, (CnetTime)1000000, 0); }

The timeouts() function will be called with three parameters: ev=EV_TIMER1, timer=somevalue, and data=0. The value of the second parameter, timer will actually be the return value of the call to CNET_start_timer() in reboot_node(). The return value is of type CnetTimerID, but we are not concerned with its value in this simple protocol. The data=0 value is the third parameter given to the CNET_start_timer() call in reboot_node().

The first time that timeouts() is called, it will simply print the string 0. tick. It then calls CNET_start_timer() to again request that the handler for EV_TIMER1 be called in a further 1 second. Although timeouts() is, itself, the handler for EV_TIMER1, this is not a recursive call as no handler will be called until this current handler has returned. The timeouts() function will be called, again, in another 1 seconds and will, this time, simply print 1. tock. That's all it does - printing 0. tick, 1. tock, 2. tick, 3. tock ... every second. It's still not really a protocol.

Introducing the Physical Layer and debug events

We'll next consider an example which demonstrates how to transmit data across cnet's Physical Layer and to handle the button-based ''debug'' events. The example is imaginatively named click.c, supported by CLICK. Firstly some declarations and the global data (character strings) that we'll randomly transmit:

#include <cnet.h> #include <ctype.h> #include <string.h> int count = 0; char *fruit[] = { "apple", "pineapple", "banana", "tomato", "plum" }; #define NFRUITS (sizeof(fruit) / sizeof(fruit[0]))

The first function called is, of course, reboot_node() to handle the EV_REBOOT event. In this handler we simply register this node's interest in a number of events, passing to CNET_set_handler() the addresses of the functions to be called when each event is raised. We also call CNET_set_debug_string() to place an indicated string/label on two of the five debug buttons. Out of habit, we wrap each call with CHECK to ensure that there have been no errors.

EVENT_HANDLER(reboot_node) { // Indicate our interest in EV_DEBUG1, EV_DEBUG2 and EV_PHYSICALREADY CHECK(CNET_set_handler(EV_PHYSICALREADY, frame_arrived, 0)); CHECK(CNET_set_handler(EV_DEBUG1, send_reliable, 0)); CHECK(CNET_set_debug_string( EV_DEBUG1, "Reliable")); CHECK(CNET_set_handler(EV_DEBUG2, send_unreliable, 0)); CHECK(CNET_set_debug_string( EV_DEBUG2, "Unreliable")); }

We next look at two very similar functions. Each chooses a random character string (a fruit), uses it to format a frame for transmission, and determines the length of the frame (adding 1 for its NULL-byte terminator). The length of the frame and a checksum of the frame (using the CNET_IP_checksum() function provided by cnet) are next reported.

The frame is finally transmitted across the Physical Layer. In handler send_reliable() we call CNET_write_physical_reliable() to bypass cnet's Error Layer. Handler send_unreliable() calls CNET_write_physical(), and because CLICK introduces frame corruption with a probability of one in four, some of the frames will be corrupt during transmission.

The three parameters to the CNET_write_physical* functions provide the required link number (1), the address of the frame to be transmitted, and the length (in bytes) of the frame. The length is passed by reference because, on return, the functions indicate how many bytes they accepted by possibly modifying this reference parameter. We will assume that all went well, and that all bytes were actually transmitted. There is no real need to have two such similar functions - we could, instead, have a single function which handles both of the EV_DEBUG1 and EV_DEBUG2 events and calls the appropriate transmission function after inspecting the value of ev.

EVENT_HANDLER(send_reliable) { char frame[256]; size_t length; sprintf(frame, "message %d is %s", ++count, fruit[rand()%NFRUITS]); length = strlen(frame) + 1; printf("sending %u bytes, checksum=%6d\n\n", length, CNET_IP_checksum((unsigned short *)frame,(int)length)); CHECK(CNET_write_physical_reliable(1, frame, &length)); } EVENT_HANDLER(send_unreliable) { char frame[256]; size_t length; sprintf(frame, "message %d is %s", ++count, fruit[rand()%NFRUITS]); length = strlen(frame) + 1; printf("sending %u bytes, checksum=%6d\n\n", length, CNET_IP_checksum((unsigned short *)frame,(int)length)); CHECK(CNET_write_physical(1, frame, &length)); }

Finally we handle the arrival of a frame with the handler for the EV_PHYSICALREADY event, frame_arrived(). We first determine the maximum length of the frame that we are able to receive. If this length is sufficient to receive the incoming frame, the next call to CNET_read_physical() will inform us of the link number on which the frame arrived (here, it will be 1), copy the frame into the address/space that we have provided, and inform us of the frame's actual length (i.e. length will be modified).

EVENT_HANDLER(frame_arrived) { char frame[256]; size_t length; int link, i, ch; length = sizeof(frame); CHECK(CNET_read_physical(&link, frame, &length)); printf(" %u bytes arrived on link %d, checksum=%6d : \"", length, link, CNET_IP_checksum((unsigned short *)frame,(int)length)); for(i=0 ; i<length-1 ; ++i) { ch = frame[i]; if(!(isalnum(ch) || ch == ' ')) ch = '?'; putchar(ch); } printf("\"\n\n"); }

We next report the length and link number of the new frame, and again print its checksum (using the same checksum function as used by the sender). If the frame was corrupted in transmit, we will observe that the ''before and after'' checksum values will be different. Identical checksum values indicate that the frame arrived intact. We finally print out the actual bytes of the message - if the frame arrives intact, we will see the expected message carrying tha name of one of the fruits. If the frame was corrupted, we will see a '?' character for any unexpected byte.

OK, now that we've understood all of the above, let's walk through a complete stopandwait Data-Link Layer protocol.


cnet v4.0.4 - [email protected]