|
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.
Some standard headers are also essential to clearly manage program structure and for reading text from the standard input:
#include <cstdlib> #include <iostream>
There is an additional header used here that declares a commonly used function for converting strings to integers (this function is used by other examples as well):
#include "../common_utils/string_to_int.h"
The whole client program is implemented within a single main()
function.
Initial actions in this function check whether there are three required arguments describing server destination address and two integers.
int main(int argc, char * argv[]) { if (argc != 4) { std::cout << "expecting three parameters: " "server destination and two integers\n"; return EXIT_FAILURE; } const std::string server_address = argv[1]; int a; int b; if (examples::string_to_int(argv[2], a) == false || examples::string_to_int(argv[3], b) == false) { std::cout << "cannot parse the second or third parameter" << std::endl; return EXIT_FAILURE; }
In order to send the message three entities are needed at the client side: agent to handle the communication, parameters object to carry the payload (the two integer values) and the outgoing message object for progress tracking.
The agent is created first as the managing entity:
try { yami::agent client_agent;
The client program has to prepare the content or payload of the message that will be sent to the server. In this example the content parameters object incorporates two integer values names ``a'' and ``b'':
yami::parameters params; params.set_integer("a", a); params.set_integer("b", b);
When the parameters object is filled with data, the actual outgoing message is created:
std::auto_ptr<yami::outgoing_message> om( client_agent.send(server_address, "calculator", "calculate", params));
Above, the send()
function is invoked with the following parameters:
server_address
- The server target, as it was resolved on the server side when the server's listener was created; this is the string that was printed by the server just after starting and passed to the client program as its first argument."calculator"
- The logical destination object name; this name has to be known by the server agent, which is true if the message handler was registered there with this name. This name can be interpreted as the object name within the namespace of the server's agent."calculate"
- The message name, which is simply the name of the requested operation that the server should perform. In this particular example there is only one type of message that is sent from client to server, but in bigger systems this name will be certainly used by the server to figure out what to do. For example, the calculator system might be restructured to allow separate message for each basic operation (``add'', ``subtract'', ``multiply'' and ``divide'') - then, the message name would be a very natural mean for expressing the request.params
- The payload to be sent, which here contains two integer numbers from the command-line.The outgoing message object is created as a result of this operation.
It is important to note that the actual message is transmitted to the server in background and there is no provision for it to be already transmitted when the send()
function returns. In fact, the user thread might continue its work while the message is being processed by the background I/O thread.
In this particular example, the client program does not have anything to do except waiting for the response:
om->wait_for_completion();
Depending on the message state, client either processes the result or prints appropriate report on the console.
In the most expected case the completion of the message is a result of receiving appropriate reply from the server - this can be checked with the replied
message state:
const yami::message_state state = om->get_state(); if (state == yami::replied) { const yami::parameters & reply = om->get_reply(); int sum = reply.get_integer("sum"); int difference = reply.get_integer("difference"); int product = reply.get_integer("product"); int ratio; yami::parameter_entry ratio_entry; const bool ratio_defined = reply.find("ratio", ratio_entry); if (ratio_defined) { ratio = ratio_entry.get_integer(); }
Above, the ratio_defined
variable is true
when the reply content has the entry for the result of division (and then the ratio
variable will get that value), and false
otherwise. This reflects the ``protocol'' of interaction between client and server that allows the server to send back the ratio only when it can be computed.
The code then prints all received values on the console:
std::cout << "sum = " << sum << '\n'; std::cout << "difference = " << difference << '\n'; std::cout << "product = " << product << '\n'; std::cout << "ratio = "; if (ratio_defined) { std::cout << ratio; } else { std::cout << "<undefined>"; } }
Alternatively, the message might be completed due to rejection or being abandoned - these cases are reported as well:
else if (state == yami::rejected) { std::cout << "The message has been rejected: " << om->get_exception_msg(); } else { std::cout << "The message has been abandoned."; } std::cout << std::endl; }
The client program finishes with simple exception handling.
catch (const std::exception & e) { std::cout << "error: " << e.what() << std::endl; } }
The server program is entirely implemented in the server.cpp
file.
#include <yami4-cpp/yami.h> #include <cstdlib> #include <iostream>
One of the most important server-side entities is a message handler, which has to be a callable entity - a regular function or a functor - that accepts the incoming message object as its only parameter.
In this example the message handler is implemented by a global function call()
:
void call(yami::incoming_message & im) {
The message handler extracts the parameters object that came as the incoming message's payload. The handler expects two integers named ``a'' and ``b'':
// extract the parameters for calculations const yami::parameters & params = im.get_parameters(); const int a = params.get_integer("a"); const int b = params.get_integer("b");
The server computes the results of four basic calculations on these two numbers, with the possible omission of the ratio, which might be impossible to compute if the divisor is zero. In this case the ratio entry is simply not included in the resulting parameters object:
// prepare the answer with results of four calculations yami::parameters reply_params; reply_params.set_integer("sum", a + b); reply_params.set_integer("difference", a - b); reply_params.set_integer("product", a * b); // if the ratio cannot be computed, // it is not included in the response // the client will interpret that fact properly if (b != 0) { reply_params.set_integer("ratio", a / b); }
Once the parameters object for the reply is prepared, the incoming message can be replied to:
im.reply(reply_params);
The handler finishes with a report on the console.
std::cout << "got message with parameters " << a << " and " << b << ", response has been sent back" << std::endl; }
The main()
function of the server program deals with command-line arguments, where the server target is expected.
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];
Once the server target is known, it is passed to the freshly constructed agent in order to add a new listener. The resolved target that results from this operation is printed on the console.
try { yami::agent server_agent; const std::string resolved_address = server_agent.add_listener(server_address); std::cout << "The server is listening on " << resolved_address << std::endl;
The previously defined message handler (the call()
function) is registered as an implementation of the logical object named ``calculator''. This object name has to be known by clients so that they can properly send their messages.
server_agent.register_object("calculator", call);
The server's 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; } }