Introducing Nyx. Basic Networking API
PROGRAMMING
Introducing Nyx. Basic Networking API
2016-04-06
By
David "DeMO" Martínez Oliveira

Recently I had been working on a small library to put together some general functions I often use in my programs. I started working on some generic networking capabilities. They are is still in the early stages and, to be honest, I'm not very happy with the progress so far. Finally there is something to talk about so I will write a little bit about it.
First...I guess you may be wondering where that name comes from. So, there you go.

http://en.wikipedia.org/wiki/Nyx

Nyx – is the Greek goddess (or personification) of the night. A shadowy figure, Nyx stood at or near the beginning of creation, and was the mother of other personified deities such as Hypnos (Sleep) and Thanatos (Death). Her appearances are sparse in surviving mythology, but reveal her as a figure of such exceptional power and beauty, that she is feared by Zeus himse lf. She is found in the shadows of the world and only ever seen in glimpses.

You get the point, don't you?...

In this brief post I will show how to build a pretty basic TCP server using Nyx. More interesting things are coming but for the time being, let's start with a modest echo server.

You can download the library and also the example I will describe here from my github account:

git clone https://github.com/picoflamingo/nyx.git

A Simple ECHO Server

THe basic echo server can be found at test/test_server.c Let's jump straight into the main function:

int
main (int argc, char *argv[])
{
  NYX_NET    *net;
  NYX_SERVER *s;

  nyx_init ();

  s = nyx_server_new ("*", 8000);
  
  net = nyx_net_init ();
  nyx_server_register (s, net, consumer);
  nyx_net_run (net);
  
  nyx_server_free (s);
  nyx_net_free (net);

  nyx_cleanup ();
  return 0;
}

That is basically it for the main function. The program uses two NYX objects:

  • NYX_NET. These objects are intended to keep all the network related stuff together and provide a simple top level main loop interface to the application. They keep a list of channels in use and manage data reading from them.
  • NYX_SERVER. Represents a generic server. To use it you just need to create the object with an address (interface) and port to bind to. Then you have to register this object into a NYX_NET object. The third parameter passed to nyx_server_register is a function to execute when data sent by a client is available.

After creating and initialising the object. The program starts its main loop (nyx_net_run()) and whenever it is finished (it will never end in this example) memory is cleaned up, destroying all objects.

Before explaining how the client callback works we need to look deeper into the NYX_SERVER object.

The NYX_SERVER object

A NYX_SERVER object represents a generic TC{ server and it is composed of five elements:

  • NYX_CHANNEL s. This is the listen/server socket. The one accepting connections. Whenever a connection is accepted in this channel, a new client is added to the clients list.
  • NYX_LIST clients. This is a generic list containing NYX_CHANNEL objects associated to each currently connected client.
  • NYX_QUEUE q. The default NYX_SERVER object works on text based protocols, where each line is considered a new message. Data is read from the network channels when available. After each data read the system looks for a full text line, and pushes it into this queue.
  • NYX_WORKER w. The worker object is basically a thread getting messages from the queue and processing them.
  • NYX_NET net. We had already talked about this. Each server is associated to a network object abstraction.

NYX_SERVER Working Internals

Overall this is how it works. The server socket is created and added to a NYX_NET object that will be polling it. When a remote client tries to connect to that socket, the following things happen:

  • The connection is automatically accepted.
  • A new NYX_CHANNEL object is created to manage this new connection.
  • The new created channel is added to the internal client list and it is also registered in the associated NYX_NET object, so any data received on that channel will be polled as part of the main loop.
  • The NYX_CHANNEL object is configured so that when data is available in the network, that data is read in a internal buffer and whenever a full line is found, it is pushed into the server associated NYX_QUEUE.

NYX_SERVER and NYX_CHANNEL objects does all the networking work under the hood. The only thing we have to provide is a function to do what we want our server to do.

The Server Function

In this case, we are implementing a simple echo server. The server just send back whatever it receives. Let's see how does the function looks like:

void*
consumer (void *arg)
{
  NYX_WORKER  *w = (NYX_WORKER *) arg;
  NYX_SERVER  *s = (NYX_SERVER*) nyx_worker_get_data (w);
  NYX_QUEUE   *q;
  NYX_NET_MSG *msg;

  q = nyx_server_get_queue (s);
  while (1)
    {
      msg = (NYX_NET_MSG*) nyx_queue_get (q);

      nyx_server_printf (s, msg->c, "ECHO:%s\n", msg->data);

      free (msg->data);
      nyx_net_msg_free (msg);
    }
}

The function is actually executed in a separate thread... it is a worker. And it just receives a single parameter that, by default, it a pointer to a NYX_WORKER object. This object holds all the required information to process any client request.

Before starting to process messages forever, the worker gets some information it will need to provide the service: The server it is working for and the queue to get messages from.

Then it starts to get messages from the queue as they arrive and process them. We have to do a extra comment on this.

When using a NYX_NET object, the data coming from the network is pushed into the queues using as NYX_NET_MSG object. These messages contains the message data and the NYX_CHANNEL object that produced that data. This is how the worker objects will know where to send their replies back.

Adding More Workers

The example we had just seen is a bit simplistic. Let's go one step beyond to see the potential of Nyx, and add one more worker to our echo server.

We just need to add two lines, in the main function, just before the server registration:


...
  net = nyx_net_init ();
  NYX_WORKER *w = nyx_worker_new (nyx_server_get_queue(s), (void*)s);
  nyx_worker_start (w, consumer, (void*)w);

  nyx_server_register (s, net, consumer);
...

The code above is quite straightforward. First the worker object is created calling nyx_worker_new. The server and associated queue are passed as parameter to the worker constructor.

Then we just need to start the worker calling nyx_worker_start. This function receives as first parameter, the NYX_WORKER object we want to start, then the function to execute whenever a message is available in the workers associated queue, and finally the parameter that will be passed to that function. In this case just the NYX_WORKER object as it has all the information we need.

Now, two workers (threads) will be processing messages and sending messages back. In order to see that such a thing is actually happening we will also do a small change in the server function. Let's add the thread id to each reply so we can see which worker is sending the echo message.

void*
consumer (void *arg)
{
  NYX_WORKER  *w = (NYX_WORKER *) arg;
  NYX_SERVER  *s = (NYX_SERVER*) nyx_worker_get_data (w);
  NYX_QUEUE   *q;
  NYX_NET_MSG *msg;

  q = nyx_server_get_queue (s);
  while (1)
    {
      msg = (NYX_NET_MSG*) nyx_queue_get (q);

      nyx_server_printf (s, msg->c, "ECHO:(%p)%s", (void*)w->tid, msg->data);

      free (msg->data);
      nyx_net_msg_free (msg);
    }
}

So this is it for now... As mentioned above, I will post some more interesting contents soon... I still need to work them out a bit more.