ARICPP: an ARI library for modern C++

ARICPP: an ARI library for modern C++

As is well known, there are several ways to extend Asterisk features, but if you want to exploit the full power of its raw primitive objects — channels, bridges, endpoints, media, etc. — you really need to use ARI. On the other hand, if the performances of your application are important, chances are you’re using C or C++. Unfortunately, until now there was no C/C++ library available to interface with ARI, and even finding a good HTTP/WebSocket library was certainly not child’s play.

Some months ago, I started working on a C++ project that required a strict interaction with Asterisk internal objects and, not finding a library, I decided to develop one myself. The result is aricpp: a C++14 library for Asterisk ARI interface, released under the Boost Software License.

This is a short summary of its main characteristics :

  • it’s multi-platform. It uses standard ISO C++ so you will be able to use it on every platform having a decent C++ compiler.
  • It’s header only. No need to compile the library to use it: just include aricpp header files in your project and you’re done.
  • It’s asynchronous (more on this later).
  • It provides a high-level interface for the manipulation of asterisk concepts (channels, bridges, …) developed on top of a low-level interface with primitives to send and receive ARI commands and events from asterisk. The two interfaces coexist and can be mixed in the same client application.

Last but not least, aricpp is production-ready. Though I’d like to add more features, the library is already used in several projects, running 24/7 under high call loads.

Aricpp design

When you develop an application subject to a high rate of incoming requests, going asynchronous is a good way to improve performances, compared to spanning a new thread per request. Furthermore, with an asynchronous event-driven model, a single thread can handle multiple requests, so that the developer doesn’t have to worry about synchronization, thus reducing latencies and avoiding potential races and deadlocks.

Aricpp design is asynchronous at the core, and its features are exposed to the client through non-blocking APIs taking continuation callbacks as parameters.

Please note that having non-blocking APIs doesn’t necessarily mean the client application must be single threaded. On the contrary, the async APIs give the user of the library the freedom to choose which thread model adopt:

  • single thread
  • one thread per request
  • a thread pool

though I strongly suggest using a single thread, since (in this case) I can’t see any real benefit in the other two options.

Aricpp takes advantage of C++14 standard, that provides lots of useful abstractions ready to use. Nevertheless, it has dependencies from two other libraries: boost and beast. Beast is the library that provides HTTP and WebSockets, that in turns requires boost libraries. It’s planned that beast will be integrated into boost 1.66 (the next release, at the time I’m writing) so that in a few weeks aricpp will depend only on boost libraries.

While I don’t like very much beast interface design (I hope it will be improved during the process of integration in boost 1.66 ), I chose to use it anyway because it follows boost ASIO idioms (and also because it’s the only HTTP/WebSocket asynchronous library available :-).

With the help of beast library, aricpp provides its low-level interface: the class aricpp::Client  having the two public methods RawCmd and OnEvent  (respectively to send HTTP commands to asterisk and to receive JSON formatted events from asterisk).

The high-level interface aricpp::AriModel provides classes that reflect the asterisk objects (aricpp::Channel , aricpp::Bridge  and aricpp::Recording ) and exposes methods to register callbacks for the main events:

class AriModel
{
public:
    ...
    void OnChannelCreated(ChHandler handler);
    void OnChannelDestroyed(ChHandler handler);
    void OnChannelStateChanged(ChHandler handler);
    void OnStasisStarted(StasisStartedHandler handler);
    ...
};

Ok, show me some code!

Since aricpp is based on boost asio async library, you must have an instance of boost::asio::io_service running. Then, aricpp features are available through an instance of aricpp::Client :

#include <boost/asio.hpp>
#include "aricpp/client.h"

using namespace aricpp;

...
boost::asio::io_service ios;
aricpp::Client client(ios, host, port, username, password, stasisapp);
...
ios.run();

The client can establish a connection with asterisk using the method Client::Connect :

Client client(ios, host, port, username, password, stasisapp);
client.Connect( [](boost::system::error_code e){
    if (e) std::cerr << "Error connecting: " << e.message() << '\n';
    else std::cout << "Connected" << '\n';
});

Once connected, you can register for raw events by using the method Client::OnEvent :

client.Connect( [&](boost::system::error_code e){
    if (e)
    {
        cerr << "Error connecting: " << e.message() << '\n';
        return;
    }
    cout << "Connected" << '\n';
    client.OnEvent("StasisStart", [](const JsonTree& ev){
        Dump(ev); // print the json on the console
        auto id = Get<string>( ev, {"channel", "id"} );
        cout << "Channel id " << id << " entered stasis application\n";
    });
});

As you can see from the snippet, aricpp provides a couple of handy functions to manipulate JSON data for when you’re using the low-level interface.

Finally, you can send requests by using the method Client::RawCmd :

client.RawCmd(
    Method::get,
    "/ari/channels",
    [](boost::system::error_code error, int state, string reason, string body)
    {
        // if no errors, body contains the detail of all channels
    }
);

As already mentioned, aricpp also provides a higher level interface, with which you can manipulate asterisk telephonic objects (e.g., channels).

To use this interface, you need to create an instance of the class aricpp::AriModel, on which you can register for channel events ( AriModel::OnStasisStarted , AriModel::OnStasisDestroyed ,  AriModel::OnChannelStateChanged  ) and allows you to create channels (AriModel::CreateChannel() ).

All these methods give you references to aricpp::Channel  objects, that provide the methods for the usual actions on asterisk channels (e.g., ring, answer, hangup, dial, …).

The following code connects to asterisk, calls the endpoint pjsip/Bob  and waits for a channel to enter in the stasis application, differentiating between channels created by the library and external ones.

#include "aricpp/arimodel.h"

...

boost::asio::io_service ios;
aricpp::Client client(ios, host, port, username, password, stasisapp);
aricpp::AriModel channels(client);

client.Connect( [&](boost::system::error_code e){
    if (e)
    {
        cerr << "Connection error: " << e.message() << endl;
        ios.stop();
    }
    else
    {
        cout << "Connected" << endl;
        channels.OnStasisStarted(
            [](shared_ptr<Channel> ch, bool external)
            {
                if (external) { /* manage an incoming call */ }
                else { /* manage a channel created from the library */ }
            }
        );
		
        auto ch = channels.CreateChannel();
        ch->Call("pjsip/Bob", stasisapp, "Alice")
            .OnError([](Error e, const string& msg)
                {
                    if (e == Error::network)
                        cerr << "Error creating channel: " << msg << '\n';
                    else
                        cerr << "Error: reason " << msg << '\n';
                }
            )
            .After([]() { cout << "Call ok\n"; } );
	}
});
...
ios.run();

As you can see, the high-level interface offers a fluent syntax to specify asynchronous callbacks:

ch->Call(...).After(...).OnError(...)

This code basically means:

  1. require asterisk to perform an originate using the channel ch ,
  2. if aricpp receives a positive response from asterisk, the code specified with After(…) is executed,
  3. if aricpp receives an error response, instead, the code specified with OnError(…) is executed.

Please note that these are only convenience methods to specify callbacks, that improve a bit the syntax with respect to the usual way to pass them as a parameter, but by no means they’re “futures”. Even if you can chain multiple After methods, they would be all executed in sequence when asterisk will have issued its response. So, these methods don’t save you from the so-called “callback pyramid” in those (rare) cases when you need to chain a sequence of ARI requests.

For this reason, I’d like to provide in the next release a new interface using futures, so that a client could write something like:

ch1->Hangup()
.Then([=](){ return bridge->Destroy(); })
.Then([=](){ return ch2->Hold(); })
.Then([=](){ cout << "ch2 in hold\n"; })
.OnError([](exception const& e) {cerr << e.what() << endl; });

instead of the current:

ch1->Hangup()
.After([=]()
{ 
    bridge->Destroy()
    .After([=]()
    { 
        ch2->Hold()
        .After([=]()
        { 
            cout << "ch2 in hold\n"; 
        })
        .OnError([](Error e, const string& msg) 
        {
            cerr << msg << endl; 
        }); 
    })
    .OnError([](Error e, const string& msg) 
    {
        cerr << msg << endl; 
    });  
})
.OnError([](Error e, const string& msg) 
{
    cerr << msg << endl; 
});

As you can see, an interface with futures would permit code more compact.

Please note that the high and low-level interfaces can coexist. Since the high-level interface does not have yet classes for all the asterisk objects (i.e., devices, endpoints, mailboxes,… ), in the meantime you can use the low-level interface for the missing commands.

Performances

From the very first release of aricpp, my goal was to provide a library as much as reliable as possible and optimized to work with a heavy load of telephonic traffic. For that reason, after writing the core of the library, I developed a simple dial application based on aricpp in order to take some measurements with a SIP traffic generator and try to optimize the library.

As a plan on paper, it was a good one. Except that this process led me to discover an asterisk performance issue, instead. During my tests, I discovered a uniform distribution latency in the range [0, 200ms] every time asterisk receives an ARI command. I discussed the topic in the community and then I filed an issue that (at the time I’m writing) has been aknowledged but not closed yet.

To cut it short: I used the traffic generator anyway, but only to prove that the library is correct and robust. I hope asterisk developers will remove the latency soon, so that we can test aricpp performances and optimize it, and — above all — create ARI clients able to managing high traffic loads.

Miles to go…

Aricpp is already production ready, it is used in mission-critical projects, and runs twenty-four hours a day under heavy load. However, some improvements can be made:

  • add the missing asterisk ARI objects to the high-level interface: devices, endpoints, mailboxes…
  • provide a better syntax to simplify the code needed for an asynchronous sequence of operation (i.e., custom futures)
  • improve the error handling mechanism
  • provide new examples and documentation.

In the meantime, any comment about the library is welcome.

12 Responses

  1. Hi Daniele,
    We’ve spoken before in the blog. In the past I wrote in another article I was developing a c++ http/websocket client and you showed me yours.
    But today I have a concern.
    I am worried about the max length of the content-length. When i send a GET ari/channels, i need to specify the size of the buffer to read the http response. For now i just used 4096 as I read in some posts (i.e. https://issues.asterisk.org/jira/browse/ASTERISK-24883)

    Do you know if the request will fail in case it exceeds the 4096 length?
    In my GET ari/channels request, i might have several channels active on asterisk and that size will might exceed the 4096.

    Questions:
    1) Do you know how asterisk will behave if it exceeds 4096 length response message? If it will fail to answer, or if it will send me a second message wth the remaining channels?

    2) I have a server application which will need to connect to asterisk and retrieve all the channels already active. Let’s say my server crashed and i have several calls in progress. When i restore my server, i will need to sync it with asterisk (meaning… i will need to check with asterisk what channels are active as soon as it restarts). Is there an alternative to the GET ari/channels ?

    Thanks in advance,
    Fabio

  2. Hi Fabio,
    I admit I didn’t know about this asterisk limitation. So, first of all, I’d like to thank you for letting me know.
    I haven’t performed any experiment yet, but from what I can read in the issue # 24883 of jira, I understand that:

    1) asterisk will fail to answer a request if the response would exceed 4096 bytes. It does not split the response into multiple messages.

    2) I’m afraid there is no alternative to get all the channels.

    Actually, I’ve never faced this problem. Often, my applications live on the same server with asterisk, so they’re launched at the same time and no channel can exist when the ari application starts.

  3. Thanks for your quick response Daniele,

    As I read in Asterisk ARI documentation,
    “HTTP
    Asterisk’s HTTP server now supports chunked Transfer-Encoding. This will be automatically handled by the HTTP server if a request is received with a Transfer-Encoding type of chunked.”
    I was wondering if the chunked option would allow me to get big messages separated in different chunks. I am not very familiar with http protocol but that looks promising.
    I will try to prepare a scenario with lots of active channels and try to simulate the issue, and then enable the chunk option to see if it works.

  4. [UPDATES]
    I just ran another test with 60 active channels and it worked fine. So maybe there is no limitation on the content-length

    60 active channels
    60 active calls
    ContentLength ==================== 24896

    And I was able to get all the channels information and parse them right out of the json to a ptree.

  5. Very good, Fabio.
    So can we assert with some degree of certainty that we don’t have the issue? Even using Aricpp (I know that you use boost::beast, too)? Have you done something explicitly to enable chunked Transfer-Encoding (in asterisk or in boost::beast)?
    I’m currently very busy with my job and conferences, but as soon as possible I’m gonna test the HTTP GET using Aricpp to be sure we don’t have the issue.
    Thanks.

  6. Hi Daniele,
    I believe we can assert that the max content-length of 4096 is not true. Anyway I will still try to contact asterisk-app-dev mailing list to check what’s the current content-length limitation and how should we handle a bigger content (the server must define a max content-length).

    Info:
    – I didn’t have to enable chunked transfer-encoding on my request or neither on asterisk.
    – My cpp lib choices are very similar to yours. I am also using boost::beast.

    My next step is to try to contact that asterisk-app-dev mailing list. I have some other implementation doubts related with ARI that i need to figure out how i am supposed to do, but gladly i am moving forward.

    I will keep you updated on this post.

  7. Done, I sent an email to the emailing list. Let’s wait for their response.

    I basically asked:
    1) Can you tell me what’s the current max content-length allowed for the http responses?
    2) What happens if I exceed that max allowed size? Is it still possible to maybe define a chunked Transfer-Encoding to get that information in more than 1 message?

  8. You wrote:

    “I don’t like very much beast interface design (I hope it will be improved during the process of integration in boost 1.66 )”

    I can only address things that I know about, so if you would like to open an issue describing your specific objections to beast’s interfaces it would help me considerably. I can’t guarantee that I will agree with everything but I consider all feedback from users. This is especially important since I plan on writing a paper to propose Beast for the C++ standard library. The more feedback I get, the better this paper will be.

    Issues can be opened on the official repository page at GitHub.

    Thanks!

  9. Hi Vinnie,

    first of all, I’d like to thank you for your useful library and your desire to improve it.
    I use BEAST in several projects and I think it’s the best HTTP and Websocket C++ library available. I like your decision to develop it on top of BOOST ASIO and use a similar interface. Above all, I find very useful the asynchronous interface.

    Nevertheless, I believe that sometimes user-defined types in the interface would make the library less error-prone with respect to basic types. After all, in C++ we’ve always written code in such a way that the compiler could detect as many errors as possible, especially by defining user types, right?

    See, for example:
    https://github.com/boostorg/beast/issues/123
    and
    https://github.com/boostorg/beast/issues/128
    (as you can see, I already tried to give my little contribution to your library :-).
    The first one has been “fixed” in the meantime, but not the second.

    I think the library is pretty usable anyway, but I can’t help asking myself why should I use a type that admits 2^32 possible values when only 2 are valid (10 and 11) 🙂

    Thanks,
    Daniele.

  10. Great information. Since last week, I am gathering details about the c++ experience. There are some amazing details on your blog which I didn’t know. Thanks.

  11. unbelievable news. It is a really outstanding article. I use BEAST in several projects and I think it’s the best HTTP and Websocket C++ library available. Thanks for sharing this blog.

Leave a Reply

Your email address will not be published. Required fields are marked *


The reCAPTCHA verification period has expired. Please reload the page.

About the Author

What can we help you find?