Welcome
Welcome to the Remoting tutorial. This tutorial will familiarize you with the basic concepts and techniques required for implementing remote services with Remoting. Part one of the tutorial covers the Remoting basics. In part two, we will take a look at advanced Remoting programming concepts.
This tuturial assumes that you are familiar with basic POCO C++ Libraries programming techniques. You should also have read the Remoting Overview and be familiar with basic Remoting concepts.
Writing a Service Class
The first step in creating a remote service is designing and implementing the service class — the class that implements the actual remote service. For this example, we will implement a simple service that just reports the current time. To keep things simple in the beginning, the time will be returned as a string, in the format HH:MM:SS (hour, minute, second). Later on we will see how we can return the time in other formats, using other data types (even user defined ones) as well.
So, let's start with the class definition:
class TimeService { public: TimeService(); ~TimeService(); std::string currentTimeAsString() const; };
So far, our class looks like any other ordinary C++ class. To turn the class into a remote service, we have to add the remote attribute, either to the class, or to the currentTimeAsString() member function. The complete header file (TimeService.h) for the class is shown below. The remote attribute has been added at class level, which means that all public member functions of the class will be available remotely.
#ifndef TimeService_INCLUDED #define TimeService_INCLUDED #include <string> namespace Sample { //@ remote class TimeService { public: TimeService(); /// Creates the TimeService. ~TimeService(); /// Destroys the TimeService. std::string currentTimeAsString() const; /// Returns the current time, formatted /// as a string (HH:MM::SS). }; } // namespace Sample #endif // TimeService_INCLUDED
The actual implementation of the class is straightforward. We simply use the Poco::DateTime class to obtain the current time (and date), as well as the Poco::DateTimeFormatter to format the time into a string.
#include "TimeService.h" #include "Poco/DateTime.h" #include "Poco/DateTimeFormatter.h" namespace Sample { TimeService::TimeService() { } TimeService::~TimeService() { } std::string TimeService::currentTimeAsString() const { Poco::DateTime now; return Poco::DateTimeFormatter::format(now, "%H:%M:%S); } } // namespace Sample
Generating The Server Code
After writing the service class, the next step is running the RemotingNG code generator (RemoteGenNG) on the service class. To run the code generator, a configuration file for the code generator must be crafted first. The configuration file tells the code generator which classes to generate code for, as well as what code should be generated. The code generator needs to invoke the C++ compiler's preprocessor before parsing each header file, so the configuration file also contains information on how to invoke the preprocessor. The configuration file uses the XML format.
Configuring The Code Generator
The following configuration file configures the code generator to create the server code for our TimeService remote service. It uses the preprocessor from the C++ compiler.
<AppConfig> <RemoteGen> <files> <include> include/TimeService.h </include> </files> <output> <mode>server</mode> <include>include</include> <src>src</src> <namespace>Sample</namespace> <copyright>Copyright (c) 2020</copyright> </output> </RemoteGen> </AppConfig>
Following is a brief discussion of the XML elements present in the configuration file.
AppConfig
The name of the root element is actually not relevant. We have chosen AppConfig, but any other valid element name would do as well.
RemoteGen
This element is required. Its child elements contain the configuration data for the code generator.
files/include
The content of this element is a list of paths (or Glob expressions, exactly) that tell the code generator which header files to parse. The paths specified here can be relative (as in the example) or absolute. Paths must be separated by either a newline, a comma or a semicolon. We have used the configuration variable ${POCO_BASE} to specify the path to the POCO base directory. For this to work, a value for that variable must be passed to the code generator when invoking it.
There are three header files that always must be specified here. These are Poco/RemotingNG/RemoteObject.h, Poco/RemotingNG/Proxy.h and Poco/RemotingNG/Skeleton.h. Failing to include these files will result in the code generator reporting an error. If events are used, the header files Poco/RemotingNG/EventDispatcher.h and Poco/RemotingNG/EventSubscriber.h must be included as well.
output/mode
This specifies what code the generator should generate. For server code, we specify "server". Other valid values would be "client", "both" (generates both client and server code) and "interface" (generates only the interface class).
output/include
This specifies the directory where generated header files are written to.
output/src
This specifies the directory where generated implementation files are written to.
output/namespace
This specifies the C++ namespace, where generated classes will be in.
output/copyright
This specifies a copyright (or other) message that will be added to the header comment section of each generated file.
Invoking The Code Generator
Assuming that both the RemoteGen executable and the C++ compiler are in your executable search path (%PATH% or $PATH), and that the configuration file is named TimeServer.xml, you can start the code generator from a Windows shell with:
RemoteGenNG TimeServer.xml /D:POCO_BASE=c:\poco
From a Unix shell, use:
RemoteGenNG TimeServer.xml -DPOCO_BASE=/path/to/poco
For this to work, we assume the following directory layout for our source and header files:
TimeServer/ include/ TimeService.h src/ TimeService.cpp TimeServer.xml
The current working directory when starting the code generator should be the TimeServer directory.
The code generator takes a few seconds to run the preprocessor, parse the preprocessed header files and generate the code. After the code generator has finished its work, the following source and header files can be found in our project directory:
TimeServer/ include/ TimeService.h ITimeService.h TimeServiceRemoteObject.h TimeServiceSkeleton.h TimeServiceServerHelper.h src/ TimeService.cpp ITimeService.cpp TimeServiceRemoteObject.cpp TimeServiceSkeleton.cpp TimeServiceServerHelper.cpp TimeServer.xml
As can be seen from the file names, the code generator has generated the following classes:
- the interface class (ITimeService),
- the remote object class (TimeServiceRemoteObject),
- the server skeleton class (TimeServiceSkeleton),
- and the server helper class (TimeServiceServerHelper).
The most important class for us on the server side is the server helper class, as this class will do most of the work required for making our service object available remotely.
Writing The Server Application
Now that all the necessary remoting code has been generated, the last step in creating a server application is to write the code that sets up the server.
For this example, we are using the Poco::Util::ServerApplication class from the POCO Util library as a base class for our application.
Setting up the Remoting machinery involves the following steps:
- Setting up and registering with the ORB the Listener objects, which are listening for incoming client connections.
- Creating the service object, and
- Registering our service object with the ORB, using the server helper class.
The code for the server application is shown below.
#include "Poco/Util/ServerApplication.h" #include "Poco/RemotingNG/TCP/Listener.h" #include "Poco/RemotingNG/ORB.h" #include "TimeService.h" #include "TimeServiceServerHelper.h" #include <iostream> class TimeServer: public Poco::Util::ServerApplication { int main(const std::vector<std::string>& args) { // 1. Create and register TCP listener. std::string listener = Poco::RemotingNG::ORB::instance().registerListener( new Poco::RemotingNG::TCP::Listener("localhost:9999") ); // 2. Create the service object. Poco::SharedPtr<Sample::TimeService> pTimeService = new Sample::TimeService; // 3. Register service object with ORB. std::string uri = Sample::TimeServiceServerHelper::registerObject(pTimeService, "TheTimeService", listener); std::cout << "TimeService URI is: " << uri << std::endl; // Wait for CTRL-C or kill. waitForTerminationRequest(); // Stop the remoting machinery. Poco::RemotingNG::ORB::instance().shutdown(); return Application::EXIT_OK; } }; POCO_SERVER_MAIN(TimeServer)
The first statement creates and registers a listener for the TCP Transport.
std::string listener = Poco::RemotingNG::ORB::instance().registerListener( new Poco::RemotingNG::TCP::Listener("localhost:9999") );
The Listener for the TCP transport will set up a TCP server on the given port number (9999), which waits for and accepts incoming client connections. The registerListener() method returns a listener ID string, which will be needed when the service object is registered.
The next lines create our service object, assigning it to a Poco::SharedPtr.
Poco::SharedPtr<Sample::TimeService> pTimeService = new Sample::TimeService;
Use of a shared pointer is mandatory for service objects. The shared pointer is then passed to the static registerObject() function of the server helper class, along with a few more arguments.
std::string uri = Sample::TimeServiceServerHelper::registerObject( pTimeService, "TheTimeService", listener);
The second argument specifies the name under which the service object is known on the server. The name of the service class, together with this name, forms a unique identifier for the service object on the server. The third argument specifies the ID of the listener, which should handle requests for this object. The listener ID has been obtained previously in the registerListener() call.
The registerObject() method returns a string containing the URI of our server object. This URI can be used by clients to access the service object on the server.
After registering our service object, the server is ready to accept connections. The listener runs and processes incoming requests in its own thread, so all that remains to do in the main function is to wait until someone tells the application to shut down. We do this with a call to Poco::Util::ServerApplication::waitForTerminationRequest().
When shutting down, we also explicitely shut down the ORB. This will take care of shutting down the listener, as well as unregistering and destroying the service object.
For building the server application, ensure that all the source files generated by the code generator are included in the build process. Furthermore, the application must be linked with the following libraries:
- PocoFoundation
- PocoXML (for PocoUtil)
- PocoUtil (for the Poco::Util::ServerApplication class)
- PocoNet (network classes required by the TCP transport)
- PocoRemotingNG (the RemotingNG core framework)
- PocoRemotingNGTCP (the TCP Transport implementation)
This is all there is to do to implement a Remoting server application. Next, we will look at the client.
Generating The Client Code
Before we start writing the actual client application, we must again invoke the Remoting code generator to generate the client-side Remoting code for our TimeService service class.
Configuring The Code Generator
The following configuration file configures the code generator to create the client code for our TimeService remote service.
<AppConfig> <RemoteGen> <files> <include> ../TimeServer/include/TimeService.h </include> </files> <output> <mode>client</mode> <include>include</include> <src>src</src> <namespace>Sample</namespace> <copyright>Copyright (c) 2020</copyright> </output> </RemoteGen> </AppConfig>
The configuration file for the client is remarkably similar to the one for the server. In fact, the only significant difference is the value of the mode element, which is set to "client" instead of "server". We also specify a different path to the TimeService.h header file, but this is only so that the code generator picks up the header file from the server project directory. Again, this configuration file is for Visual C++, but it can be easily converted for use with GCC, by simply changing the compiler element or adding additional compiler elements.
It is even possible to use the same configuration file for both the server and the client. The only thing that needs to be done is to change the mode setting accordingly. This is straightforward, as the RemoteGenNG application supports a command-line argument to override this setting.
Invoking The Code Generator
Assuming that our (currently empty) client project directory lies beneath the server project directory, as shown below, we can now invoke the code generator.
TimeServer/ include/ TimeService.h ... src/ TimeService.cpp ... TimeServer.xml TimeClient/ include/ src/ TimeClient.xml
Start the code generator from a shell with:
RemoteGen TimeClient.xml /D:POCO_BASE=c:\poco
If the code generator config file is shared between the client and server, we can also invoke the code generator with:
RemoteGen /mode=client ..\TimeServer\TimeServer.xml /D:POCO_BASE=c:\poco
from a Windows shell, or:
RemoteGen --mode=client ../TimeServer/TimeServer.xml -DPOCO_BASE=/path/to/poco
from a Unix shell, respectively.
Like with the server code, the code generator takes a few seconds to run the preprocessor, parse the preprocessed header files and generate the client code. After the code generator has finished its work, the following source and header files can be found in our project directory:
TimeClient/ include/ ITimeService.h TimeServiceProxy.h TimeServiceProxyFactory.h TimeServiceClientHelper.h src/ ITimeService.cpp TimeServiceProxy.cpp TimeServiceProxyFactory.cpp TimeServiceClientHelper.cpp TimeClient.xml
As can be seen from the file names, the code generator has generated the following classes:
- the interface class (ITimeService),
- the proxy class (TimeServiceProxy),
- the proxy factory class (TimeServiceProxyFactory),
- and the client helper class (TimeServiceClientHelper).
The two most important classes on the client side are the interface class and the client helper class. The interface class is the class we work with whenever we want to invoke a method on the remote object. The client helper class helps us setting up the Remoting machinery, by registering the proxy factory class with the local ORB.
If you look at the header file for the interface class, you will notice that this file includes the header file for the service class. Therefore, the header file for the service class must be available to clients as well. The header file of the service class is required because it might contain type or class definitions needed by the interface class. It is not necessary to make the implementation of the service class available to clients, though (or even link the client with the code for the service class).
Writing The Client Application
Writing the client application is even simpler than writing the server application. All one has to do is to register the transport factory for the given Remoting Transport we are going to use (the TCP Transport in this case), and to register the proxy factory with the ORB, which is conveniently done by the client helper class. After this has been done, an interface object can be obtained from the ORB, again with the help from the client helper class.
Following is the source code for the client application.
#include "TimeServiceProxy.h" #include "TimeServiceClientHelper.h" #include "Poco/RemotingNG/TCP/TransportFactory.h" #include <iostream> int main(int argc, char** argv) { try { // Register TCP transport. Poco::RemotingNG::TCP::TransportFactory::registerFactory(); // Get proxy for remote TimeService. std::string uri("remoting.tcp://localhost:9999/tcp/TimeService/TheTimeService"); Sample::ITimeService::Ptr pTimeService = Sample::TimeServiceClientHelper::find(uri); // Invoke methods on remote object. std::string currentTime = pTimeService->currentTimeAsString(); std::cout << "Current time: " << currentTime << std::endl; } catch (Poco::Exception& exc) { std::cerr << exc.displayText() << std::endl; return 1; } return 0; }
Once we have obtained the interface object from the ORB, we can use it like any ordinary C++ object. The ORB will actually give us an instance of the TimeServiceProxy object (which is a subclass of ITimeService), and the methods of the proxy will transparently forward any method calls to the server.
For building the client application, ensure that all the source files generated by the code generator are included in the build process. Furthermore, the application must be linked with the following libraries:
- PocoFoundation
- PocoNet (network classes required by the TCP transport)
- PocoRemotingNG (the RemotingNG core framework)
- PocoRemotingNGTCP (the TCP Transport implementation)
Testing Server And Client
Now that we have completed both the server and the client, it's time to test our system.
Starting The Server
First, we start the server, by simple starting the server executable. There's no need to pass any command line arguments. Once started, the server will output the URI of the remote service class:
C:\poco\RemotingNG\tutorials\TimeServer>bin\TimeServer TimeService URI is: remoting.tcp://localhost:9999/tcp/TimeService/TheTimeService
The server is then ready to accept requests from clients. To stop the server, simply type CTRL-C (or, on Windows, close the console window).
Starting the Client
The client can also be started by simply executing the client executable. No command line arguments must be passed. Once started, the client will connect to the server, obtain the current time, and quit.
C:\poco\RemotingNG\tutorials\TimeClient>bin\TimeClient Current time: 12:30:09
If the client is started with no server running, the client will display an error message and exit.
C:\poco\RemotingNG\tutorials\TimeClient>bin\TimeClient Connection refused: 127.0.0.1:8080
Congratulations, you now have completed your first Remoting project. This concludes part one of the tutorial.