|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionRCF (Remote Call Framework) is a C++ framework I've written to provide an easy and consistent way of implementing interprocess calling functionality in C++ programs. It is based on the concept of strongly typed client/server interfaces, a concept familiar to users of IDL-based middlewares such as CORBA and DCOM. However, by catering only to C++, RCF can harness C++-specific language features that allow the formulation of interprocess calls in a relatively simple and uncluttered manner. This is the second generation of the RCF library, the first of which was presented in a previous article here on CodeProject. An example is worth a thousand words, to paraphrase an old Chinese saying... This article is framed as a series of examples, covering the salient features of RCF. BasicsWe'll begin with the canonical echo server and client examples that seem to feature in all networking and IPC demonstrations. We want to expose and then call a function that accepts a #include <RCF/Idl.hpp>
#include <RCF/RcfServer.hpp>
#include <RCF/TcpEndpoint.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 &s)
{
return s;
}
};
int main()
{
Echo echo;
RCF::RcfServer server(RCF::TcpEndpoint(50001));
server.bind<I_Echo>(echo);
server.startInThisThread();
return 0;
}
... and the client code becomes: #include <RCF/Idl.hpp>
#include <RCF/TcpEndpoint.hpp>
RCF_BEGIN(I_Echo, "I_Echo")
RCF_METHOD_R1(std::string, echo, const std::string &)
RCF_END(I_Echo)
int main()
{
RcfClient<I_Echo> echoClient(RCF::TcpEndpoint("localhost", 50001));
std::string s = echoClient.echo(RCF::Twoway, "what's up");
return 0;
}
The The client stub is not synchronized in any way, and should only be accessed by a single thread at a time. The server, though, is inherently multi-threaded although in the example above, it has been written as a single-threaded process. In multi-threaded builds, one can also call We can also rewrite our client/server pair to use the UDP protocol instead. This time, we'll let the server and the client both reside in the same process, but in different threads: #include <RCF/Idl.hpp>
#include <RCF/RcfServer.hpp>
#include <RCF/UdpEndpoint.hpp>
#ifndef RCF_USE_BOOST_THREADS
#error Need to build with RCF_USE_BOOST_THREADS
#endif
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 &s)
{
return s;
}
};
int main()
{
Echo echo;
RCF::RcfServer server(RCF::UdpEndpoint(50001));
server.bind<I_Echo>(echo);
server.start();
RcfClient<I_Echo> echoClient(RCF::UdpEndpoint("127.0.0.1", 50001));
std::string s = echoClient.echo(RCF::Twoway, "what's up");
server.stop(); // would happen anyway as server object goes out of scope
return 0;
}
The distinguishing feature of UDP, with respect to TCP, is that UDP is stateless. There is no guarantee of the order in which packets that are sent will arrive, or if they will even arrive at all. On a local connection, like in the example above, using two-way semantics will usually work since the packets are not being subjected to the vagaries of a real network. In general, however, one-way semantics would be needed. InterfacesThe interface definition macros function exactly like they did in the previous generation of RCF. The The last two letters in the The net result of these macros is to define the namespace A
{
namespace B
{
RCF_BEGIN(I_X, "I_X")
RCF_METHOD_V0(void, func1)
RCF_METHOD_R5(int, func2, int, int, int, int, int)
RCF_METHOD_R0(std::auto_ptr<std::string>, func3)
RCF_METHOD_V2(void, func4,
const boost::shared_ptr<std::string> &,
boost::shared_ptr<std::string> &)
// ..
RCF_END(I_X)
}
}
int main()
{
A::B::RcfClient<A::B::I_X> client;
// or
A::B::I_X::RcfClient client;
// ...
}
Server BindingsOn the server side, the interface needs to be bound to a concrete implementation. This is done through the templated {
// bind to an object...
Echo echo;
server.bind<I_Echo>(echo);
// or to a std::auto_ptr<>...
std::auto_ptr<Echo> echoAutoPtr(new Echo());
server.bind<I_Echo>(echoAutoPtr);
// or to a boost::shared_ptr<>...
boost::shared_ptr<Echo> echoPtr(new Echo());
server.bind<I_Echo>(echoPtr);
// or to a boost::weak_ptr<>...
boost::weak_ptr<Echo> echoWeakPtr(echoPtr);
server.bind<I_Echo>(echoWeakPtr);
}
By default, the binding is available to clients through the name of the interface. The server can also expose several objects through the same interface, but in that case it needs to explicitly set the binding names: {
RcfServer server(endpoint);
// bind first object
Echo echo1;
server.bind<I_Echo>(echo1, "Echo1");
// bind second object
Echo echo2;
server.bind<I_Echo>(echo2, "Echo2");
server.start();
RcfClient<I_Echo> echoClient(endpoint);
echoClient.getClientStub().setServerBindingName("Echo1");
std::cout << echoClient.echo("this was echoed by the echo1 object");
echoClient.getClientStub().setServerBindingName("Echo2");
std::cout << echoClient.echo("this was echoed by the echo2 object");
}
MarshalingRCF follows C++ conventions when it comes to determining in which directions arguments are marshaled. In particular, all arguments to an interface method are Not everything in C++ can be safely marshaled, and this places some limitations on the types of arguments to interface methods. To wit: pointers and references are allowed as arguments; references to pointers are not allowed as arguments; pointers and references are not allowed as return values. This means that if an interface method needs to return a pointer -- for instance, a polymorphic pointer -- then the return type needs to be a smart pointer such as SerializationThe echo example only serialized a In general, it is necessary to include serialization code for the classes that are being marshaled. If you have a To use your own classes as arguments in RCF interfaces, you will need to define custom serialization functions. In most cases, it's quite simple: #include <boost/serialization/string.hpp>
#include <boost/serialization/map.hpp>
#include <boost/serialization/vector.hpp>
#include <SF/string.hpp>
#include <SF/map.hpp>
#include <SF/vector.hpp>
struct X
{
int myInteger;
std::string myString;
std::map<
std::string,
std::map<int,std::vector<int> > > myMap;
};
// this serialization function
// will work as is with both SF and B.S.
template<typename Archive>
void serialize(Archive &ar, X &x)
{
ar & x.myInteger & x.myString & x.myMap;
}
// if you need to distinguish between SF and B.S. serialization,
// specialize the SF serialization function:
//void serialize(SF::Archive &ar, X &x)
//{
// ar & myInteger & myString & myMap;
//}
Serialization can quickly become a complex affair, especially when dealing with pointers of polymorphic objects and cycles of pointers. Both class Base
{
// some members here
// ...
};
typedef boost::shared_ptr<Base> BasePtr;
class Derived1 : public Base
{
// some members here
// ...
};
class Derived2 : public Base
{
// some members here
// ...
};
template<typename Archive>
void serialize(Archive &ar, Base &base, const unsigned int)
{
// ...
}
template<typename Archive>
void serialize(Archive &ar, Derived1 &derived1, const unsigned int)
{
// valid for both SF and B.S.
serializeParent<Base>(derived1);
// ...
}
template<typename Archive>
void serialize(Archive &ar, Derived2 &derived2, const unsigned int)
{
// valid for both SF and B.S.
serializeParent<Base>(derived1);
// ...
}
// Boost type registration, needed on both server and client
BOOST_CLASS_EXPORT_GUID(Derived1, "Derived1")
BOOST_CLASS_EXPORT_GUID(Derived2, "Derived2")
RCF_BEGIN(I_PolymorphicArgTest, "")
RCF_METHOD_R1(std::string, typeOf, BasePtr)
RCF_END(I_PolymorphicArgTest)
class PolymorphicArgTest
{
public:
std::string typeOf(BasePtr basePtr)
{
return typeid(*basePtr).name();
}
};
{
// SF type registration, needed on both server and client
SF::registerType<Derived1>("Derived1");
SF::registerType<Derived2>("Derived2");
RcfClient<I_PolymorphicArgTest> client(endpoint);
// SF serialization (default)
client.getClientStub().setSerializationProtocol(RCF::SfBinary);
std::string typeBase = client.typeOf( BasePtr(new Base()) );
std::string typeDerived1 = client.typeOf( BasePtr(new Derived1()) );
std::string typeDerived2 = client.typeOf( BasePtr(new Derived2()) );
// Boost serialization
client.getClientStub().setSerializationProtocol(RCF::BsBinary);
typeDerived2 = client.typeOf( BasePtr(new Derived2()) );
}
InheritanceMultiple inheritance is now supported for RCF interfaces. Interfaces may inherit not only from each other, but also from standard C++ classes. Methods in an interface are identified by the combination of their dispatch ID and the name of the interface that they belong to. This information suffices for the server to map an incoming client call to the correct function on the server binding. The example below demonstrates interface inheritance: RCF_BEGIN(I_A, "I_A")
RCF_METHOD_V0(void, func1)
RCF_END(I_Base)
RCF_BEGIN(I_B, "I_B")
RCF_METHOD_V0(void, func2)
RCF_END(I_Base)
// derives from I_A
RCF_BEGIN_INHERITED(I_C, "I_C", I_A)
RCF_METHOD_V0(void, func3)
RCF_END(I_Base)
// derives from I_A and I_B
RCF_BEGIN_INHERITED_2(I_D, "I_D", I_A, I_B)
RCF_METHOD_V0(void, func4)
RCF_END(I_Base)
class I_E
{
public:
virtual void func5() = 0;
};
// derives from abstract base class I_E
RCF_BEGIN_INHERITED(I_F, "I_F", I_E)
RCF_METHOD_V0(void, func5)
RCF_END(I_Base)
{
RcfClient<I_C> clientC(endpoint);
clientC.func3();
clientC.func1();
RcfClient<I_D> clientD(endpoint);
clientD.func4();
clientD.func2();
clientD.func1();
RcfClient<I_F> clientF(endpoint);
I_E &e = clientF;
e.func5();
}
FiltersRCF supports compression and encryption of messages out of the box, by way of a filter concept. Filters are applied to both server- and client-side, and can be applied either to the transport layer -- for instance, applying an SSL filter to a stream-oriented transport like TCP -- or to individual message payloads such as, for instance, compression of messages destined for a packet-oriented transport like UDP. The first case will be referred to as a transport filter, and the second as a payload filter. Transport FiltersThe process of installing a transport filter on a server-client conversation is initiated by the client. The client queries the server to ascertain if the server supports a given filter. If the server does, the filter is installed on both ends of the transport and the communication resumes. The procedure of querying the server and installing the filter is handled automatically by the client stub. The user only needs to call Transport filters can be two-way, in the sense that a single read or write operation may well result in multiple read and write requests being made in the downstream direction. A typical example of that is an SSL encryption filter that may, at any moment, need to initiate a handshake operation involving multiple downstream read and write requests. Payload FiltersPayload filters do not require any particular steps on behalf of either the server or the client. If a client stub has been imbued with a sequence of payload filters via Payload filters must be one-way, i.e. a read operation can only result in one or more read operations in the downstream direction, and similarly for write operations. RCF comes pre-supplied with several filters: two for compression, based on Zlib, and one for SSL encryption, based on OpenSSL. These filters can also be chained to each other to create sequences of filters. The OpenSSL encryption filter can only be used as a transport filter, since the process of SSL encryption and decryption requires a two-way conversation, as mentioned above. On the other hand, both the stateless and the stateful compression filters can be used either as transport or payload filters. Over non-stream-oriented transports such as UDP, it will only be meaningful to use the stateless compression filter. However, over transports like TCP, either or both can be used. An example: {
// compression of payload
RcfClient<I_X> client(endpoint);
client.setPayloadFilters( RCF::FilterPtr(
new RCF::ZlibStatelessCompressionFilter() ) );
// encryption of transport
std::string certFile = "client.pem";
std::string certFilePwd = "client_password";
client.setTransportFilters( RCF::FilterPtr(
new RCF::OpenSslEncryptionFilter(
RCF::SslClient, certFile, certFilePwd)) ) );
// multiple chained transport filters
// - compression followed by encryption
std::vector<RCF::FilterPtr> transportFilters;
transportFilters.push_back( RCF::FilterPtr(
new RCF::ZlibStatefulCompressionFilter()));
transportFilters.push_back( RCF::FilterPtr(
new RCF::OpenSslEncryptionFilter(
RCF::SslClient, certFile, certFilePwd)) ) );
client.setTransportFilters(transportFilters);
// multiple chained payload filters - double compression
std::vector<RCF::FilterPtr> payloadFilters;
payloadFilters.push_back( RCF::FilterPtr(
new RCF::ZlibStatefulCompressionFilter()));
payloadFilters.push_back( RCF::FilterPtr(
new RCF::ZlibStatefulCompressionFilter()));
client.setPayloadFilters(payloadFilters);
}
{
std::string certFile = "server.pem";
std::string certFilePwd = "server_password";
// FilterService service enables the server to load the filters it needs
RCF::FilterServicePtr filterServicePtr( new RCF::FilterService );
filterServicePtr->addFilterFactory( RCF::FilterFactoryPtr(
new RCF::ZlibStatelessCompressionFilterFactory) );
filterServicePtr->addFilterFactory( RCF::FilterFactoryPtr(
new RCF::ZlibStatefulCompressionFilterFactory) );
filterServicePtr->addFilterFactory( RCF::FilterFactoryPtr(
new RCF::OpenSslEncryptionFilterFactory(certFile, certFilePwd)) );
RCF::RcfServer server(endpoint);
server.addService(filterServicePtr);
server.start();
}
Remote Object CreationThe The {
// allow max 50 objects to be created
unsigned int numberOfTokens = 50;
// delete objects after 60 s, when no clients are connected to them
unsigned int objectTimeoutS = 60;
// create object factory service
RCF::ObjectFactoryServicePtr objectFactoryServicePtr(
new RCF::ObjectFactoryService(numberOfTokens, objectTimeoutS) );
// allow clients to create and access Echo objects, through I_Echo
objectFactoryServicePtr->bind<I_Echo, Echo>();
RCF::RcfServer server(endpoint);
server.addService(objectFactoryServicePtr);
server.start();
}
{
RcfClient<I_Echo> client1(endpoint);
bool ok = RCF::createRemoteObject<I_Echo>(client1);
// client can now be used to access a newly created object on the server.
RcfClient<I_Echo> client2(endpoint);
client2.getClientStub().setToken( client1.getClientStub().getToken() );
// client1 and client2 will now be accessing the same object
}
Publish/SubscribeThe publish/subscribe pattern is a well-known idiom of distributed programming. One process plays the role of a publisher and sends regular packets of information to a list of subscribers. The subscribers call in to the publisher and request subscriptions to the data that the publisher is publishing. For packet-oriented transports such as UDP, it is relatively straightforward to build this functionality on top of existing RCF primitives. For stream-oriented transports, TCP in particular, RCF provides some extra features to enable publish/subscribe functionality. The connection that a subscriber calls in on is reversed and then subsequently used by the publisher for publishing. This functionality is encompassed by the dual RCF_BEGIN(I_Notify, "I_Notify")
RCF_METHOD_V1(void, func1, int)
RCF_METHOD_V2(void, func2, std::string, std::string)
RCF_END(I_Notify)
{
RCF::RcfServer publishingServer(endpoint);
RCF::PublishingServicePtr publishingServicePtr(
new RCF::PublishingService() );
publishingServer.addService(publishingServicePtr);
publishingServer.start();
// start accepting subscription requests for I_Notify
publishingServicePtr->beginPublish<I_Notify>();
// call func1() on all subscribers
publishingServicePtr->publish<I_Notify>().func1(1);
// call func2(std::string, std::string) on all subscribers
publishingServicePtr->publish<I_Notify>().func2("one", "two");
// stop publishing I_Notify and disconnect all subscribers
publishingServicePtr->endPublish<I_Notify>();
publishingServer.stop();
}
{
RCF::RcfServer subscribingServer(endpoint);
RCF::SubscriptionServicePtr subscriptionServicePtr(
new RCF::SubscriptionService() );
subscribingServer.addService( subscriptionServicePtr );
subscribingServer.start();
Notify notify;
subscriptionServicePtr->beginSubscribe<I_Notify>(
notify,
RCF::TcpEndpoint(ip, port));
// notify.func1() and notify.func2()
// will now be remotely invoked by the publisher
// ...
subscriptionServicePtr->endSubscribe<I_Notify>(notify);
}
ExtensibilityTransportsA (well-deserved) criticism levelled at the previous version of RCF concerned its overly close relationship with the TCP protocol. RCF now sports a transport-agnostic design and, for starters, supports both the TCP and UDP protocols. More importantly, the architecture is such that it shouldn't be too difficult for third parties to write and customize their own client and server transports. On the client side, the RcfServer ServicesThe Server transports are implemented as services. It is thus possible to have several transports serving a single
PortabilityCompilersRCF 0.4 has been tested on Visual C++ 7.1, Visual C++ 8.0, gcc 3.x, Borland C++ 5.6, and Metrowerks CodeWarrior 9.2. Additionally, RCF 0.9c supports Visual C++ 6.0 and gcc 2.95. PlatformsRCF's TCP server implementation is now based on Win32 I/O completion ports, a technology limited to Windows 2000 and later. The TCP client implementation and the UDP server/client implementations are based on BSD sockets, and should be widely portable. On non-Windows platforms, and optionally on Windows as well, RCF leverages the asio library to implement TCP servers. BuildingIn general, to build an application using RCF, you need to include the file src/RCF/RCF.cpp among the sources of your application. You will also need the header files of the Boost library; any of the more recent versions should be OK. If you plan on using There are several preprocessor symbols that can be used to control which parts of RCF are compiled. These symbols need to be defined globally, i.e. they should be placed in the project settings and not defined/undefined in source code.
Summing up, all third party build dependencies ( To use the As for OpenSSL, the usual export/import restrictions apply. I won't bore anyone by quoting them here... TestingThere is a comprehensive set of tests in the /test directory of the download, all of which should compile and run without failures. The tests can be built and run automatically using the FeedbackThe current and eventual shape and scope of this library is very dependent on the feedback I get from its users. So, please let the comments fly! All viewpoints are welcome. Either post them here or email them to me at jlindrud -at- hotmail -dot- com. And, of course, a big thanks to all those who weighed in on the previous incarnation of RCF! Your comments made a difference. History
LicensesPlease note: The code for each download is entirely separate. The licenses for the article and code are as follows:
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||