Welcome to the NEAT tutorial

What is NEAT?

NEAT is a library for networked applications, intended to replace existing socket APIs with a simpler, more flexible API. Additionally, NEAT enables endpoints to make better decisions as to how to utilize the available network resources and adapts based on the current condition of the network.

With NEAT, applications are able to specify the service they want from the transport layer. NEAT will determine which of the available protocols fit the requirements of the application and tune all the relevant parameters to ensure that the application gets the desired service from the transport layer, based on knowledge about the current state of the network when this information is available.

NEAT enables applications to be written in a protocol-agnostic way, thus allowing applications to be future-proof, leveraging new protocols as they become available, with minimal to no change. Further, NEAT will try to connect with different protocols if possible, making it able to gracefully fall back to another protocol if it turns out that the most optimal protocol is unavailable, for example, because of a middlebox such as a firewall. A connection in the NEAT API will only fail if all protocols satisfying the requirements of the application are unable to connect, or if no available protocol can satisfy the requirements of the application.

Most operating systems support the same protocols. However, the same protocol may often have a slightly different API on different operating systems. NEAT provides the same API on all supported operating systems, which is currently Linux, FreeBSD, OpenBSD, NetBSD, and OS X. The availability of a protocol depends on whether the protocol is supported by the OS or if NEAT is compiled with support for a user-space stack that implements the protocol.

Contexts and flows

The most important concept in the NEAT API is that of the flow. A flow is similar to a socket in the traditional Berkely Socket API. It is a bidirectional link used to communicate between two applications, on which data may be written to or read from. Further, just like a socket, a flow uses some transport layer protocol to communicate.

However, one important difference is that a flow is not as strictly tied to the underlying transport protocol in the same way a socket is. In fact, a flow may be created without even specifying which transport protocol to use. This is not possible with a socket.

The same applies to modifying options on sockets. Setting the same kind of option on two sockets with different protocols in the traditional socket API requires setsockopt calls with different protocol IDs, option names, and sometimes even values with different units. The setsockopt calls also vary depending on what system you are on. This is not the case with NEAT. As long as the desired option is available for the protocol in use, the API for setting that option is the same for all protocols, and on all operating systems supported by NEAT.

A context is a common environment for multiple flows. Along with flows, it contains several services that are used by the flows internally in NEAT, such as a DNS resolver and a Happy Eyeballs implementation. Flows within a context are polled together. A flow may only belong to the context in which it is created, and it cannot be transferred to a different context. Most applications need only one context.

Properties

Different types of applications have different requirements and desires to the services provided by the transport layer. An application for real-time communication may require the communication to have properties such as low latency, high bandwidth, quality of service, and have less strict requirements with regards to reliable delivery. Losing a packet or bit errors may be less critical to these applications. A web browser, on the other hand, might require communication that is (partially) ordered and error-free. A BitTorrent application might only require the ability to send packets to some destination with a minimum amount of effort, and not at the expense of other applications with stricter bandwidth requirements.

With the traditional socket API, the application requirements dictate the choice of which protocol to use. With NEAT, this is not the case. NEAT enables applications to specify the properties of the communication instead of specifying which protocol to use. Some properties may be required; other properties may be desired, but not mandatory. Based on the properties, NEAT will determine which protocols can support the requirements of the application and the options to set for each protocol. It will try to establish a connection by trying each of them until one connection succeeds, known as Happy Eyeballing.

The ability to specify properties instead of protocols allows applications to take advantage of available protocols where possible. By Happy Eyeballing, NEAT ensures that applications are able to cope with different network configurations, and gracefully fall back to another protocol if necessary should the most desirable protocol not be available for whatever reason.

Asynchronous API

The NEAT API is asynchronous and non-blocking. Once the execution is transferred to NEAT, it will poll the sockets internally, and, when an event happens, execute the appropriate callback in the application. This creates a more natural way of programming communicating applications than with the traditional socket API.

The three most important callbacks in the NEAT API are on_connected, on_readable and on_writable, which may be set per flow. The on_connected callback will be executed once the flow has connected to a remote endpoint, or a flow has connected to a server listening for incoming connections. The on_writable and on_readable callbacks are executed once data may be written to or read from the flow without blocking.

A minimal server

To get started using the NEAT API, we will write a small server that will send Hello, this is NEAT! to any client that connects to it. Later, we will write a similar client, before modifying this server so that it works with the client.

We can summarize the functionality as follows:

  • When a client connects, start writing when the flow is writable
  • When a flow is writable, write Hello, this is NEAT! to it.
  • When the flow has finished writing, close it.

Pay close attention to how easily this natural description can be implemented using the NEAT API.

Here are the includes that should be put on top of the file:

#include <neat.h>

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

We will start writing the main function of our server. The first thing we need to do is to declare a few variables:

    struct neat_ctx *ctx;
    struct neat_flow *flow;
    struct neat_flow_operations ops;

And initialize them:

    ctx  = neat_init_ctx();
    if (!ctx) {
        fprintf(stderr, "neat_init_ctx failed\n");
        return EXIT_FAILURE;

We are already familiar with the flow and the context. neat_init_ctx is used to initialize the context, and neat_new_flow creates a new flow withing the context. The neat_flow_operations struct is used to tell NEAT what to do when certain events occur. We will use that next to tell which function we want NEAT to call when a client connects:

    }

    flow = neat_new_flow(ctx);
    if (!flow) {

The function on_connected has not been defined yet, we will do that later. Now that we have told NEAT what to do with a connecting client, we are ready to accept incoming connections.

        fprintf(stderr, "neat_new_flow failed\n");
        return EXIT_FAILURE;
    }

    memset(&ops, 0, sizeof(ops));

This will instruct NEAT to start listening to incoming connections on port 5000. The flow passed to neat_accept is cloned for each accepted connection. The last two parameters are used for optional arguments. This example does not use them.

The last function call we will do in main will be the one that starts the show:


    ops.on_readable  = on_readable;

When this function is called, NEAT will start doing work behind the scenes. When called with the NEAT_RUN_DEFAULT parameter, this function will not return until all flows have closed and all events have been handled. It is also possible to run NEAT without having NEAT capture the main loop. Our final main function looks like this:

int
main(int argc, char *argv[])
{
    struct neat_ctx *ctx;
    struct neat_flow *flow;
    struct neat_flow_operations ops;

    ctx  = neat_init_ctx();
    if (!ctx) {
        fprintf(stderr, "neat_init_ctx failed\n");
        return EXIT_FAILURE;
    }

    flow = neat_new_flow(ctx);
    if (!flow) {
        fprintf(stderr, "neat_new_flow failed\n");
        return EXIT_FAILURE;
    }

    memset(&ops, 0, sizeof(ops));

    ops.on_readable  = on_readable;
    ops.on_connected = on_connected;
    neat_set_operations(ctx, flow, &ops);

We have now filled in the main function of our server application. It is time to start working on the callbacks that NEAT will use. The first callback we need is on_connected.

static neat_error_code
on_connected(struct neat_flow_operations *opCB)

From the functional description above, we know that we need to write to connecting clients when this becomes possible. The callback contains a parameter that is a pointer to a neat_flow_operations struct, which we can use to update the active callbacks of the flow. We set the on_writable callback so that we can start writing when the flow becomes writable:

    opCB->on_writable    = on_writable;

It is also good practice to set the on_all_written callback when setting the on_writable callback:

    opCB->on_all_written = on_all_written;

The change is applied by calling neat_set_operations, just as in the main function:

    neat_set_operations(opCB->ctx, opCB->flow, opCB);

Next, we write the on_writable callback:

on_writable(struct neat_flow_operations *opCB)
{

Here, we call the function that will send our message:

    neat_write(opCB->ctx, opCB->flow, message, 20, NULL, 0);
    opCB->on_writable = NULL;
    return NEAT_OK;

Here we specify the data to send and the length of the data. As with the neat_accept function, neat_write takes optional parameters. We do not need to set any optional parameters for this call either, so again we pass NULL and 0.

The final callback we need to implement is the on_all_written callback:

static neat_error_code
on_all_written(struct neat_flow_operations *opCB)

Here, we call neat_close to close the flow:

    neat_close(opCB->ctx, opCB->flow);

This is the final piece of our server. You may now compile and run the server. You can use the tool socat to test it. The following output should be observed:

$ socat STDIO TCP:localhost:5000
Hello, this is NEAT!
$ socat STDIO SCTP:localhost:5000
Hello, this is NEAT!

You may find the complete source for the server here.

A minimal client

Next, we want to implement a client that will send the message "Hi!" after connecting to a server, and then receive a reply from the server. A fair amount of the code will be similar to the server we wrote above, so you may make a copy of the code for the server and use that as a starting point for the client.

We will make two additions and one change to the main function. First, since we are connecting to a server, we change the neat_accept call to neat_open instead:

    }

    flow = neat_new_flow(ctx);
    if (!flow) {

Next, we will specify a few properties for the flow:

static char *properties = "{\n\
    \"transport\": [\n\
        {\n\
            \"value\": \"SCTP\",\n\
            \"precedence\": 1\n\
        },\n\
        {\n\
            \"value\": \"TCP\",\n\
            \"precedence\": 1\n\
        }\n\
    ]\n\
}";

These properties will tell NEAT that it can select either SCTP or TCP as the transport protocol. The properties are applied with neat_set_properties, which may be done at any point between neat_new_flow and neat_open.

Finally, we add neat_free_ctx after neat_start_event_loop, so that NEAT may free any allocated resources and exit gracefully. The complete main function of the client will look like this:

int
main(int argc, char *argv[])
{
    struct neat_ctx *ctx;
    struct neat_flow *flow;
    struct neat_flow_operations ops;

    ctx  = neat_init_ctx();
    if (!ctx) {
        fprintf(stderr, "neat_init_ctx failed\n");
        return EXIT_FAILURE;
    }

    flow = neat_new_flow(ctx);
    if (!flow) {
        fprintf(stderr, "neat_new_flow failed\n");
        return EXIT_FAILURE;
    }

    memset(&ops, 0, sizeof(ops));

    ops.on_connected = on_connected;
    ops.on_close = on_close;
    neat_set_operations(ctx, flow, &ops);

    if (neat_set_property(ctx, flow, properties) != NEAT_OK) {
        fprintf(stderr, "neat_set_property failed\n");
        return EXIT_FAILURE;

Leave the on_connected callback similar to the server.

We change the on_writable callback to send "Hi!" instead:

static neat_error_code
on_writable(struct neat_flow_operations *ops)
{
    const unsigned char message[] = "Hi!";
    neat_write(ops->ctx, ops->flow, message, 3, NULL, 0);
    return NEAT_OK;
}

The on_all_written callback should not close the flow, but instead stop writing and set the on_readable callback:

static neat_error_code
on_all_written(struct neat_flow_operations *ops)
{
    ops->on_readable = on_readable;
    ops->on_writable = NULL;
    neat_set_operations(ops->ctx, ops->flow, ops);
    return NEAT_OK;
}

Finally, we will write an on_readable callback for the client. We allocate some space on the stack to store the received data, and use a variable to store the length of the received message. If the neat_read call returns successfully, we print the message. Finally, we stop the internal event loop in NEAT, which will eventually cause the call to neat_start_event_loop in the main function to return. The on_readable callback should look like this:

static neat_error_code
on_readable(struct neat_flow_operations *ops)
{
    uint32_t bytes_read = 0;
    unsigned char buffer[32];

    if (neat_read(ops->ctx, ops->flow, buffer, 31, &bytes_read, NULL, 0) == NEAT_OK) {
        buffer[bytes_read] = 0;
        fprintf(stdout, "Read %u bytes:\n%s", bytes_read, buffer);
    }

    neat_close(ops->ctx, ops->flow);
    return NEAT_OK;
}

And there we have our finished client! You can test it with socat:

$ socat TCP-LISTEN:5000 STDIO

When you run the client, you should see Hi! show up in the output from socat. You can type a short message followed by pressing return, and it should show up in the output on the client.

You may find the complete source for the client here.

Tying the client and server together

A few small changes are required on the server to make the client and server work together. In the on_connected callback, the server should set the on_readable callback instead of the on_writable callback. An on_readable callback should be added and read the incoming message from the client, and set the on_writable callback.

The callbacks for the updated server is as follows:

static neat_error_code
on_readable(struct neat_flow_operations *ops)
{
    uint32_t bytes_read = 0;
    unsigned char buffer[32];

    if (neat_read(ops->ctx, ops->flow, buffer, 31, &bytes_read, NULL, 0) == NEAT_OK) {
        buffer[bytes_read] = 0;
        fprintf(stdout, "Read %u bytes:\n%s\n", bytes_read, buffer);
    }

    ops->on_readable = NULL;
    ops->on_writable = on_writable;
    ops->on_all_written = on_all_written;
    neat_set_operations(ops->ctx, ops->flow, ops);

    return NEAT_OK;
}

static neat_error_code
on_writable(struct neat_flow_operations *ops)
{
    const unsigned char message[] = "Hello, this is NEAT!";
    neat_write(ops->ctx, ops->flow, message, 20, NULL, 0);

    ops->on_writable = NULL;
    ops->on_readable = NULL;
    ops->on_all_written = on_all_written;
    neat_set_operations(ops->ctx, ops->flow, ops);

    return NEAT_OK;
}

static neat_error_code
on_all_written(struct neat_flow_operations *ops)
{
    ops->on_readable = NULL;
    ops->on_writable = NULL;
    ops->on_all_written = NULL;
    neat_set_operations(ops->ctx, ops->flow, ops);

    neat_close(ops->ctx, ops->flow);

    return NEAT_OK;
}

static neat_error_code
on_connected(struct neat_flow_operations *ops)
{
    ops->on_readable = on_readable;
    neat_set_operations(ops->ctx, ops->flow, ops);

    return NEAT_OK;
}

You may find the complete source for the updated server here. A minimal CMakeLists.txt for cmake can be found here.