Tutorial

A brief introduction and tutorial how to get you started with the neuropil messaging layer.

Sending messages

This example explains how to setup a simple neuropil node that will send periodic messages to a destination.

Note

The source code of this example is available in examples/neuropil_sender.c

Note

You can modify this example program and (re)build it with scons bin/neuropil_sender.

Note

You can run this example like so LD_LIBRARY_PATH=build/lib:$LD_LIBRARY_PATH bin/neuropil_sender. It will create and print events to a log file in the current directory.

First and foremost, we have to include header file which defines the API for the neuropil messaging layer.
#include "neuropil.h"
To initialize the neuropil messaging layer, we prepare a np_settings by populating it with the default settings using np_default_settings(), and create a new application context with np_new_context().
struct np_settings cfg;
np_default_settings(&cfg);

np_context *ac = np_new_context(&cfg);
Next, we allocate a network address and port tuple to listen on for incoming connections using np_listen().
assert(np_ok == np_listen(ac, "udp4", "localhost", 1234));
To join a neuropil network, we have to connect with our initial bootstrap node using np_join(). Other nodes in the network will be discovered automatically, but we need explicitly specify the network address and port tuple for our initial contact.
assert(np_ok == np_join(ac, "*:udp4:localhost:2345"));
We should also set an authorization callback via np_set_authorize_cb() to control access to this node. More on this later.
assert(np_ok == np_set_authorize_cb(ac, authorize));

Now to our application logic. We will repeatedly run the neuropil event loop for five seconds with np_run(), and then send our message with the subject "mysubject" using np_send(). If anything goes wrong we return the error code (an np_return.)

Effectively, this means that our node will process protocol requests continuously (for as long as there is no error situation) and send a message every five seconds periodically.

enum np_return status;
char *message = "Hello, World!";
size_t message_len = strlen(message);
do status = np_run(ac, 5.0)
| np_send(ac, "mysubject", message, message_len);
while (np_ok == status);

return status;
All that is left is to implement our authorization callback, a function of type np_aaa_callback. The one defined is eternally lenient, and authorizes every peer to receive our messages. To ensure that our message is not read by strangers, it should really return false for np_token of unknown identities.
bool authorize (np_context *ac, struct np_token *id)
{
// TODO: Make sure that id->public_key is the intended recipient!
return true;
}

Receiving messages

This example explains how to setup a simple neuropil node that will receive messages on a subject.

Note

The source code of this example is available in examples/neuropil_receiver.c

Note

You can modify this example program and (re)build it with scons bin/neuropil_receiver.

Note

You can run this example like so LD_LIBRARY_PATH=build/lib:$LD_LIBRARY_PATH bin/neuropil_receiver. It will create and print events to a log file in the current directory.

The simple receiver example looks very much like the sender we just discussed. Instead of sending messages it registers a receive callback for messages on the subject "mysubject" with np_add_receive_cb().
assert(np_ok == np_add_receive_cb(ac, "mysubject", receive));
In its in main loop it simply runs the neurpil event loop repeatedly, and handles any error situations by halting.
enum np_return status;
do status = np_run(ac, 5.0); while (np_ok == status);

return status;
The receive callback interprets the message payload as a string, and prints it to standard output.
bool receive (np_context* ac, struct np_message* message)
{
printf("Received: %.*s\n", (int)message->data_length, message->data);
return true;
}

Using identities

This example shows you how you can use digital identities to achieve loadbalancing between two nodes.

Note

The source code of this example is available in examples/neuropil_receiver_lb.c

Note

You can modify this example program and (re)build it with scons bin/neuropil_receiver_lb.

Note

You can run this example like so LD_LIBRARY_PATH=build/lib:$LD_LIBRARY_PATH bin/neuropil_receiver_lb. It will create and print events to a log file in the current directory.

first, let’s define a callback function that will be called each time a message is received by the node that you are currently starting
bool receive_this_is_a_test(np_context* context, struct np_message* msg)
{
for this message exchange the message is send as a text element (if you used np_send_text) otherwise inspect the properties and payload np_tree_t structures …
char text[msg->data_length+1];
memcpy(text, msg->data, msg->data_length);
return true to indicate successfull handling of the message. if you return false the message may get delivered a second time
return true;
}
second, let’s define a callback function that will be called each time a message intent is received by the node that you are currently running you can check and investigate the token and authorize the message exchange
bool authorize (np_context *ac, struct np_token *id)
{

as an example you may check the the fingerprint of the token issuer. This is fine as the token is always authentic (integrity check already done), and you have authenticated this issuer fingerprint in your authentication callback before.

You could choose arbitrary attributes values in the token attributes as well to authorize data exchange.

char sender[65]; sender[64] = '\0';
char* ctx = (char*) np_get_userdata(ac);
sodium_bin2hex(sender, 65, id->issuer, 32U);
// fprintf(stdout, "AUTHZ(%s): subj: %s ## pk: %s\n", ctx, id->subject, sender);
// fflush (stdout);
return true to indicate the successful handling of the message. if you return false the message may get delivered a second time
return true;
}
in your main program, initialize the two neuropil nodes, but this time use the a single identity on top of both
struct np_settings *settings_1 = np_default_settings(NULL);
struct np_settings *settings_2 = np_default_settings(NULL);
settings_1->n_threads = 4;
settings_2->n_threads = 4;

snprintf(settings_1->log_file, 255, "%s%s_%s.log", logpath, "/neuropil_receiver_lb_1", port);
snprintf(settings_2->log_file, 255, "%s%s_%s.log", logpath, "/neuropil_receiver_lb_2", port);
fprintf(stdout, "logpath: %s\n", settings_1->log_file);
fprintf(stdout, "logpath: %s\n", settings_2->log_file);

np_context* context_1 = np_new_context(settings_1);
np_set_userdata(context_1, "context 1");
np_context* context_2 = np_new_context(settings_2);
np_set_userdata(context_2, "context 2");
create a new identity and use it for both nodes
struct np_token my_id = np_new_identity(context_1, _np_time_now(NULL) + 3600.0, NULL);
strncpy(my_id.subject, "urn:np:id:this.is.a.test.identity", 255);

np_use_identity(context_1, my_id);
np_use_identity(context_2, my_id);
Make sure that you have implemented and registered the appropiate aaa callback functions to control with which nodes you exchange messages. By default everybody is allowed to interact with your node
as in the simple example: set the authorization callbacks and listen on a network interface
assert(np_ok == np_set_authorize_cb(context_1, authorize));
assert(np_ok == np_set_authorize_cb(context_2, authorize));
start up the job queue with and check the error code if the event loop can be processed.
if (np_ok != np_run(context_1, 0)) {
exit(EXIT_FAILURE);
}
if (np_ok != np_run(context_2, 0)) {
exit(EXIT_FAILURE);
}
join a network of nodes and wait until both nodes have joined the network
enum np_return status = np_ok;
if (NULL != j_key)
{
status |= np_join(context_1, j_key);
status |= np_join(context_2, j_key);
}
NP_CHECK_ERROR(status);

while ( np_has_joined(context_1) && np_has_joined(context_2) && status == np_ok) {
status |= np_run(context_1, 0.04);
status |= np_run(context_2, 0.04);
}
NP_CHECK_ERROR(status);
register the listener function to receive data from the sender for both nodes
status |= np_add_receive_cb(context_1, "urn:np:subj:this.is.a.test",  receive_this_is_a_test);
struct np_mx_properties mx_1 = np_get_mx_properties(context_1, "urn:np:subj:this.is.a.test");
mx_1.max_parallel = 7; // only receive seven messages in parallel
status |= np_set_mx_properties(context_1, "urn:np:subj:this.is.a.test", mx_1);
NP_CHECK_ERROR(status);

status |= np_add_receive_cb(context_2, "urn:np:subj:this.is.a.test", receive_this_is_a_test);
struct np_mx_properties mx_2 = np_get_mx_properties(context_2, "urn:np:subj:this.is.a.test");
mx_2.max_parallel = 7;
status |= np_set_mx_properties(context_2, "urn:np:subj:this.is.a.test", mx_2);
NP_CHECK_ERROR(status);
the loopback function will be triggered each time a message is received make sure that you’ve understood how to alter the message exchange to change receiving of message from the default values
loop (almost) forever, and watch the messages drop in on the different nodes. et voila, identity based loadbalancing …
while (1)
{
status |= np_run(context_1, 0.04);
NP_CHECK_ERROR(status);
status |= np_run(context_2, 0.04);
NP_CHECK_ERROR(status);
}

Bootstrapping a network

This example explains how to bootstrap a neuropil network.

Note

The source code of this example is available in examples/neuropil_controller.c

Note

You can modify this example program and (re)build it with scons bin/neuropil_controller.

Note

You can run this example like so LD_LIBRARY_PATH=build/lib:$LD_LIBRARY_PATH bin/neuropil_controller. It will create and print events to a log file in the current directory.

In order to bootstrap a neuropil network we need an initial peer that will invite our node into the mesh. We call Nodes whose only function is providing transit services to the network “infrastructure nodes.” A simple infrastructure node could look very much like the simple receiver example above, and could serve as the initial contact for other neuropil nodes (such as our sender and receiver examples.)

This node will not receive any messages, and will not set an authorization callback. More importantly, our bootstrap node will not attempt to join a network via np_join(). Since it will be the first node in the network there is no network it could join. Still, it will just listen on a known network address/port tuple.

assert(np_ok == np_listen(ac, "udp4", "localhost", 2345));

Other nodes can now join the network by calling :c:func:`np_join`
with the bootstrap node’s address. Using the absolute address as
returned by c:func:`np_get_address` will guarantee that nodes will
connect to the intended node only, and not say an impersonator.
char address[256];
assert(np_ok == np_get_address(ac, address, sizeof(address)));
printf("Bootstrap address: %s\n", address);

Alternatively, you can attempt to join any node that listens on a
specific network address/port tuple by joining a wildcard address,
which in this case would be ``"*:udp4:localhost:2345"``.

In the neuropil messaging layer, nodes need no authorization to join
a network, but they do need to authenticate themselves. This node
sets an authentication callback via :c:func:`np_set_authenticate_cb`
that will be called each time a node attempts to join this node.
The authentication callback gets passed the identity of the node that requesting to join, and can reject the request by returning false. For convenience, we merely log the first seven bytes of the public key of each node that joins the network via this node in its authentication callback.
bool authenticate (np_context *ac, struct np_token *id)
{
// TODO: Make sure that id->public_key is an authenticated peer!
printf("Joined: %02X%02X%02X%02X%02X%02X%02X...\n",
id->public_key[0], id->public_key[1], id->public_key[2],
id->public_key[3], id->public_key[4], id->public_key[5],
id->public_key[6]);
return true;
}