In this tutorial we will create a simple, decentralized chat application with Dinemic. This application will be able to run in two modes: listen what other people says and send own message to network. You can find application from this example at github repository.

Create data model

All data models, which you may to use in your application should inherit DModel class. This is an interface to Dinemic ORM, which allows to synchronize data across all nodes in network and store it securely at your local node.

#include <libdinemic/dmodel.h>

class Person : public DModel
{
public:
    Person(const std::string &db_id, StoreInterface *store, SyncInterface *sync, DModel *parent=NULL);
    Person(DModel *parent);
    Person(StoreInterface *store,
           SyncInterface *sync,
           const std::vector &authorized_keys);

    void set_name(std::string name);
    std::string get_name();

    void say(std::string phrase);
};

We have to define all three constructors known from DModel. First and second constructors are used to recreate objects in running applications, on other nodes. In our example we won’t do any additional tasks there. The third constructor is used to create new objects. Let’s set our nick name to New born for each new person by DModel’s set:

set("name", "New Born");

Additionally, when creating constructor, you have to pass to DModel name of your data model. Finally this constructor will look like following:

#include "person.h"

using namespace std;

Person::Person(StoreInterface *store, SyncInterface *sync, const std::vector &authorized_keys)
    : DModel("Person", store, sync, authorized_keys)
{
    set("name", "New Born");
}

To make our data model functional, we can create some additional methods: set_name, get_name and say. First two will be used to set our nick name in chat, after creating objects. Again, we will use DModel’s data api: set and get methods:

void Person::set_name(string name) {
    set("name", name);
}

string Person::get_name() {
    return get("name");
}

The last method will set the passphrase, which we want to say:

void Person::say(string phrase) {
    set("phrase", phrase);
}

Why we set passphrase as field? Each time we change anything in our model, an update is broadcasted to all other nodes in network. This causes, that we will be able to monitor changes of this field in “listen” mode of our application. We will show how to monitor this changes in next chapters.

Check libdinemic repository for person.h and person.cpp files.

Create main application – DApp

Once our model is ready, we should create main app. Dinemic framework provides application skeleton, for creating console applications. This is the best way to simply create our application, without knowledge about all internals of framework. Let’s create new Chat class, which will be our main application:

#include <iostream>
#include <libdinemic/dapp.h>
#include "person.h"

class Chat : public DApp
{
public:
    Chat(int argc, char **argv);
    void create();
    void oneshot();
};

DApp class defines how application should work and enforces you to define this behaviors. Dinemic allows to run application in three modes: create, oneshot and launch. The create mode is used to create all models in our application, create encryption keys and, if necessary you can install your app in system. The second mode – oneshot is used to update models and execute short, one time actions. For example, if you use cron to monitor your system, you can launch your application with oneshot mode and store collected data. In our example, oneshot will be used to send messages to all chat members. The launch mode starts application in listener mode and monitors for changes in network. We will return to this mode in next chapters.

First of all, we will define constructor of our application. Here you should define additional parameters and set name of application. As you can see above, Chat class takes only two parameters: argc and argv. DApp class takes additional two (name and app description), which should be passed in constructor:

#include "chat.h"

using namespace std;

Chat::Chat(int argc, char **argv)
    : DApp(argc,
           argv,
           "Chat",
           "Simple example of using Dinemic Framework. Launch in create mode to create your new identity in network. Launch in oneshot mode to send messages. Launch in launch mode to listen for others messages.")

Inside constructor we can add some command line parameters to our application. With add_option method from DApp class we can add short and long options, for instance -n or –name:

add_option("n", "name", "Your name in chat", false, DAppOptionMode_create);

add_option("i", "id", "Identifier of person, which sends messages to chat board");
add_option("m", "message", "Send message", false, DAppOptionMode_oneshot);

In this example we will add one option to set person’s name when creating new identity, in create mode. Next, we need to add two another options for oneshot mode: id and message. This is necessary to send new messages (–message parameter) and sign it as our identity.

What are that modes? Let’s see an example of installation HTTP daemon on your system. First of all you have to install a package with your HTTP server, configure it and generate SSL certificates. You should also put some html in webroot and so on. This is the installation phase. Next you can launch your server – this is launch mode of Dinemic application, which will be described later. Webserver waits for clients, and dinemic application waits for ongoing updates form network. The third mode is the oneshot in dinemic. We can for instance update some files in webroot and write there our current mood in index.html. In dinemic application you can update your models, or read data from database through your models.

So, let’s add create method, to handle our application installation:

void Chat::create() {
    if (!has_option("name")) {
        cout << "Missing name parameter!" << endl;
        return;
    }

}

The create method needs to --name parameter is present and checks it with has_option method from DApp class:

if (!has_option("name")) {
    cout << "Missing name parameter!" << endl;
    return;
}

Then we can create new entity of Person model:

Person person(store, sync, vector());
person.set_name(get_option("name").values[0]);
cout << "Your new ID: " << person.get_db_id() << endl;

Finally we have to add oneshot mthod, which will handle application running in oneshot mode. We will update in this way field related to our phrase and thus, generate update of our model. This update will be visible on all nodes in network. Again, we should check if parameters message and id are present:

if (!has_option("message") || !has_option("id")) {
    cout << "Missing message or id parameter!" << endl;
}

Then we can recreate Person model from passed to our application ID and set new message in chat:

Person person(get_option("id").values[0], store, sync);
person.say(get_option("message").values[0]);

Why should we recreate this object? Dinemic framework assumes, that any object can be destroyed at any time, and it could be recreated in any other place of cluster. At beginning, it might seem to be strange, but it is main principle of this library. Once object of our Person class was created at one of nodes in network, its public key was announced to everybody. So, anyone in cluster can read contents of this object (unless field is not encrypted). Anyone can also write an update of this object (but without owning private key assigned to this object it will be ignored).

So, creating object in dinemic framework is the event for all nodes in network. Object appears on all nodes with its public encryption credentials. This is also why we use set/get methods from DModel class instead local fields of class. All updates are signed by Dinemic framework, transparently. In this way you are able to recreate this object anywhere in any point of cluster.

Here you can find both files with chat application: chat.h and chat.cpp

Create main.cpp

Finally, let's create main.cpp file with entry point to our application:

#include "chat.h"

using namespace std;

int main(int argc, char *argv[])
{
    Chat app(argc, argv);
    return app.exec();
}

As you can see this is quite simple. We just create application instance and execute it, through DApp.exec()

To compile our example use following CMakeLists file:

cmake_minimum_required(VERSION 2.8)
include_directories("/usr/local/include")

set(CMAKE_CXX_STANDARD 11)
set (CMAKE_CXX_FLAGS "-std=c++11 ${CMAKE_CXX_FLAGS}")

project(chat)
add_executable(${PROJECT_NAME} "main.cpp" "person.cpp" "chat.cpp")
target_link_libraries (${PROJECT_NAME} LINK_PUBLIC pthread dinemic )

To compile type in console:


cmake .
make

Starting application

Now our application is ready to use. Install Redis and create simple configuration file (or get it from here):

LOGLEVEL: error
KEYRING_DIR: /tmp/dinemic_keys
ZEROMQ_SYNC_MULTICAST_PORT: 3344
ZEROMQ_SYNC_MULTICAST_ADDR: 239.0.0.33
STORE: MemoryDriver
REDIS_SERVER: localhost
REDIS_PORT: 6379
REDIS_DATABASE: 1

and launch our application. First, you need to create your identity in chat:

./chat --create --name John
II void DConfig::open(const string&)(18081)[/data/maciek/C++/libdinemic/libdinemic/dconfig.cpp:18]: Loading configuration file from ./config.dinemic
Your new ID: Person:ce058f2e8712e1bcda75bbea705b3c0d090617a88b1064cea4f2b540ca4c3a6764f61ad504bf9b447075a5d6dbae8b970ca1f24db7e26348f7f10f1fcc0a1f65

Then, with your ID (check output!) you can send new message in oneshot mode:

./chat --oneshot --id Person:ce058f2e8712e1b... --message "Hello world!"

At this point, your message will not appear at other instances of chat application. To monitor changes and print user messages on screen, we need to add the listener.

Listeners

Actions in dinemic framework are dedicated tasks to be executed when something happens in database. Each action could be assigned to single field, object or whole database. With this mechanism you can control what happens in database - action could prevent updating it and how your application reacts on each change.

Each action can execute some logics on each of following events in your local database:

  • on create - before new object is created
  • on created - after object was created
  • on update - before field, list or dictionary in object was updated
  • on updated - after update
  • on delete - field, list or dictionary will be removed
  • on deleted - field, list or dictionary was removed
  • on removed - whole object was removed from database

Each action is an object, inheriting the DAction class form framework. To create one, simply inherit this class as in following example:

#include 
#include 
#include 
#include 
#include "person.h"

class ChatListener : public DAction
{
public:
    ChatListener();

    void apply(SyncInterface *sync, const std::string &filter="");
    void revoke(SyncInterface *sync, const std::string &filter="");

    void on_create(DActionContext &context, const std::string &key);
    void on_update(DActionContext &context,
                   const std::string &key,
                   const std::string &old_value,
                   const std::string &new_value);
};

Now we can create methods on_create which will be called when new person appears in database:

void ChatListener::on_create(DActionContext &context, const std::string &key) {
    std::cout << "New person was created: " << context.get_object().get_db_id() << std::endl;
}

and another, which will be called, when any Person state is updated:


void ChatListener::on_update(DActionContext &context, const std::string &key, const std::string &old_value, const std::string &new_value) {
    // Recreate person object. We will recreate it from ID stored in context
    Person person(context.get_object().get_db_id(), context.get_object().get_store_interface(), context.get_object().get_sync_interface());

    // Check if field, which was updated is called phrase. The same could be achieved by filter
    if (key == "value_phrase") {
        std::cout << person.get_name() << " says: " << new_value << std::endl;
    }
}

Due to code can be executed anywhere in our cluster (exactly anywhere where our chat is launched), we need to recreate Person object each time form update details, sored in context.

Additionally we can add validation checks to our listener:


    if (!context.is_verified()) {
        throw DFieldVerificationFailed("Update is not verified");
    }

at the beginning of the method. This will prevent from unauthorized modification of Person models. This is cryptography based mechanism, which relays on underlying dinemic tools. Now, for you it is important to know only that you can check in context object, wether the update was digitally signed, by is_verified method.

Each time if Person model will be updated and modified key is phrase (internally key is string value_phrase in condition), our listener will print what was updated.

Additional two methods - apply and revoke could be defined to connect DAction listener with proper events in database. For example, to set action filter to certain field of model.

void ChatListener::apply(SyncInterface *sync, const std::string &filter) {
    sync->add_on_create_listener("Person:*", this);
    sync->add_on_update_listener("Person:*:value_phrase", this);
    sync->add_on_update_listener("Person:*:value_name", this);
}

void ChatListener::revoke(SyncInterface *sync, const std::string &filter) {
    sync->remove_on_create_listener("Person:*", this);
    sync->remove_on_update_listener("Person:*:value_phrase", this);
    sync->remove_on_update_listener("Person:*:value_name", this);
}

Without it you will have to manually set filters each time the listener is applied to SyncInterface.

Finally, we can create our listerer in Chat application and apply it directly on SyncInterface instance:

Chat::Chat(int argc, char **argv)
    : DApp(argc, argv, "Chat", "Simple example of using Dinemic Framework. Launch in create mode to create your new identity in network. Launch in oneshot mode to send messages. Launch in launch mode to listen for others messages.")
{
    add_option("n", "name", "Your name in chat", false, DAppOptionMode_create);
    add_option("i", "id", "Identifier of person, which sends messages to chat board");
    add_option("m", "message", "Send message", false, DAppOptionMode_oneshot);

    listerer.apply(sync);
...

Now, update the CMakeLists file and recompile application. Call ./chat --launch to wait for changes in database and check how listener works.

The full code of DAction example could be found here (source) and here (header).