|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
IntroductionThe officially sanctioned way of making distributed function calls between C++ programs is to use CORBA, but for many applications, this is overkill. The CORBA specifications allow distributed function calls to be made between code written in any number of languages, and to make it all work, specialized tools need to be integrated into the build process, in order to translate object definitions written in CORBA's IDL to whichever native language is being used (C++, Java, etc.). However, if we assume that the server and client are both written in the same language, let us assume C++, since it is possible to do away with these complexities. In particular, instead of elaborate definitions of interfaces and marshalling specifications, we can simply defer to C++. Instead of separate IDL files with object interfaces, we specify the interfaces directly in C++ source code, using the preprocessor, and to marshal arguments across process boundaries, we use the native C++ serialization framework provided in the latest release of the Boost library. A simple exampleAs an example, a simple echo server looks like this: #include <RCF/RCF.hpp> RCF_BEGIN(I_Echo, "I_Echo") RCF_METHOD_R1(std::string, echo, const std::string &); RCF_END(I_Echo); class Echo { public: std::string echo(const std::string &msg) { return msg; } }; int main() { int port = 50001; RCF::RcfServer server(port); server.bind<I_Echo, Echo>(); server.start(); return 0; } And the client: #include <RCF/RCF.hpp> RCF_BEGIN(I_Echo, "I_Echo") RCF_METHOD_R1(std::string, echo, const std::string &); RCF_END(I_Echo); int main() { std::cout << RcfClient<I_Echo>("localhost", 50001).echo("my message"); return 0; } The Boost.Serialization library is used to serialize parameters and return values. It handles standard types and containers automatically, and is easily extended to user defined classes. It also allows us to serialize pointers, with proper handling of polymorphic pointers and multiple pointers to single objects. Basic usageThere are three basic steps to using this framework:
The interface definition macros are used as follows: RCF_BEGIN( type, type_id ) // ... RCF_METHOD_xx( return_type, name, ....): // ... RCF_END( type )
RCF_METHOD_R2(int, func, std::string, std::string); and if the function has a RCF_METHOD_V2(void, func, std::string, std::string); Dispatch IDs for each function are generated automatically; the first member function is numbered 0, the next one 1, and so on. So, the order in which the functions appear in the definition is important, unlike in CORBA, where dispatch IDs are based on the function name. The dispatch IDs are generated using templates and not any preprocessor __LINE__ trickery, so the interface does not change if blank lines are inserted. The maximum number of member functions that can appear between The purpose of the Once we have defined an interface using the {
// create the server and tell it which port to listen on
RCF::RcfServer server(port);
// Interface is the identifer of the interface we're exporting,
// Object is a type that implements that interface
// one object for each client
server.bind<Interface, Object>();
// ... or one object shared by all clients
Object object;
server.bind<Interface>(object);
// tell the server to start listening for connections
server.start();
// ...
// the server will shut down automatically as it goes out of scope
}
The objects are statically bound to the corresponding interface; there is no need for the object to derive from an interface class as is the case for traditional dynamic polymorphism. Instead, the compiler resolves the interface at compile time, which is not only more efficient, but also allows more flexible semantics. The server can handle multiple simultaneous clients, even in single threaded mode, and can be stopped at any time. The lifetime of objects exposed by the server is determined by the number of current connections to the given object; once there are no more live connections to the object, a timeout is set, and when it expires, the object is deleted. To make a client call, we instantiate the corresponding // define the interface RCF_BEGIN(Interface, "Interface") RCF_METHOD_R2(int, add, int, int); RCF_END(Interface); // ... { std::string ip = "localhost"; int port = 50001; RcfClient<Interface> client(ip, port); // connect and call the add function int sum = client.add(1,1); // connection closed as we exit scope } Should any exceptions arise on the server side while invoking the requested object, an exception of type RCF will automatically handle a range of parameter types, including C++ primitive types ( In CORBA, one can tag a parameter as Value: in
Pointer: in
Const reference: in
Nonconst reference: inout
Nonconst reference to pointer: out
To use user-defined types as parameters or return values, some additional serialization code is needed. What that code is depends on which serialization protocols are being used; by default Boost.Serialization is used, and an example of passing a user-defined type would look like the following: struct MyStruct { int a; int b; int c; double d; std::string s; std::map <std::string, std::vector<std::string> > m; template<typename Archive> void serialize(Archive &archive, unsigned int version) { ar & a & b & c & d & s & m; } }; RCF_BEGIN(MyInterface, "MyInterface") RCF_METHOD_R1(MyStruct, myfunc, const MyStruct &); RCF_END(MyInterface); DetailsThe server and client classes use BSD-style sockets to implement the networking, over TCP, and the whole framework has been compiled and tested on Linux, Solaris (x86 and SPARC) and Win32, using Visual C++ 7.1, Codewarrior 9.0, Borland C++ 5.5, and GCC 3.2. Building RCF requires v. 1.32.0 or later of the Boost library, although the only parts of Boost that need to be built are Boost.Serialization, and, for multithreaded builds, Boost.Threads. Multithreaded builds are enabled by defining To use RCF in your own application, you'll need to include the src/RCF.cpp file among the sources of the application, and link to the necessary libraries from Boost, along with OS-specific socket libraries (on Windows that would be ws2_32.lib, on Linux libnsl, etc.). I've included a demo project for Visual Studio .NET 2003, which includes everything needed to compile, link, and run a server/client pair, with the exception of the Boost library, which needs to be downloaded and unzipped, but no building is needed. Performance, as measured in requests/second, is highly dependent on the serialization protocol, and also on the compiler being used. Before turning to Boost.Serialization, I used a serialization framework of my own, with which I could clock around 3000 minimal requests/sec. using Visual C++ 7.1, and 3300 requests/sec. with Codewarrior 9.0, on a loopback connection on a 1400Mhz, 384Mb PC running Windows XP. GCC 3.2, on the other hand, was far slower. Using Boost.Serialization, however, I've been nowhere near these numbers; on average, it's around five times slower. ConclusionRMI is a well known concept in Java circles, what I've done here is to do something similar in C++, without all the complications of CORBA. If you like it, please tell me, if you don't, well, please tell someone else.... Jokes aside, any and all feedback is appreciated, all I ask is that if you grade the article, and do so with a low grade, then please leave an explanatory comment! History
| ||||||||||||||||||||