Webmaster  |  Imprint 
C++ Server Pages
Main  |  License  |  Documentation  |  Download 

XMLRPC with cxxtools

Introduction

In larger systems we often have the requirement to use some functionality on a different process either on the same host or even via network on another host. This may be, that we need some data from another process or need to trigger some action there. For this we need some connection to the other host and a protocol, which we need to define. Luckily there are standards for this.

A good starting point is http as a transport protocol which is widely accepted and flexible enough for a wide range of applications. But that is not quite enough. Http defines just a request and reply structure but no content format. This is left open for best flexibility.

So we need another definition for transporting data in a http body. Since the http body is just a sequence of bytes, the operation for transforming our data structure to this byte sequence is commonly called serialization. Here we may use XML since this is a widely accepted standard.

Now we have a transport protocol and a data structure defined. Next we need to define some request and reply structure, so we can tell our server, how to interpret the data and how to interpret the answer on the client side.

There are different standard for this. We choose XML-RPC here, since it is very simple. It is a standard for calling functions via http on a remote host.

Cxxtools has simple to use features to implement both a XML-RPC server and client in C++. Lets start with a simple example.

XMLRPC client and server

We implement a xmlrpc server first.

Here is the source:

#include <iostream>
#include <cxxtools/http/server.h>
#include <cxxtools/xmlrpc/service.h>
#include <cxxtools/eventloop.h>

class CalcService : public cxxtools::xmlrpc::Service
{
  public:
    CalcService()
    {
      registerMethod("add", *this, &CalcService::add);
      registerMethod("sub", *this, &CalcService::sub);
      registerMethod("mul", *this, &CalcService::mul);
      registerMethod("div", *this, &CalcService::div);
    }

    double add(double a, double b);
    double sub(double a, double b);
    double mul(double a, double b);
    double div(double a, double b);
};

double CalcService::add(double a, double b)
{
  return a + b;
}

double CalcService::sub(double a, double b)
{
  return a - b;
}

double CalcService::mul(double a, double b)
{
  return a * b;
}

double CalcService::div(double a, double b)
{
  if (b == 0)
    throw std::runtime_error("division by zero");
  return a / b;
}

int main(int argc, char* argv[])
{
  try
  {
    cxxtools::EventLoop eventLoop;
    cxxtools::http::Server server(eventLoop, "", 7001);
    CalcService calcService;
    server.addService("/calc", calcService);
    eventLoop.run();
  }
  catch (const std::exception& e)
  {
    std::cerr << e.what() << std::endl;
  }
}

This server defines a service with 4 functions, which are offered as a XMLRPC service. To do this, we need to define a service class. We define the 4 functions as methods in our service and register them by name by calling registerMethod in the constructor.

To run the server we define a event loop and a http server object and add a service instance to this. At the end we start the event loop of our server, so it starts waiting for incoming service requests on the specified port (7001 in our example).

The second parameter of the server object specifies the ip address, where to listen. By passing an empty string, the server listens on all local addresses.

Next we create a client. Here is the source:

#include <iostream>
#include <cxxtools/xmlrpc/httpclient.h>
#include <cxxtools/xmlrpc/remoteprocedure.h>
#include <cxxtools/log.h>

int main(int argc, char* argv[])
{
  try
  {
    log_init();

    cxxtools::xmlrpc::HttpClient client("", 7001, "/calc");
    cxxtools::xmlrpc::RemoteProcedure<double, double, double> add(client, "add");
    cxxtools::xmlrpc::RemoteProcedure<double, double, double> sub(client, "sub");
    cxxtools::xmlrpc::RemoteProcedure<double, double, double> mul(client, "mul");
    cxxtools::xmlrpc::RemoteProcedure<double, double, double> div(client, "div");

    std::cout << "4 + 5 = " << add(4, 5) << std::endl;
    std::cout << "4 - 5 = " << sub(4, 5) << std::endl;
    std::cout << "4 * 5 = " << mul(4, 5) << std::endl;
    std::cout << "4 / 5 = " << div(4, 5) << std::endl;
  }
  catch (const std::exception& e)
  {
    std::cerr << e.what() << std::endl;
  }
}

We first define a xmlrpc::HttpClient instance and tell the client, where to find the service. By passing a empty string, we connect to the local host, where our server is currently running.

Next we define a class of the template type cxxtools::xmlrpc::RemoteProcedure for each function. This template needs a list of types as parameters. The first type is the return type of our function. The remainder are the types of parameters. Currently templates for up to 5 parameters are defined.

Now we are done by now. Simple isn't it?

We compile and link both programs. Assumed, the source in in files server.cpp and client.cpp and we use gcc the commands needed are:

$ g++ -o client -lcxxtools-xmlrpc client.cpp
$ g++ -o server -lcxxtools-xmlrpc server.cpp

To test it, start 2 terminals and run the server with “./server” in one terminal and run the client using “./client” in the other. You should see the result of the calculation on the client side. If you don't believe, the server is really used, try to stop the server and run the client again. It does not work. You get a error message “connection refused”. You may also want to put a std::cout to the server side, where the functions are called to verify, the server is really working. If you have more than one computer at hand, you may also want to put the server on a different host. The you have to justify the ip address in the client of course.

One thing you should try is to call div(4, 0). This will really throw the runtime_error on the server and it will be propagated to the client. Note that the exception is not of type runtime_error as thrown but a cxxtools::xmlrpc::Fault, which is derived from std::exception. All exceptions on the server side are actually thrown as cxxtools::xmlrpc::Fault on the client side, since the exception classes are normally not serializable.

Serialization

Lets look a little deeper what is going on here.

The client serialize the parameters into XML and send the XML together with a XMLRPC method call envelope to the server. The server deserialize the call with the parameters and dispatches to the right methods. The return value is again serialized into XML and sent back to the client.

So we actually need a way to serialize and deserialize data into XML. Cxxtools makes a 2 step process out of it for most flexibility. We have a step for interpreting our data into some serialization info and a step for formatting that into XML.

The second step - formatting XML is a straight forward algorithm and does not depend on the data, which need to be transformed. Therefore cxxtools has ready to go classes, which implement that. In XML-RPC this is done completely in the background, so we do not need to look further into it here.

The first step - interpreting our data is specific to our data structures. So we need to specify, how to interpret our data. The data structure, which is used in cxxtools to held the serializationinfo is called cxxtools::SerializationInfo. To serialize our data we need a converter function to and from cxxtools::SerializationInfo. Cxxtools has chosen the operators >>= and <<= to call that conversion.

SerializationInfo is a class, which held hierarchical information about your data. A member in a SerializationInfo has a name, a type name, a value and a vector of subnodes. Simple scalar types will fill the type name and value. Complex types use submembers to build hierarchical structures.

There are may standard data types in C++ which form the atomics of our data. There are predefined serialization operators for these types. You will use them to build serialization operator for complex types.

All container classes of the standard C++ library have proper predefined serialization operators. A container of some type is serializable when the contained objects are serialzable.

Standard types with predefined serialization operators are: bool, char, signed char, unsigned char, int, short, long, unsigned int, unsigned short, unsigned long, float and double.

Supported types from the standard library are std::string std::vector, std::list, std::deque, std::set, std::multiset, std::map, std::multimap and std::pair.

Note that the deserialization operators of container classes do not check the type, so that you may serialize a std::set and deserialize it as a std::vector. Only the value_type of the collection must be compatible.

Ok - lets look at a example for a serialization and deserialization operator:

#ifndef MYSTRUCT_H
#define MYSTRUCT_H

#include <cxxtools/serializationinfo.h>

struct MyStruct
{
  int _aValue;
  std::string _aString;
  std::list<unsigned long> _aList;
};

inline void operator >>=(const cxxtools::SerializationInfo& si, MyStruct& myStruct)
{
  si.getMember("aValue) >>= myStruct._aValue;
  si.getMember("aString) >>= myStruct._aString;
  si.getMember("aList) >>= myStruct._aList;
}

inline void operator <<=(cxxtools::SerializationInfo& si, const MySturct& myStruct)
{
  si.addMember("aValue) <<= myStruct._aValue;
  si.addMember("aString) <<= myStruct._aString;
  si.addMember("aList) <<= myStruct._aList;
}

#endif // MYSTRUCT_H

That's all! Lets look at some details.

First of all look at the definition of our custom type. It has no indication, that the class is serializable. We say, that the serialization system of cxxtools is non intrusive, since it does not need any changes in our custom type itself.

The first operator >>= defines our deserialization operator. It fetches 3 members from the passed SerializationInfo object and uses the >>=-operator of the standard types to read the value. Note that we do not need to mention, which type our members actually are. If we later decide to change e.g. _aValue to be a long instead of a int, the operation works as well.

Note also that since std::list<T> and the used type both are deserializable (we have predefined operator for them), the std::list<unsigned long> is deserialzable as well.

The second operator <<= defines the serialization operator. It again just uses the predefined operator for standard types to add named members to our SerializationInfo.

As with the underlying list we are ready to serialize and deserialize not only MyStruct but also containers of MyStruct like std::vector<MyStruct> or complexer types like std::map<int, std::deque<MyStruct> >.

Now we are ready to use our MyStuct in e.g. XMLRPC. Note that we have not mentioned XML here at all and indeed these operators are completely independent of XML. Cxxtools has e.g. a formatter for JSON, so we can also use the very same operators to form JSON structures. In the future we may have formatters and parsers for different formats and we keep on using our serialization operators although we do not know anything about the formats.

Let's try our new class. We again define the server similar to our first example:

#include <iostream>
#include <cxxtools/http/server.h>
#include <cxxtools/xmlrpc/service.h>
#include "myStruct.h"

class MyService : public cxxtools::xmlrpc::Service
{
  public:
    MyService()
    {
      registerMethod("doubleTheValues", *this, &CalcService::doubleTheValues);
    }

    MyStruct doubleTheValues(MyStruct myStruct);
};

MyStruct MyService::doubleTheValues(MyStruct myStruct)
{
  myStruct._aValue *= 2;
  myStruct._aString += myStruct._aString;
  for (std::list<unsigned long>::iterator it = myStruct._aList.begin();
    it != myStruct._aList.end(); ++it)
  {
    *it *= 2;
  }

  return myStruct;
}

int main(int argc, char* argv[])
{
  try
  {
    cxxtools::http::Server server("0.0.0.0", 7001);
    MyStructService myStructService;
    server.addService("/myService", myStructService);
    server.run();
  }
  catch (const std::exception& e)
  {
    std::cerr << e.what() << std::endl;
  }
}

And a client:

#include <iostream>
#include <cxxtools/xmlrpc/httpclient.h>
#include <cxxtools/xmlrpc/remoteprocedure.h>
#include "myStruct.h"

int main(int argc, char* argv[])
{
  try
  {
    cxxtools::xmlrpc::HttpClient client("127.0.0.1", 7001, "/calc");
    cxxtools::xmlrpc::RemoteProcedure<MyStruct, MyStruct> dup(client, "doubleTheValues");

    MyStruct s;
    s._aValue = 6;
    s._aString = "Hello";
    s._aList.push_back(45);
    s._aList.push_back(123);

    MyStruct r = dup(s);

    std::cout << "aValue = " << r._aValue << std::endl;
    std::cout << "aString = " << r._aString << std::endl;
    for (std::list<unsigned long>::iterator it = r._aList.begin();
      it != r._aList.end(); ++it)
    {
      std::cout << "aList: " << *it << std::endl;
    }
  }
  catch (const std::exception& e)
  {
    std::cerr << e.what() << std::endl;
  }
}

Non blocking I/O

Frequently we have programs, which need to do more than one task at once. Of course you may always start a thread for parallel processing. But this is often not the best way to go. For I/O a better way to go is to work non blocking. And luckily cxxtools makes this quite easy. What we need is a event loop, which handles the events from asynchronous operations.

Cxxtools uses a signal-slot framework for notifications. The tasks we need to do is to set up our event loop, connect the events we are interested into to slots, start our asynchronous operations and run the event loop.

Let's extend the first example. We would like to call all calculation methods in parallel. It may be that we need to collect informations from multiple hosts. They may well run in parallel so it will really reduce the total processing time.

First of all we need a event loop. We define it as a global variable to make the example simple:

cxxtools EventLoop loop;

So lets start with setting up one xmlrpc client and one remote procedure per operation. We also need to pass the event loop to the client:

    cxxtools::xmlrpc::HttpClient addClient(loop, "", 7001, "/calc");
    cxxtools::xmlrpc::HttpClient subClient(loop, "", 7001, "/calc");
    cxxtools::xmlrpc::HttpClient mulClient(loop, "", 7001, "/calc");
    cxxtools::xmlrpc::HttpClient divClient(loop, "", 7001, "/calc");
    cxxtools::xmlrpc::RemoteProcedure<double, double, double> add(addClient, "add");
    cxxtools::xmlrpc::RemoteProcedure<double, double, double> sub(subClient, "sub");
    cxxtools::xmlrpc::RemoteProcedure<double, double, double> mul(mulClient, "mul");
    cxxtools::xmlrpc::RemoteProcedure<double, double, double> div(divClient, "div");

Notice, that we really need one client per procedure, since one client can handle only one connection to the server.

Next we define some callback functions for each request:

void addCallback(const cxxtools::xmlrpc::Result<double>& r)
{
  double value = r.get();
  std::cout << "4 + 5 = " << value << std::endl;
}

void subCallback(const cxxtools::xmlrpc::Result<double>& r)
{
  double value = r.get();
  std::cout << "4 - 5 = " << value << std::endl;
}

void mulCallback(const cxxtools::xmlrpc::Result<double>& r)
{
  double value = r.get();
  std::cout << "4 * 5 = " << value << std::endl;
}

void divCallback(const cxxtools::xmlrpc::Result<double>& r)
{
  double value = r.get();
  std::cout << "4 / 5 = " << value << std::endl;
}

We need also a way to break the event loop, when we are done. We do this by defining a global counter for running tasks another callback, which just monitors the left operations and exits the event loop when we are done with all 4 operations.

unsigned runningTasks = 0;

void counterCallback(const cxxtools::xmlrpc::Result<double>& r)
{
  if (--runningTasks == 0)
    loop.exit();
}

In our main function we need to connect these callbacks with the procedures and start processing using beginCall:

    cxxtools::connect(add.finished, addCallback);
    cxxtools::connect(sub.finished, subCallback);
    cxxtools::connect(mul.finished, mulCallback);
    cxxtools::connect(div.finished, divCallback);

    cxxtools::connect(add.finished, counterCallback);
    cxxtools::connect(sub.finished, counterCallback);
    cxxtools::connect(mul.finished, counterCallback);
    cxxtools::connect(div.finished, counterCallback);

    add.begin(4, 5);
    sub.begin(4, 5);
    mul.begin(4, 5);
    div.begin(4, 5);

Then we run the loop and wait, until the program is finished:

    loop.run();

The full example code can be found in xmlrpcAsyncClient.cpp. You may want to add a sleep for one second into the calculation methods in the server and you may notice, that in this asynchronous example all 4 methods will sleep in parallel. The client will return in 1 second instead of 4 as would happen, if you use the non asynchronous client from the first client example.

Copyright © 2008 The Tntnet Development Team
Tntnet 1.6