Wireless Embedded Internetworking Short Course
David E. Culler
Department of Electrical Engineering and Computer Science
|
Lab 5 Programming with Sockets |
|
Section
1: Embedded Network Programming
OS Networking APIs –
Implementing Clients and Servers
Before we dig into embedded network programming, let’s get reacquainted with how we program networked applications on conventional host operating systems. In a UNIX environment, the networking API is primarily BSD sockets. (Windows winsock is quite similar, but with substantial differences.) Sockets allow messages to be sent and received between two end-points (a.k.a. sockets). Sockets can be used for more than just inter-networking using TCP/UDP/IP; they are also used to communicate with various system-level components such as I/O and device drivers. We focus on the inter-networking functionality that sockets provide. Also, many are much more familiar with IPv4 programming than IPv6 programming, so we’ll show you the differences.
Exploring UDP Sockets
Networking applications tend to come in pairs, name the two sides of a protocol: client/server, talk/listen, and so on. We’ve put together several useful examples in unix/ipv4 and unix/ipv6. In your Linux VM, open a terminal window.
cd unix/ipv4
make
Open up a second terminal window and also cd unix/ipv4.
Now start the UDP server example in one window.
./udpserver 1234
Open up a different terminal and run the client example
./udpclient 127.0.0.1 1234
Type into the
udpclient. Observe what happens.
Let’s do the same thing using IPv6.
cd
unix/ipv6
make
./udpserver 1234
In the other window
cd
unix/ipv6
./udpclient ::1 1234
-
What does the
client do?
-
What does the
server do?
-
What is the
equivalent of '::1' in IPv4?
-
What does 1234 correspond
to?
UDP Client
Okay, now that you have a feel of what these programs do, let’s open them up and see how these are implemented. Using your favorite editor, open up unix/ipv6/udpclient.c.
The main() function starts by parsing command-line arguments and setting up a signal handler to clean up resources when interrupted. It’s generally a good idea to have these when building a robust application.
Now to the real stuff. The next action in main() is to setup the server variable, which is declared as a sockaddr_in6. You can find the definition of sockaddr_in6 in /usr/include/netinet/in.h. The actual code for initializing server is shown here:
server.sin6_family
= AF_INET6;
if
(inet_pton(AF_INET6, argv[1], &server.sin6_addr) <= 0)
error("Invalid IPv6 address\n");
server.sin6_port
= htons(port);
-
What does AF_INET6
specify?
-
What does the
port specify?
-
What does htons()
do?
-
What does inet_pton()
do?
Note: If you don’t know what something in UNIX does, chances are there is a man page about it. Man is the UNIX way of documenting things. For example, you can execute the following at the command-line:
man memcpy
After setting up the server variable, a new socket is creating using the socket command.
sockfd = socket(AF_INET6, SOCK_DGRAM, 0);
Look up the man-page for socket to find out more.
Now everything is setup properly to start using the socket. The client app now just sits in a while-loop sending messages to the server using sendto and receives messages using recvfrom. Lookup the man pages to find out more about them.
UDP Server
Let’s take a look at the server code. Using your favorite editor, open up udpserver.c. Notice that much of the code actually looks the same. The server still initializes the server variable, creates a socket, sends and receives messages. However there are subtle but important differences. Let’s take a look at each one.
Notice that the server variable is initialized slightly differently.
-
What is the IPv6
address set to? What does it mean? (hint: in.h
may be useful to you again)
-
Why is specifying
a port important?
There is also a new function call being used: bind
-
What does bind
do?
The remainder of the programming is pretty much the same as the client, except that recvfrom and sendto are reversed.
If you haven’t figured out already, the control flow is as follows:
Sockets API in Other
Settings
Since its original implementation, BSD Sockets have been ported to many languages other than C. Included in the unix directory is a python example of the UDP client: udpclient.py. Python is known for its ease of quickly programming new applications. Take a look at the python UDP client and compare it to the C version. You’ll quickly notice why Python has been gaining lots of popularity in recent years.
Additionally, the original BSD sockets API is tightly tied to the execution model defined by the OS, in this case UNIX. The BSD sockets API is written with sequential programming in mind and that is why all of the examples you’ve seen so far contain while-loops in the main body of the program. This will be different when we get to the embedded code.
Interacting with 6LoWPAN
So far, we’ve only been playing with clients and servers on the same Linux machine. This communication model is already fairly powerful as it lets you communicate with other process on the same box, while the kernel provides all of the nice buffer management and queuing needed to get messages from one process to another.
Exercise
Of course, we can use these same application protocols for inter-machine communication, not just inter-process communication.
But now let’s use the UDP client application to interact with 6LoWPAN nodes.
- If you don’t already have your 6LoWPAN network running, set it up as you did in Lab 2.
- You will need to install Udp Echo on them.
cd
tos/Echo
make
epic
swupdate
–t build/epic/tos_image.xml i <ipaddr>
swupdate
r <ipaddr>
Now try running the IPv6 udpclient on your Linux VM again but with <ip6addr> set to the IPv6 address of one of your sensor node.
./udpclient <ip6addr> 7
Is there any difference in communicating with a remote linux machine?
Try using the IPv4 udpclient on your Linux VM again but with <ip4addr> set to the IPv4 address of one of your sensor node.
IPv4 and IPv6 are treated as separate address space at the sockets
API, so unfortunately you have to pick one when you build your conventional
sockets application (or explicitly do both).
Since the LoWPAN router is providing the 6-to-4 translation, you can use
either one to communicate with motes.
However, you need to use the address that matches the code on the linux
side.
Programming TinyOS
Okay, enough with the Linux side for now. Let’s move on to the 6LoWPAN side. As a part of this exercise, we’ll get more experience with TinyOS programming and see how the event-driven execution model extends naturally to IP based communication.
TinyOS Modules and UDP Sockets
Let’s go take a look at the 6LoWPAN UDP server in tos/echo/.
Now open up EchoUdpP.nc. This file contains the module for the UDP server. It uses the Boot interface that we saw in the previous lab and a new Udp interface.
module EchoUdpP {
uses interface Boot;
uses interface Udp;
}
implements {
… implementation here …
}
Awfully simple isn’t it. At first glance, things look pretty different than they did on the Linux side. There’s no main, no need to create a socket, and now it looks like we’ve implemented a function called recvfrom() rather than actually using one. It’s a small snippet of code, but it exemplifies many of the TinyOS goals and design philosophy.
First, notice that all logic within this module is done only within event handlers. The booted event occurs when the underlying OS is ready to go. This is equivalent to main() in a traditional program (you could argue that main() is also an event handler). It’s the first event that gets signaled. In the booted event, there’s a call that should look familiar to you: bind. Here we issue the bind command to the Udp component. The argument is the port to bind to.
event void Boot.booted() {
call Udp.bind( ECHO_PORT );
}
The second event, recvfrom(), is signaled whenever a UDP message is received by the socket. Because this is the Echo server, all it needs to do is send the message right back using sendto(). Both of these calls should look familiar to you. Don’t worry about minor differences in the arguments for now.
event void Udp.recvfrom( void *buf, uint16_t
len,
sockaddr_in6_t
*from,
link_metadata_t
*linkmsg ) {
call Udp.sendto( buf, len, from );
}
TinyOS Interfaces
You’ll also find that most of this code looks like basic C-code with a few added key words. The nesC language is just that. The event keyword signifies call flow coming up through an interface. The call keyword signifies call flow going down into the interface. The keywords indicate that the functions belong to an interface being used or provided by the component. Interface functions are specified in nesC interface files. The UDP interface is specified in:
kernel/interfaces/Udp.nc
The keywords also act as hints to the nesC compiler so that it can verify that all interface functions are implemented. Any module using the UDP interface must implement the recvfrom() handler and is free to call any of the commands, such as bind().
To see what interfaces the kernel provides, open up src/BinKernel.nc. Notice that it has both Boot and UDP. It has a lot of other interfaces too. Another interesting note is the Timer interface, which is indexed by the parameter id. This is called a parameterized interface and simply represents multiple instances of the Timer interface. We have already seen timers, but we’ll see more of them.
As a side note, the nesC compiler takes nesC code and transforms it to C-code, which is then passed to a C-compiler, such as GCC. To view the generated C-code, first compile it:
make epic
Now look in build/epic/app.c. (Remember, this is compiler generated code. It is not for human consumption.)
TinyOS Configurations
Now take a look at EchoUdpC.nc. This file contains a configuration for the UDP server. This is where the power of modularity takes place in TinyOS. Configuration files simply ‘wire’ modules together. In other words, when a module uses an interface, the configuration file specifies which specific implementation to use. In this case, both Boot and UDP are implemented by the kernel.
-
Why don’t we need
to create a socket as we did with the socket call in Linux?
Wiring is a perfect example of TinyOS’ design goal philosophy of static binding. In a traditional OS, calling the sockets command allocates some state (specifically, a file descriptor). When you have very limited memory and resources, dynamic allocation can make it hard to be predictable. Another example of dynamic allocation is malloc. You will rarely see malloc in TinyOS. Instead, all variables, buffers, etc. must be declared explicitly within the module and allocated at compile time.
-
Why does static
binding and allocation make the system more predictable?
-
What capabilities
do you lose if you do not allow dynamic allocation/binding?
Notice, on the Linux side all system calls are blocking. Once the call is made, the thread blocks until the call completes. Recvfrom just hangs. If you wanted to anything else, like sample or handle timer events you would need to have previously forked a separate thread to do so. In the event driven model it is natural to continue to service all sorts of events while network protocols are on-going. It can handler timer events, input events, do processing, and so on. This suggests our open ended exercise.
Open ended Exercise – UDP based embedded server.
Now that you have seen an echo server and you have developed embedded applications that have timers and capture various binary inputs, put these two together into an application of your own design. You can use updclient to send commands to the mote. Instead of echoing the incoming packet, the server on the mote responds with the requested information. For example, this might be the number of binary events since the last request or over some period of time. You might want to include a sequence number in the response so the server can potentially tell when requests or responses get dropped. You could write a more sophisticated server that makes regular requests, keeps sequence numbers, times out on lost responses and reissues them. Whatever you would like. We’ll discuss what comes out.
In this lab we have built an UDP/IP based embedded network application in which the infrastructure essentially polled the embedded device by sending requests to it. This is a natural use of a remote procedure call (RPC) paradigm. Of course, in conventional networks all sorts of RPC style distributed system architectures have been developed, including Sun RPC, Microsofts DCOM, Corba, XML-RPC. And in the world of industrial instrumentation, there are many command/response protocols used. In lab 5 you probably sent text strings back and forth. You could instead have sent binary packets according to some packet format. This is in fact what most instrumentation standards do. The entire area of low-power wireless embedded RPC distributed systems design is an almost completely unexplored research area. In the past, the only mechanism available was active messages within the embedded network and a dedicated gateway device at the egress point. The presence of IP based communication allows the ends of the RPC to be on completely different networks.