Inspirel banner

Programming Distributed Systems with YAMI4

9.1.2 C++

The C++ client and server can be compiled by running make, assuming that the core and C++ libraries were already compiled.

The client program is entirely implemented in the client.cpp file, which is dissected in detail below.

First the YAMI4 header file is included:

#include <yami4-cpp/yami.h>

The yami.h header file groups all public header files for the C++ library. Users might be tempted to streamline the compilation process by selectively including only those header files that are actually needed, but in practice no significant gain should be expected from doing this - therefore, in these simple examples the single header approach is used.

Some standard headers are also essential to clearly manage program structure and for reading text from the standard input:

#include <cstdlib>
#include <iostream>

The whole client program is implemented within a single main() function. Initial actions in this function check whether there is one required argument describing server destination address:

int main(int argc, char * argv[])
{
    if (argc != 2)
    {
        std::cout
            << "expecting one parameter: server destination\n";
        return EXIT_FAILURE;
    }

    const std::string server_address = argv[1];

After the program arguments are processed, client can create its own YAMI agent.

    try
    {
        yami::agent client_agent;

The whole logic of the client program is devoted to reading lines of text from standard input:

        // read lines of text from standard input
        // and post each one for transmission

        std::string input_line;

        while (std::getline(std::cin, input_line))
        {

For each line of text, a new message is sent to the server with parameters object that contains the given text. Once the parameters object is created, it can be filled with data - here, only one data entry is needed:

            yami::parameters params;

            // the "content" field name is arbitrary,
            // but needs to be recognized at the server side

            params.set_string("content", input_line);

The data entry is named "content", but the name does not matter as long as it is properly recognized by the server. In other words, the choice of entry names forms a kind of contract between client and server and can be compared to the naming of method parameters in object-oriented programming.

Everything is now ready for sending the message:

            client_agent.send_one_way(server_address,
                "printer", "print", params);

The message is sent as one-way, which means that there is no feedback on its progress.

The arguments to the send_one_way() function are:

It should be noted that the message is being sent in background, so the user task can immediately continue and read another line of text.

The program code concludes with rudimentary exception handling:

        }
    }
    catch (const std::exception & e)
    {
        std::cout << "error: " << e.what() << std::endl;
    }
}

Another version of the client program (client_synchronous.cpp) synchronizes each message separately and allows to guarantee that all lines of text are really delivered to the server, so that when the client program finishes there are no messages pending in its outgoing queue.

The code of that alternative client is very similar:

#include <yami4-cpp/yami.h>

#include <cstdlib>
#include <iostream>

int main(int argc, char * argv[])
{
    if (argc != 2)
    {
        std::cout
            << "expecting one parameter: server destination\n";
        return EXIT_FAILURE;
    }

    const std::string server_address = argv[1];

    try
    {
        yami::agent client_agent;

        // read lines of text from standard input
        // and post each one for transmission

        std::string input_line;

        while (std::getline(std::cin, input_line))
        {
            yami::parameters params;

            // the "content" field name is arbitrary,
            // but needs to be recognized at the server side

            params.set_string("content", input_line);

The only implementation difference is the part that sends the message - instead of posting a one-way message to the queue, a regular message object is created and later used for synchronization with its delivery:

            std::auto_ptr<yami::outgoing_message> om(
                client_agent.send(server_address,
                    "printer", "print", params));

The user code can then synchronize itself with the message delivery by waiting on its transmission condition:

            om->wait_for_transmission();

The above call ensures that the user code will not proceed until the line of text is sent to the server - if this was the last line, the program can finish immediately by exiting the main loop without any risk of losing pending messages.

Program code concludes with simple exception handling:

        }
    }
    catch (const std::exception & e)
    {
        std::cout << "error: " << e.what() << std::endl;
    }
}

The server program (server.cpp) is also implemented in a single file and begins with appropriate include directive:

#include <yami4-cpp/yami.h>

#include <cstdlib>
#include <iostream>

An important part of every server is the definition of its message handlers. In this example there is only one such handler:

void call(yami::incoming_message & im)
{

The above function signature represents the simplest possible callable entity that can accept the incoming object - more elaborated servers with stateful message handlers will use functors (classes with appropriately overloaded function call operator) to define their state components.

    const yami::parameters & params = im.get_parameters();

    // extract the content field
    // and print it on standard output

    std::cout << params.get_string("content") << std::endl;
}

In this example the call() function has to print the line of text that was sent by remote client on the screen - here this process is straightforward since the parameters object that was passed together with the message is accessible via reference. No other processing is performed and in particular no feedback - neither reply nor rejection - is sent back to the client.

In its main part the server program first verifies whether it was given a single required argument that is interpreted as a server target:

int main(int argc, char * argv[])
{
    if (argc != 2)
    {
        std::cout
            << "expecting one parameter: server destination\n";
        return EXIT_FAILURE;
    }

    const std::string server_address = argv[1];

Then the agent object is created and a listener is added to it with the given target name:

    try
    {
        yami::agent server_agent;

        const std::string resolved_address =
            server_agent.add_listener(server_address);

The resolved server address is printed on the screen to show how it was expanded and to allow the user to pass it properly to the client program:

        std::cout << "The server is listening on "
            << resolved_address << std::endl;

Then the ``printer'' object is registered with the already defined message handler (that is, with the call() function as the callable entity):

        server_agent.register_object("printer", call);

After creating the listener and registering the object the server is ready for operation - every message that will arrive from remote clients will be routed to the registered message handler and the line of text that was sent by client will be printed on the screen.

In order to allow the agent to operate in background the main thread effectively stops by blocking on standard input. Any other method of stopping the main thread would be equally good and here the only goal is to keep the agent object alive for the whole time span of server's operation. In this state the whole activity of the server program is driven by incoming messages that are received in background.

        // block
        std::string dummy;
        std::cin >> dummy;

The server code finishes with simple exception handling:

    }
    catch (const std::exception & e)
    {
        std::cout << "error: " << e.what() << std::endl;
    }
}