Remoting NG

Remoting NG Tutorial Part 2: Advanced Remoting

Welcome Back

Welcome to part two of the Remoting tutorial. This part of the Remoting tutorial covers topics such as creating SOAP web services with Remoting, using user-defined argument types in remote methods and remote events.

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, such as presented in part 1 of the Remoting tutorial.

Creating a SOAP Web Service

Lets take a look back at the TimeService remote service that we have created in part one of the tutorial. In part 1 of the tutorial, we have used to TCP Transport. While the TCP Transport is an efficient and fast transport implementation, it can only be used if both client and server have been built with Remoting.

However, Remoting also provides a transport that allows us to provide (or invoke) a SOAP 1.1 (or 1.2) Web Service. This transport is called the SOAP Transport.

If you are new to SOAP, WSDL and Web Services, please familiarize yourself with the basics of these technologies using the many tutorials and articles available on the Web.

Adding The SOAP Transport

The first thing we have to do to build a web service is to add the SOAP transport to both the server and client application. To keep things simple, we simply replace the TCP Transport with the SOAP Transport in our applications. We could as well simply add the SOAP Transport and keep using the TCP Transport as well. Remoting supports multiple simultaneous transports, and the same remote service object can be made available over multiple transports.

Following is the updated server code.

#include "Poco/Util/ServerApplication.h"
#include "Poco/RemotingNG/SOAP/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 SOAP listener.
        std::string listener = Poco::RemotingNG::ORB::instance().registerListener(
            new Poco::RemotingNG::SOAP::Listener("localhost:8080", "")
        );

        // 2. Create the service object.
        Poco::SharedPtr<Sample::TimeService> pTimeService = new Sample::TimeService;

        // 3. Register service object with ORB.
        std::string uri = Sample::TimeServiceServerHelper::registerRemoteObject(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)

All we have done is to replace references to the TCP Transport with corresponding references to the SOAP transport. We have also changed the port number used by the listener from 9999 to 8080.

We now do the same with the client code.

#include "TimeServiceProxy.h"
#include "TimeServiceClientHelper.h"
#include "Poco/RemotingNG/SOAP/TransportFactory.h"
#include <iostream>


int main(int argc, char** argv)
{
    try
    {
        // Register SOAP transport.
        Poco::RemotingNG::SOAP::TransportFactory::registerFactory();

        // Get proxy for remote TimeService.
        std::string uri("http://localhost:8080/soap/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;
}

Both the server and the client are now ready to talk to each other using SOAP over HTTP. However, before we can build and run our applications, an additional step must be performed.

Specifying the XML Namespace For The Web Service

SOAP makes use of XML namespaces, and for the SOAP serializer to be able to serialize and deserialize SOAP messages, we must define a namespace for our web service. This is done by adding the namespace attribute to our service class definition. We simply add the new attribute after the existing remote attribute. The resulting header file is shown below.

#ifndef TimeService_INCLUDED
#define TimeService_INCLUDED


#include <string>


namespace Sample {


//@ remote
//@ namespace = "http://sample.com/webservices/TimeService"
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

After updating the header file of the service class, we have to re-run the code generator for both the server and the client.

Now we are ready to build and run our applications. Please note that, instead of the TCP Transport library, the SOAP Transport library must be linked to both the server and client applications. The client will also need the PocoXML library.

Testing Server And Client

Now that we have completed both the server and the client, it's time to test our new web service.

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\Remoting\tutorials\TimeServer>bin\TimeServer
TimeService URI is: http://localhost:8080/soap/TimeService/TheTimeService

We notice that the URI generated by the server has changed to reflect the fact that we are now using the SOAP Transport. 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: 14:22:18

Creating a WSDL Document

Although we now have created a web service, this web service cannot yet be used from clients other than our own client. For other clients, such as .NET or Java applications, to use our web service, we must create a WSDL document.

Creating a WSDL document for our web service is simple, though. By adding a few entries to the configuration file for the Remoting code generator, the code generator can be instructed to generate the WSDL document for us.

Specifically, to enable generation of a WSDL document, we must add a schema element to the configuration file. For every class for which a WSDL document should be generated, a child element having the same name as the class must be added to the schema element. Under this element must be a serviceLocation element, having as content an URI specifying the location of the web service.

The URI can be obtained simply by taking the URI reported by the server when registering the object. For our example, we therefore have to add the following section:

<schema>
    <TimeService>
        <serviceLocation>http://localhost:8080/soap/TimeService/TheTimeService</serviceLocation>
    </TimeService>
</schema>

With the schema element added, the new code generator configuration now looks like this:

<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>
        <schema>
            <TimeService>
                <serviceLocation>http://localhost:8080/soap/TimeService/TheTimeService</serviceLocation>
            </TimeService>
        </schema>
    </RemoteGen>
</AppConfig>

After re-running the code generator (this time for the server only) we can find a WSDL document (TimeService.wsdl) in the TimeServer project directory.

To instruct the code generator to place the generated WSDL document in another location, a schema element can be added to the output element.

Serving a WSDL Document

The HTTP server created by the SOAP listener can serve WSDL files to clients. For this to work, all WSDL files of the server must either be located in the working directory of the server, or the directory where the files are located must be specified when creating the SOAP listener object (as the second argument to the Poco::RemotingNG::SOAP::Listener constructor). If an empty string is passed, as in our example, the WSDL file is expected to be in the current working directory.

Once the WSDL file is available in a location accessible to the SOAP listener, with the server application running, we can point a web browser to http://localhost:8080/soap/TimeService and look at the WSDL document in the web browser.

Instead of the web browser, you can of course also direct the WSDL parser/code generator from another SOAP implementation to the WSDL document and have it generate client code for the platform of your choice.

Using Multiple Transports

We have now seen how a remote service can be implemented using the TCP Transport and the SOAP Transport. It is also possible to use both transports simultaneously in an application. This is straightforward — simply register both transport factories, and set-up and register two (or even more) listeners on the server. The remote object must also be registered with both listeners on the server.

Following is the modified server code that sets up both a TCP Transport and a SOAP Transport listener, and makes the TimeService remote service available over both transports.

#include "Poco/Util/ServerApplication.h"
#include "Poco/RemotingNG/SOAP/Listener.h"
#include "Poco/RemotingNG/TCP/Listener.h"
#include "Poco/Remoting/ORB.h"
#include "TimeService.h"
#include "TimeServiceServerHelper.h"
#include <iostream>


class TimeServer: public Poco::Util::ServerApplication
{
    int main(const std::vector<std::string>& args)
    {
        // Register listeners for TCP and SOAP Transport.
        std::string tcpListener  = Poco::RemotingNG::ORB::instance().registerListener(
        	new Poco::RemotingNG::TCP::Listener("localhost:9999")
        );
        std::string soapListener = Poco::RemotingNG::ORB::instance().registerListener(
        	new Poco::RemotingNG::SOAP::Listener("localhost:8080", "")
        );

        // Create and register service object with both listeners.
        Poco::SharedPtr<Sample::TimeService> pTimeService = new Sample::TimeService;

        std::string tcpURI = Sample::TimeServiceServerHelper::registerObject(pTimeService, "TheTimeService", tcpListener);
        std::cout << "TimeService URI for TCP Transport is: " << tcpURI << std::endl;

        std::string soapURI = Sample::TimeServiceServerHelper::registerObject(pTimeService, "TheTimeService", soapListener);
        std::cout << "TimeService URI for SOAP Transport is: " << soapURI << std::endl;

        // Wait for CTRL-C or kill.
        waitForTerminationRequest();

        // Stop the Remoting machinery.
        Poco::Remoting::ORB::instance().shutdown();

        return Application::EXIT_OK;
    }
};


POCO_SERVER_MAIN(TimeServer)

Using User-Defined Classes as Parameters

Besides the built-in C++ types (with the exception of pointers), including enumerations (with some restrictions), std::string, std::vector, std::array, std::set and std::multiset, as well as Poco::DateTime, Poco::LocalDateTime, Poco::Timestamp, Poco::Timespan, Poco::Nullable, Poco::Optional, Poco::Array, Poco::AutoPtr and Poco::SharedPtr. Remoting also supports user-defined classes and structures as parameter types.

For this to work, a serializer and a deserializer class must be generated for each class or structure used as parameter or return value type. Serializers and deserializers can be generated automatically by the Remoting code generator. For this to work, the respective class or struct must be annotated with the serialize attribute.

To demonstrate this, we extend the TimeService class with a new method, currentTimeAsStruct(), that returns the current time in a struct.

Following is the updated header file for the TimeService class.

#ifndef TimeService_INCLUDED
#define TimeService_INCLUDED


#include <string>


namespace Sample {


//@ serialize
struct Time
{
    int hour;
    int minute;
    int second;
};


//@ remote
//@ namespace = "http://sample.com/webservices/TimeService"
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).

    Time currentTimeAsStruct() const;
        /// Returns the current time
        /// in a Time structure.
};


} // namespace Sample


#endif // TimeService_INCLUDED

The actual implementation of the currentTimeAsStruct() member function is left as an exercise to the reader.

Now that we have updated the header file, we have to re-run the code generator. This time, the code generator will notice that there is a Time structure annotated as serialize, and it will generate a serializer and a deserializer class for it. Serializers and deserializers are implemented as template specializations of the Poco::RemotingNG::TypeSerializer (or Poco::RemotingNG::TypeDeserializer, respectively) class template, and thus header file only.

Two new files will be generated in the include directory of the TimeServer project: TimeSerializer.h contains the TypeSerializer template specialization for the Time structure, and TimeDeserializer.h contains the TypeDeserializer template specialization for the Time structure.

To use the new member function on the client as well, we also must re-run the code generator for the client code. The serializer and deserializer classes will be generated for the client as well.

We can now change the client to use the new method.

#include "TimeServiceClientHelper.h"
#include "Poco/RemotingNG/SOAP/TransportFactory.h"
#include <iostream>
#include <iomanip>


int main(int argc, char** argv)
{
    try
    {
        // Register SOAP Transport.
        Poco::RemotingNG::SOAP::TransportFactory::registerFactory();

        // Get proxy for remote TimeService.
        std::string uri("http://localhost:8080/soap/TimeService/TheTimeService");
        Sample::ITimeService::Ptr pTimeService = Sample::TimeServiceClientHelper::find(uri);

        // invoke methods on remote object
        Sample::Time currentTime = pTimeService->currentTimeAsStruct();
        std::cout << "Current time: " << std::setw(2) << std::setfill('0')
            << currentTime.hour << ":" << currentTime.minute << std::endl;
    }
    catch (Poco::Exception& exc)
    {
        std::cerr << exc.displayText() << std::endl;
        return 1;
    }
    return 0;
}

Classes with non-public data members can be used as parameter or return value types as well. For this to work, such a class must follow a few conventions:

  • The class must support full value semantics (copy constructor and assignment operator), and it must have a public default constructor.
  • For every non-public data member that should be serialized, there must be both a public setter and a getter member function.
  • The name of the setter and getter functions must be derived from the name of the member variable according to certain rules.

Rules for member variable and setter/getter function names are as follows. For a member variable named var, _var, var_ or m_var, the getter function signature must be:

<type> getVar() const;

or (if the member variable name is not var):

<type> var() const;

The setter function signature must be:

void setVar(<type> value);

or (if the member variable name is not var):

void var(<type> value);

The type of the getter function return value and the setter function parameter can also be a const reference of the respective type.

Classes used as parameter or return value types should be defined and implemented in their own header and implementation files, and these files must be available to the client as well.

Implementing Server-to-Client Callbacks And Remote Events

For many applications, the ability to simple have a client invoke remote objects on a server is not enough. The ability to have the server asynchronously notify the client of certain events may also be needed. With Remoting, this feature can implemented in three ways: polling the server, implementing server-to-client callbacks, and using remote events.

Polling The Server

Polling the server does not require any new programming technique. All that needs to be done is to periodically invoke a remote method on the server, which, as result, then sends back any new data that the server wants to send to the client. The advantages of the polling model are:

  • polling is very easy to implement,
  • there aren't any issues with firewalls — as soon as the client is able to reach the server, it will also receive its notifications, because it is always the client that initiates the connection, and
  • when interoperability with other SOAP implementations is needed, polling might be the only way to implement server notifications.

Polling has two serious disadvantages, though:

  • polling significantly increases the server and network load, especially with many clients and short polling intervals, and
  • the client needs additionaly logic to periodically poll the server.

Server-to-Client Callbacks

An way to implement callbacks from the server to the client is to make the client a Remoting server as well, and have the server simply invoke a remote method on the client (thus, temporarily switching the roles of client and server). The steps to implement this are as follows:

  1. The client registers a Listener, which will accept incoming connections from the server.
  2. The client registers a service object providing the callback method the server will invoke.
  3. The client tells the server the Remoting URI of its callback service object. The server should provide a remote method for that purpose.
  4. When the server needs to invoke the client's callback method, it obtains the callback object's interface (or proxy) from its ORB, using the URI supplied by the client, and simply invokes the callback method on the interface object.

The advantages of this model are:

  • it is relatively easy to implement, and
  • it does not unnecessarily increase network and server load — messages are only sent over the network if the server needs to notify the client.

This model has a significant drawback, though: it may not work across firewalls, as the server needs to open a network connection to the client. If the client is located behind a NAT router or firewall, the server won't be able to reach the client, unless additonal steps (basically, punching a hole through the firewall) are taken.

Remote Events

Remote events are the easiest way to implement server-to-client callbacks. They are based on the Poco::BasicEvent event mechanism. Similarly to how Remoting makes the member functions of a class available remotely, it can also make Poco::BasicEvent members available remotely. However, not all transports support this feature. Currently, only the TCP transport supports remote events. The SOAP transport does not.

Implementing Remote Events On The Server

We can extend our TimeServer sample to support an event-based wake up service, by adding a public Poco::BasicEvent member variable (named wakeUp), as shown below.

#ifndef TimeService_INCLUDED
#define TimeService_INCLUDED


#include "Poco/Foundation.h"
#include "Poco/BasicEvent.h"
#include "Poco/DateTime.h"
#include "Poco/Util/Timer.h"


namespace Sample {


//@ remote
class TimeService
{
public:
    TimeService();
        /// Creates the TimeService.

    ~TimeService();
        /// Destroys the TimeService.

    Poco::BasicEvent<const std::string> wakeUp;

    Poco::DateTime currentTime() const;
        /// Returns the current date and time.

    void wakeMeUp(const Poco::DateTime& time, const std::string& message);
        /// Schedules a wakeup call.
        ///
        /// Fires the wakeUp event at the given time, passing
        /// the given message as argument.

private:
    Poco::Util::Timer _timer;
};


} // namespace Sample


#endif // TimeService_INCLUDED

We've also added a Poco::Util::Timer member variable, which we'll use to schedule the callback events.

When we run RemoteGenNG on the header file to generate the server code, it will generate a new class, TimeServiceEventDispatcher.

The generated TimeServiceEventDispatcher class must be added to our server project. To implement the event in our TimeService class we use the Poco::Util::Timer task, to schedule firing the event at the specified time. For that, we'll first need a subclass of Poco::Util::TimerTask that fires the event:

class WakeUpTask: public Poco::Util::TimerTask
{
public:
    WakeUpTask(TimeService& owner, const std::string& message):
        _owner(owner),
        _message(message)
    {
    }

    void run()
    {
        _owner.wakeUp(this, _message);
    }

private:
    TimeService& _owner;
    std::string _message;
};

The wakeMeUp() member function simply creates an instance of WakeUpTask and passes it to the timer.

void TimeService::wakeMeUp(const Poco::DateTime& time, const std::string& message)
{
    _timer.schedule(new WakeUpTask(*this, message), time.timestamp());
}

One final step needs to be done in order to enable events on our server. We must tell the Remoting NG framework that we want it to handle remote events for our TimeService object. Also, the Remoting NG framework needs a Transport to actually deliver events to clients. Therefore, similarly to client code, we must register the TransportFactory for the TCP Transport which will be used for event delivery.

// register TCP transport (for events)
Poco::RemotingNG::TCP::TransportFactory::registerFactory();

To enable events for our service object, we use the client helper class. Events for a service object can be enabled after the service object has been registered. The protocol name of the transport ("tcp" in our case) to use for events must be given as argument, along with the object's URI.

// register TimeService service object and enable events
std::string uri = Sample::TimeServiceServerHelper::registerObject(new Sample::TimeService, "TheClock", listener);
Sample::TimeServiceServerHelper::enableEvents(uri, "tcp");

Now the server is ready to dispatch remote events to clients.

Receiving Remote Events On The Client

In order to receive remote events on the client, the RemoteGenNG code generator must generate the necessary client support code. Specifically an EventSubscriber class must be generated. To do that, the code generator must know the Poco::RemotingNG::EventSubscriber class, so its header file must be included in the configuration file. We already added this file when we modified the configuration file for the server. After running the code generator, the generated TimeServiceEventSubscriber class must be added to the project, along with the other generated files. When we look at the generated interface class, we'll see that it now has a Poco::BasicEvent member variable that matches the server's wakeUp event, as well as a new method remoting__enableEvents(). In order to receive events, we must first register a delegate with the proxy's event. Furthermore, we must tell the proxy how to receive events. For receiving events, the client must set up an EventListener object. The EventListener is similar to the Listener object used by the server to listen for incoming requests. In fact, the Poco::RemotingNG::EventListener class is a subclass of Poco::RemotingNG::Listener. One additional task the EventListener is responsible for is sending a transport-specific subscription request to the server, so that the server knows whom to send its events. Our EventListener object must be passed to the local proxy, so that the proxy will receive remote events, which will cause the proxy's corresponding events members (to which we've subscribed our delegates) to be fired. This is done by calling remoting__enableEvents().

Putting it all together, the client's setup code looks as follows:

// register transport
Poco::RemotingNG::TCP::TransportFactory::registerFactory();

// create event listener
Poco::RemotingNG::TCP::Listener::Ptr pListener = new Poco::RemotingNG::TCP::Listener;

// get proxy for remote object
std::string uri("remoting.tcp://localhost:7777/tcp/TimeService/TheClock");
Services::ITimeService::Ptr pClock = Services::TimeServiceClientHelper::find(uri);

// invoke method on remote object
Poco::DateTime dt = pClock->currentTime();
std::cout << Poco::DateTimeFormatter::format(dt, Poco::DateTimeFormat::SORTABLE_FORMAT) << std::endl;

// enable events
pClock->remoting__enableEvents(pListener);
pClock->wakeUp += Poco::delegate(onWakeUp);

First we register the Transport using the TransportFactory, as usual. Then we create an EventListener instance. For the TCP transport, the EventListener uses the TCP Transport's connection manager to automatically attach itself to the right connection for a specific proxy, so no arguments need to be passed to its constructor. After obtaining the proxy object, we first enable remote events for that specific proxy by calling remoting__enableEvents(), passing our EventListener as argument. Then we register a delegate function with the proxy's event. Now we are ready to receive remote events. Our delegate function is shown below:

void onWakeUp(const std::string& message)
{
    std::cout << "WakeUp: " << message << std::endl;
}

When we no longer wish to receive events, we remove our delegate from the proxy's event and disable delivery of remote events:

pClock->wakeUp -= Poco::delegate(onWakeUp);
pClock->remoting__enableEvents(pListener, false);

The complete sample code for server and client can be found in the TimeServerTCP and TimeClientTCP sample applications.

This concludes part two of the Remoting NG tutorial. You should now have a basic understanding of all important Remoting NG capabilities and programming techniques. For a complete description of all Remoting capabilities and options, please refer to the Remoting NG User Guide.

Securely control IoT edge devices from anywhere   Connect a Device