Learn to write a simple server application with unicoap
Learn to write a simple server application with unicoap
Sample code. Find the sample code in examples/networking/coap/unicoap_server.
In this tutorial, you will learn how you can create a simple CoAP server with unicoap. We will support CoAP over UDP and DTLS and try out the application using RIOT's native board. Hence, you will need a Linux host and support for tuntap (tap interfaces). Our goal is to create a server that greets its users.
To start, we will create a Makefile and add unicoap as a dependency. Because we want to support UDP and DTLS, we need to add the respective drivers in the Makefile.
Because RIOT allows you to switch the network backend, we need to specify one. In this tutorial, we choose Generic (GNRC) network stack.
In main.c, we include unicoap.
Let's define a CoAP resource. To define a new resource statically (at compile-time), we use the UNICOAP_RESOURCE macro. To make that work, we need to import the CoAP Resource Declarations module in our Makefile.
Note that you will need to supply an identifier. Let's call it hello. You can give it any name you want, as long as it is a unique C variable name. Each resource must be assigned a path. This will be the part that follows the host/domain and port in the CoAP URI. To assign a path, set the unicoap_resource_t::path property to UNICOAP_PATH_ROOT or a custom path using UNICOAP_PATH.
For example, consider a resource /gelato/flavours/menu. This path would be written as UNICOAP_PATH("gelato", "flavours", "menu") using UNICOAP_PATH. Never include a slash within one path component, i.e., never write UNICOAP_PATH("gelato/flavours", "menu").
Next, we must specify what CoAP methods (akin to HTTP methods) we want to allow. Therefore, we list all methods permitted as parameters to the UNICOAP_METHODS macro. You cannot leave unicoap_resource_t::methods empty. If you later send a request that does not match the set of methods, unicoap will reject that request with a Method Not Allowed CoAP response.
Optionally, you can also restrict the resource to a set of transports. This may be useful if you consider encryption to be mandatory. In our case, the greeting will also be reachable over unencrypted UDP.
Next, we want to make sure our greeting arrives at the client. Since we are using CoAP over UDP (or CoAP over DTLS, which relies on UDP for that matter), we instruct unicoap to send confirmable responses. unicoap will retransmit our response until the client has acknowledged it. To do this, we pass the UNICOAP_RESOURCE_FLAG_RELIABLE flag.
Finally, we specify a function that is going to handle requests to /. We're going to implement handle_hello_request next.
handle_hello_request will take four arguments: the request, auxiliary information like the client address, a context and an optional argument.
Inside handle_hello_request we're going to log the request initially. We use unicoap_string_from_method to get a string representation of the CoAP method, i.e., the CoAP message code, and unicoap_message_t::unicoap_message_payload_get_size for the number of payload bytes. This will log a messaging like GET /, 0 bytes.
This version of the resource is going to be very simple. The root resource will simply respond with a Hello, World! string. For static null-terminated strings, unicoap provides convenience initializers. Note that we can reuse the memory of the message parameter. Finally, we respond. You do not necessarily need to return the result of unicoap_send_response. However, the return value is always supposed to indicate a success (zero) or an error (negative integer). Invoking send_response before and returning a status code, as well as not calling send_response before and not returning a status code are treated as fatal errors.
The resource we implemented in the previous section was quite simple. Surely, visitors would enjoy a personalized greeting, wouldn't they? Let's develop a /greeting resource that accepts a name query parameter with their name. So if someone asks for coap://...host.../greeting?name=RIOTeer, we would respond with Hello, RIOTeer! Welcome to our itsy bitsy tiny CoAP server!.
The first step is to define a new resource.
In handle_greeting_request, we need to retrieve the name query parameter and return a Bad Request response if our visitor did not leave their name in the request. To do this, we use unicoap_options_t::unicoap_options_get_first_uri_query_by_name_string. As a simple shorthand you can return a CoAP status from the request handler as follows. This will send a response without any payload attached.
We don't really trust our visitor yet, so let's validate their name. Only this time, we tell the client the query value was invalid by leveraging the unicoap_send_response function and a dedicated error message.
Now we can craft our response. Proper responses have their Content-Format option set accordingly, in our case text/plain. Setting options can fail, e.g., due to insufficient buffer capacity, hence we do that first. We allocate options on the stack through UNICOAP_OPTIONS_ALLOC and set the format with unicoap_options_t::unicoap_options_set_content_format.
Content-Format option, we set the capacity to exactly 2 bytes.).The next step is to create the greeting string. In theory, you could write the three parts Hello,, then the name, and finally the suffix ! Welcome ...... into a buffer and send that. But that would be too easy. In some cases, when you would need to create a large buffer, the following technique might be preferable. Instead of a buffer, we create a vector, that is, a list of payload chunks. The trick is that the network backend will copy these chunks into a transmission buffer on its own, which is why we can avoid additional copy operations this way.
These are going to be the first and last chunks:
static_strlen is defined in the sample code.Next, we add the dynamic middle part.
Through .iol_next = &suffix, we chain the suffix behind the middle part and using list.iol_next = &name_chunk appends this chain of two chunks to the first chunk. Our vector is done and we can finally send the response after having set the status and message payload. unicoap supports multiple slightly different ways to use the API to respond to reduce boilerplate. Find an extended explanation in CoAP Server.
To enable Datagram Transport Layer Security, we already imported the CoAP over DTLS Driver. However, we need to include additional headers to add a DTLS credential to unicoap.
In the main function, we then need to add this credential to the DTLS socket which we can retrieve using unicoap_transport_dtls_get_socket.
Now, your server is also reachable over an encrypted DTLS connection. Time to test it!
The sample code includes a client.py script that uses aiocoap. To use it we need to compile the server, run it and then send a CoAP request to it. We're going to use RIOT's native board for this, i.e., both the client and server will run on your linux host and there will be no wireless network involved — no antenna needed. To compile and run the app, run this shell command:
This should show something similar to this:
unicoap will emit debug logs depending on CONFIG_UNICOAP_DEBUG_LOGGING and CONFIG_UNICOAP_ASSIST. These settings are enabled in the sample code in app.config.In a second terminal session, run
You should see a number of debug logs from that script culminating in:
Congrats, your server is working!
If you want to capture the actual CoAP messages being sent, open a third terminal session and run tcpdump -i tap0 -w coap-greeting.pcap, then execute the client script as described above, then terminate tcpdump with CTRL+C. You can now open coap-greeting.pcap with Wireshark to inspect CoAP messages. You should see a NON request followed by a CON response which elicits another ACK message from the client.