Universal Plug and Play

UPnP Service Discovery Tutorial And User Guide

Contents

Introduction

This document shows how to build applications and devices that announce themselves (and their built-in web server) via UPnP's Simple Service Discovery Protocol (SSDP), and how to discover such applications and devices in the local network.

The SSDP protocol, together with the underlying Multicast HTTP protocol, is implemented in the UPnP/SSDP library. Using the SSDP library requires some basic knowledge of multicast socket programming with the POCO C++ Libraries.

Making Applications And Devices Discoverable

To make an application or device discoverable via UPnP, the following prerequisites must be met:

  • A UUID must be created for the application or device.
  • The application or device must have a web server to serve (at least) the device description document and the device's web user interface.
  • A multicast socket must be created and configured properly.
  • Device, service and service type advertisements must be published, using the Poco::UPnP::SSDP::SSDPResponder class.

Creating A Device UUID

Every UPnP device (and not just device class) must have its own UUID. The simplest way to create a unique device-specific UUID is to use the configuration framework in the Util library. Using a UUID generation tool of your choice (e.g., the uuidgen sample application from the Foundation library), generate a new UUID. Then, create a new configuration file (property file format) for your application, and in the property file create a setting upnp.device.uuid with the UUID as value.

upnp.device.uuid = 0622707c-da97-4286-cafe-001ff3590148

The last six bytes of the UUID are the hardware address of the first Ethernet adapter in the system the UUID was generated on. To make this UUID unique per device, simply replace the last six bytes of the UUID with the hardware Ethernet address of the system the application actually runs on. With the configuration framework in the Util library this is easy, as there's a predefined configuration property that contains that address. Simply change the above line to:

upnp.device.uuid = 0622707c-da97-4286-cafe-${system.nodeId}

Setting Up The Web Server

Setting up the web server for use with an UPnP application or device does not differ from setting up an ordinary web server with the Poco::Net::HTTPServer class.

The important part is making the web server provide a XML device description document. A very basic device description could look as follows:

<?xml version="1.0"?>    
<root xmlns="urn:schemas-upnp-org:device-1-0">
  <specVersion>
    <major>1</major>
    <minor>0</minor>
  </specVersion>
  <URLBase>http://192.168.1.101:9980</URLBase>
  <device>
    <deviceType>urn:schemas-upnp-org:device:Basic:1</deviceType>
    <friendlyName>HTTPTimeServer</friendlyName>
    <manufacturer>Applied Informatics Software Engineering GmbH</manufacturer>
    <manufacturerURL>http://www.appinf.com/</manufacturerURL>
    <modelDescription>POCO HTTPTimeServer Sample with UPnP SSDP Support</modelDescription>
    <modelName>POCO HTTPTimeServer Sample</modelName>
    <UDN>uuid:0622707c-da97-4286-cafe-001ff3590148</UDN>
    <serviceList>
    </serviceList>
    <presentationURL>/</presentationURL>
  </device>
</root>

Note that some UPnP stacks require that the device description document starts with an XML declaration. Also, there should not be any whitespace or empty lines before the XML declaration. Even though this would be valid XML, some UPnP implementations (including the one in Windows XP) do not accept this.

To serve such a device description document, we have to create a Poco::Net::HTTPRequestHandler subclass that does exactly that.

class DeviceDescriptionRequestHandler: public HTTPRequestHandler
{
    void handleRequest(HTTPServerRequest& request, HTTPServerResponse& response)
    {
        Application& app = Application::instance();

        std::stringstream responseStream;
        responseStream << "<?xml version=\"1.0\"?>\n";
        responseStream << "<root xmlns=\"urn:schemas-upnp-org:device-1-0\">\n";
        responseStream << "  <specVersion>\n";
        responseStream << "    <major>1</major>\n";
        responseStream << "    <minor>0</minor>\n";
        responseStream << "  </specVersion>\n";
        responseStream << "  <URLBase>http://" << request.serverAddress().toString() << "</URLBase>\n";
        responseStream << "  <device>\n";
        responseStream << "    <deviceType>urn:schemas-upnp-org:device:Basic:1</deviceType>\n";
        responseStream << "    <friendlyName>" << app.config().getString("upnp.device.friendlyName") << "</friendlyName>\n";
        responseStream << "    <manufacturer>" << app.config().getString("upnp.device.manufacturer") << "</manufacturer>\n";
        responseStream << "    <manufacturerURL>" << app.config().getString("upnp.device.manufacturerURL") << "</manufacturerURL>\n";
        responseStream << "    <modelDescription>" << app.config().getString("upnp.device.modelDescription") << "</modelDescription>\n";
        responseStream << "    <modelName>" << app.config().getString("upnp.device.modelName") << "</modelName>\n";
        responseStream << "    <UDN>uuid:" << app.config().getString("upnp.device.uuid") << "</UDN>\n";
        responseStream << "    <serviceList>\n";
        responseStream << "    </serviceList>\n";
        responseStream << "    <presentationURL>/</presentationURL>\n";
        responseStream << "  </device>\n";
        responseStream << "</root>\n";

        response.setContentType("text/xml");
        response.setContentLength(static_cast<int>(responseStream.tellp()));
        Poco::StreamCopier::copyStream(responseStream, response.send());
    }
};

The implementation of this class is straightforward, except for a few details. Note that we first write the XML into a stringstream, and not directly to the response stream, using HTTP Chunked Transfer Encoding. Some less-capable UPnP stacks have issues dealing with chunked responses when receiving the device description, so we first write to a stringstream to learn the content length, and then send everything as a fixed-length response with a Content-Length header.

We use the application configuration mechanism to read most values for the device description from the application's configuration file, most notably the device's UUID for the UDN element. Also interesting is the URLBase element. For that, we get our own IP address from the request object (Poco::Net::HTTPServerRequest::serverAddress()).

In order to satisfy the UPnP Certification Test Tool, the request handler should provide a Content-Language response header if the request contains an Accept-Language header. The simplest way to implement this is to add the following code (before the response is sent):

if (request.has("Accept-Language"))
   response.set("Content-Language", "en");

Of course, with some implementation effort, proper internationalization support based on the actual contents of the Accept-Language header could also be added.

One final note: On Windows XP, UPnP devices will only be visible in Explorer if the device description document contains at least one service.

Creating A Multicast Socket

In order to use the Poco::UPnP::SSDP::SSDPResponder class, a multicast socket must be created and properly configured. By their nature, multicast sockets require a bit more setup work than ordinary sockets. Specifically, for a multicast socket:

  • an appropriate network interface must be found,
  • the socket must be bound to a network interface,
  • the socket must join a multicast group, and
  • the TTL (time-to-live) for multicast packets sent from the socket must be set to four (required by the UPnP SSDP specification).

The network interface to use can either be configured using the configuration file, or we can write some code that automatically chooses the interface.

For manual configuration, the following code will work:

std::string mcastInterfaceName = config().getString("upnp.interface");
Poco::Net::NetworkInterface mcastInterface(
    Poco::Net::NetworkInterface::forName(mcastInterfaceName, 
    Poco::Net::NetworkInterface::IPv4_ONLY)); 

To automatically find a suitable network interface, the following code can be used:

Poco::Net::NetworkInterface findActiveNetworkInterface()
{
    Poco::Net::NetworkInterface::NetworkInterfaceList ifs = Poco::Net::NetworkInterface::list();
    for (Poco::Net::NetworkInterface::NetworkInterfaceList::iterator it = ifs.begin(); it != ifs.end(); ++it)
    {
        if (!it->address().isWildcard() && !it->address().isLoopback() && it->supportsIPv4()) return *it;
    }
    throw Poco::IOException("No configured Ethernet interface found");
}

Note that both code samples only work with IPv4 interfaces. To also support IPv6, appropriate changes must be made. Finding the correct interface and IP version in a mixed IPv4/IPv6 environment can be tricky and is best left to manual configuration.

Next is creating the multicast socket. Following code will do:

Poco::Net::SocketAddress ssdpSA(Poco::Net::IPAddress(), Poco::UPnP::SSDP::SSDPResponder::MULTICAST_PORT);
Poco::Net::MulticastSocket ssdpSocket(ssdpSA);
ssdpSocket.setTimeToLive(4);
ssdpSocket.setInterface(mcastInterface);

We create a wildcard socket address with the SSDP port number (1900), and then a Poco::Net::MulticastSocket with that socket address. Then we set the TTL to 4, as required by SSDP, and set the network interface for outgoing multicast packets.

The final task when creating the multicast socket is to join a multicast group. In our case, we'll join the SSDP multicast group, which has the address 239.255.255.250. But before we'll do that, we first create our Poco::UPnP::SSDP::SSDPResponder object, which will do all the work.

SSDPResponder ssdpResponder(ssdpSocket);

Once we've done that, we can use the new object to obtain the address of the SSDP multicast group and join that group.

ssdpSocket.joinGroup(ssdpResponder.groupAddress().host(), mcastInterface);

Publishing Devices And Services

The final step is to publish devices, services and service types so as to make our device discoverable on the network.

Since our first device does not provide any UPnP services, we only need to publish two advertisements for our device. To publish an advertisement, an appropriate instance of Poco::UPnP::SSDP::Advertisement must be created and passed to our Poco::UPnP::SSDP::SSDPResponder instance. We need to publish one Advertisement for the root device, and one for our generic/basic device. Finally, the SSDPResponder must be started so that it will publish the advertisement and respond to incoming search requests.

int adLifetime = config().getInt("upnp.adLifetime", 60);
std::string deviceLocation = "http://" + httpSA.toString() + "/rootdevice.xml";
ssdpResponder.publish(new Poco::UPnP::SSDP::Advertisement(
    Poco::UPnP::SSDP::Advertisement::AD_ROOT_DEVICE, 
    deviceUUID, 
    deviceLocation, 
    adLifetime));
ssdpResponder.publish(new Advertisement(
    Poco::UPnP::SSDP::Advertisement::AD_DEVICE, 
    deviceUUID, 
    deviceLocation, 
    adLifetime));
ssdpResponder.start();

A published service can be revoked with a call to Poco::UPnP::SSDP::SSDPResponder::revoke(). The Unique Service Name (USN) of the service must be given to revoke(). To obtain the USN, we can use the Poco::UPnP::SSDP::Advertisement::uniqueServiceName() method.

Poco::UPnP::SSDP::Advertisement::Ptr pAd = new Advertisement(
    Poco::UPnP::SSDP::Advertisement::AD_DEVICE, 
    deviceUUID, 
    deviceLocation, 
    adLifetime);
ssdpResponder.publish(pAd);
// ...
ssdpResponder.revoke(pAd->uniqueServiceName().toString());

Putting It All Together

Sample Application Source Code

Following is the complete source code for the HTTPTimeServer sample application that announces itself via SSDP. The source code and project files for the application can be found in the UPnP/samples/HTTPTimeServer folder of the source code package.

#include "Poco/Net/HTTPServer.h"
#include "Poco/Net/HTTPRequestHandler.h"
#include "Poco/Net/HTTPRequestHandlerFactory.h"
#include "Poco/Net/HTTPServerParams.h"
#include "Poco/Net/HTTPServerRequest.h"
#include "Poco/Net/HTTPServerResponse.h"
#include "Poco/Net/HTTPServerParams.h"
#include "Poco/Net/ServerSocket.h"
#include "Poco/Net/IPAddress.h"
#include "Poco/Net/NetworkInterface.h"
#include "Poco/Net/MulticastSocket.h"
#include "Poco/Util/ServerApplication.h"
#include "Poco/Util/Option.h"
#include "Poco/Util/OptionSet.h"
#include "Poco/Util/HelpFormatter.h"
#include "Poco/UPnP/SSDP/SSDPResponder.h"
#include "Poco/UPnP/SSDP/Advertisement.h"
#include "Poco/UPnP/URN.h"
#include "Poco/Timestamp.h"
#include "Poco/DateTimeFormatter.h"
#include "Poco/DateTimeFormat.h"
#include "Poco/StreamCopier.h"
#include "Poco/Exception.h"
#include "Poco/ThreadPool.h"
#include <sstream>
#include <iostream>


using Poco::Net::ServerSocket;
using Poco::Net::SocketAddress;
using Poco::Net::HTTPRequestHandler;
using Poco::Net::HTTPRequestHandlerFactory;
using Poco::Net::HTTPServer;
using Poco::Net::HTTPServerRequest;
using Poco::Net::HTTPServerResponse;
using Poco::Net::HTTPServerParams;
using Poco::Timestamp;
using Poco::DateTimeFormatter;
using Poco::DateTimeFormat;
using Poco::ThreadPool;
using Poco::Util::ServerApplication;
using Poco::Util::Application;
using Poco::Util::Option;
using Poco::Util::OptionSet;
using Poco::Util::HelpFormatter;
using Poco::UPnP::SSDP::SSDPResponder;
using Poco::UPnP::SSDP::Advertisement;


class TimeRequestHandler: public HTTPRequestHandler
{
public:
    TimeRequestHandler(const std::string& format): 
        _format(format)
    {
    }

    void handleRequest(HTTPServerRequest& request, HTTPServerResponse& response)
    {
        Timestamp now;
        std::string dt(DateTimeFormatter::format(now, _format));

        response.setChunkedTransferEncoding(true);
        response.setContentType("text/html");

        std::ostream& ostr = response.send();
        ostr << "<html><head><title>HTTPTimeServer powered by POCO C++ Libraries</title>";
        ostr << "<meta http-equiv=\"refresh\" content=\"1\"></head>";
        ostr << "<body><p style=\"text-align: center; font-size: 48px;\">";
        ostr << dt;
        ostr << "</p></body></html>";
    }

private:
    std::string _format;
};


class DeviceDescriptionRequestHandler: public HTTPRequestHandler
{
    void handleRequest(HTTPServerRequest& request, HTTPServerResponse& response)
    {
        Application& app = Application::instance();

        // NOTE: some "less capable" UPnP stacks have problems handling a HTTP/1.1 chunked response,
        // so we first buffer in a string stream and send the result with content-length header.
        std::stringstream responseStream;
        responseStream << "<?xml version=\"1.0\"?>\n";
        responseStream << "<root xmlns=\"urn:schemas-upnp-org:device-1-0\">\n";
        responseStream << "  <specVersion>\n";
        responseStream << "    <major>1</major>\n";
        responseStream << "    <minor>0</minor>\n";
        responseStream << "  </specVersion>\n";
        responseStream << "  <URLBase>http://" << request.serverAddress().toString() << "</URLBase>\n";
        responseStream << "  <device>\n";
        responseStream << "    <deviceType>urn:schemas-upnp-org:device:Basic:1</deviceType>\n";
        responseStream << "    <friendlyName>" << app.config().getString("upnp.device.friendlyName") << "</friendlyName>\n";
        responseStream << "    <manufacturer>" << app.config().getString("upnp.device.manufacturer") << "</manufacturer>\n";
        responseStream << "    <manufacturerURL>" << app.config().getString("upnp.device.manufacturerURL") << "</manufacturerURL>\n";
        responseStream << "    <modelDescription>" << app.config().getString("upnp.device.modelDescription") << "</modelDescription>\n";
        responseStream << "    <modelName>" << app.config().getString("upnp.device.modelName") << "</modelName>\n";
        responseStream << "    <UDN>uuid:" << app.config().getString("upnp.device.uuid") << "</UDN>\n";
        responseStream << "    <serviceList>\n";
        responseStream << "    </serviceList>\n";
        responseStream << "    <presentationURL>/</presentationURL>\n";
        responseStream << "  </device>\n";
        responseStream << "</root>\n";

        response.setContentType("text/xml");
        response.setContentLength(static_cast<int>(responseStream.tellp()));
        Poco::StreamCopier::copyStream(responseStream, response.send());
    }
};


class TimeRequestHandlerFactory: public HTTPRequestHandlerFactory
{
public:
    TimeRequestHandlerFactory(const std::string& format):
        _format(format)
    {
    }

    HTTPRequestHandler* createRequestHandler(const HTTPServerRequest& request)
    {
        Application& app = Application::instance();
        app.logger().information("Request from " + request.clientAddress().toString());

        if (request.getURI() == "/")
            return new TimeRequestHandler(_format);
        else if (request.getURI() == "/rootdevice.xml")
            return new DeviceDescriptionRequestHandler;
        else
            return 0;
    }

private:
    std::string _format;
};


class HTTPTimeServer: public Poco::Util::ServerApplication
{
public:
    HTTPTimeServer(): _helpRequested(false)
    {
    }

    ~HTTPTimeServer()
    {
    }

protected:
    void initialize(Application& self)
    {
        loadConfiguration(); // load default configuration files, if present
        ServerApplication::initialize(self);
    }

    void uninitialize()
    {
        ServerApplication::uninitialize();
    }

    void defineOptions(OptionSet& options)
    {
        ServerApplication::defineOptions(options);

        options.addOption(
            Option("help", "h", "Display help information on command line arguments.")
                .required(false)
                .repeatable(false));
    }

    void handleOption(const std::string& name, const std::string& value)
    {
        ServerApplication::handleOption(name, value);

        if (name == "help")
            _helpRequested = true;
    }

    void displayHelp()
    {
        HelpFormatter helpFormatter(options());
        helpFormatter.setCommand(commandName());
        helpFormatter.setUsage("OPTIONS");
        helpFormatter.setHeader("A web server that serves the current date and time and announces itself via UPnP SSDP.");
        helpFormatter.format(std::cout);
    }

    Poco::Net::NetworkInterface findActiveNetworkInterface()
    {
        Poco::Net::NetworkInterface::NetworkInterfaceList ifs = Poco::Net::NetworkInterface::list();
        for (Poco::Net::NetworkInterface::NetworkInterfaceList::iterator it = ifs.begin(); it != ifs.end(); ++it)
        {
            if (!it->address().isWildcard() && !it->address().isLoopback() && it->supportsIPv4()) return *it;
        }
        throw Poco::IOException("No configured Ethernet interface found");
    }

    int main(const std::vector<std::string>& args)
    {
        if (_helpRequested)
        {
            displayHelp();
        }
        else
        {
            // get parameters from configuration file
            unsigned short httpPort = static_cast<unsigned short>(config().getInt("http.port", 9980));
            std::string format(config().getString("datetime.format", DateTimeFormat::SORTABLE_FORMAT));

            // Find network interface
            std::string mcastInterfaceName = config().getString("upnp.interface", "");
            Poco::Net::NetworkInterface mcastInterface(mcastInterfaceName.empty() ? findActiveNetworkInterface() : Poco::Net::NetworkInterface::forName(mcastInterfaceName, Poco::Net::NetworkInterface::IPv4_ONLY));
            Poco::Net::IPAddress ipAddress(mcastInterface.address());

            logger().information(Poco::format("Using multicast network interface %s (%s).", mcastInterface.name(), ipAddress.toString()));

            Poco::UUID deviceUUID(config().getString("upnp.device.uuid"));
            int adLifetime = config().getInt("upnp.adLifetime", 60);

            Poco::Net::HTTPServerParams::Ptr pHTTPParams = new Poco::Net::HTTPServerParams;
            pHTTPParams->setSoftwareVersion(config().getString("http.softwareVersion", ""));
            pHTTPParams->setMaxQueued(config().getInt("http.maxQueued", 4));
            pHTTPParams->setMaxThreads(config().getInt("http.maxThreads", 4));
            pHTTPParams->setTimeout(Poco::Timespan(config().getInt("http.timeout", 5), 0));
            pHTTPParams->setKeepAlive(config().getBool("http.keepAlive", false));
            pHTTPParams->setMaxKeepAliveRequests(config().getInt("http.maxKeepAliveRequests", 10));
            pHTTPParams->setKeepAliveTimeout(Poco::Timespan(config().getInt("http.keepAliveTimeout", 10), 0));

            ThreadPool::defaultPool().addCapacity(pHTTPParams->getMaxThreads());

            // set-up a server socket
            SocketAddress httpSA(ipAddress, httpPort);
            ServerSocket httpSocket(httpSA);
            // set-up a HTTPServer instance
            HTTPServer httpServer(new TimeRequestHandlerFactory(format), httpSocket, pHTTPParams);
            // start the HTTPServer
            httpServer.start();

            // set up UPnP
            Poco::Net::SocketAddress ssdpSA(Poco::Net::IPAddress(), SSDPResponder::MULTICAST_PORT);
            Poco::Net::MulticastSocket ssdpSocket(ssdpSA);
            ssdpSocket.setTimeToLive(4);
            ssdpSocket.setInterface(mcastInterface);
            SSDPResponder ssdpResponder(ssdpSocket);
            std::string deviceLocation = "http://" + httpSA.toString() + "/rootdevice.xml";
            ssdpResponder.publish(new Advertisement(Advertisement::AD_ROOT_DEVICE, deviceUUID, deviceLocation, adLifetime));
            ssdpResponder.publish(new Advertisement(Advertisement::AD_DEVICE, deviceUUID, deviceLocation, adLifetime));
            ssdpSocket.joinGroup(ssdpResponder.groupAddress().host(), mcastInterface);
            ssdpResponder.start();

            // wait for CTRL-C or kill
            waitForTerminationRequest();

            // shut down UPnP
            ssdpResponder.stop();
            ssdpSocket.leaveGroup(ssdpResponder.groupAddress().host(), mcastInterface);

            // Stop the HTTPServer
            httpServer.stop();
        }
        return Application::EXIT_OK;
    }

private:
    bool _helpRequested;
};


POCO_SERVER_MAIN(HTTPTimeServer)

Application Configuration File

The configuration file for the application looks as follows:

#
# Application Configuration
#

datetime.format = %W, %e %b %y %H:%M:%S %Z


#
# HTTP Server Configuration
#

# The port the HTTP server runs on
http.port = 9980

# The identification of the HTTP server (sent in the Server header)
http.softwareVersion = POCO HTTPTimeServer/1.0

# Maximum number of queued connections
http.maxQueued = 8

# Maximum number of HTTP server threads
http.maxThreads = 4

# Timeout for HTTP connections (seconds)
http.timeout = 5

# Enable/disable persistent HTTP connections
http.keepAlive = false

# Maximum number of requests sent over one persistent connection
http.maxKeepAliveRequests = 10

# Timeout for persistent connections (seconds)
http.keepAliveTimeout = 10


#
# UPnP Configuration
#

upnp.adLifetime = 120    
upnp.device.uuid = 0622707c-da97-4286-cafe-${system.nodeId}
upnp.device.modelName = POCO HTTPTimeServer Sample
upnp.device.modelDescription = POCO HTTPTimeServer Sample with UPnP SSDP Support
upnp.device.friendlyName = HTTPTimeServer
upnp.device.manufacturer = Applied Informatics Software Engineering GmbH
upnp.device.manufacturerURL = http://www.appinf.com/


#
# Logging Configuration
#

logging.loggers.root.channel.class = ConsoleChannel
logging.loggers.app.name = Application
logging.loggers.app.channel = c1
logging.formatters.f1.class = PatternFormatter
logging.formatters.f1.pattern = [%p] %t
logging.channels.c1.class = ConsoleChannel
logging.channels.c1.formatter = f1

Discovering Devices And Services

In addition to publishing devices and services on the network, the Poco::UPnP::SSDP::SSDPResponder class can also be used to discover and search for UPnP devices on the network. Discovering devices and services is straightforward. After setting up a multicast socket and the SSDPResponder object, one only needs to subscribe to the advertisementSeen and advertisementGone events provided by the SSDPResponder to receive notifications about discovered devices. To actively search for devices or services, the Poco::UPnP::SSDP::SSDPResponder::search() method can be used.

Events And Delegates

The following code snippet shows the delegate function for handling the advertisementSeen event. It simply prints out some information about the seen advertisement.

void onAdvertisementSeen(Advertisement::Ptr& pAdvertisement)
{
    std::string type;
    switch (pAdvertisement->type())
    {
    case Advertisement::AD_ROOT_DEVICE:
        type = "ROOT DEVICE";
        break;
    case Advertisement::AD_DEVICE:
        type = "DEVICE";
        break;
    case Advertisement::AD_DEVICE_TYPE:
        type = "DEVICE TYPE";
        break;
    case Advertisement::AD_SERVICE_TYPE:
        type = "SERVICE_TYPE";
        break;
    }
    std::cout << "+ " << type << ": " << pAdvertisement->notificationType().toString() << "\n"
        << "  USN: " << pAdvertisement->uniqueServiceName().toString() << "\n"
        << "  Location: " << pAdvertisement->location() << "\n"
        << "  Lifetime: " << pAdvertisement->lifetime() << "\n" 
        << "  Host: " << pAdvertisement->attributes()["Host"] << "\n"
        << "  Server: " << pAdvertisement->attributes().get("Server", "n/a") << "\n"
        << std::endl;
}

Similarly, the delegate function for handling the advertisementGone event looks as follows:

void onAdvertisementGone(std::string& urn)
{
    std::cout << "- " << urn << "\n" << std::endl;
}

Setting Up The Multicast Socket And SSDPResponder

Setting up a multicast socket and SSDPResponder object is also straightforward:

Poco::Net::SocketAddress sa(Poco::Net::IPAddress(), Poco::UPnP::SSDP::SSDPResponder::MULTICAST_PORT);
Poco::Net::MulticastSocket socket(sa, true);
socket.setLoopback(true);
socket.setTimeToLive(4);
Poco::UPnP::SSDP::SSDPResponder responder(socket);
responder.advertisementSeen += Poco::delegate(onAdvertisementSeen);
responder.advertisementGone += Poco::delegate(onAdvertisementGone);
createAdvertisements(responder);
socket.joinGroup(responder.groupAddress().host());
responder.start();

To actively search for devices, call Poco::UPnP::SSDP::SSDPResponder::search() after starting the responder. A special search target, "ssdp:all", can be used to search for all available services.

responder.search("ssdp:all");

It's also possible to search for a specific device or service type:

responder.search("urn:schemas-upnp-org:device:InternetGatewayDevice:1");

Also, multiple searches can be started simultaneously. Note that each call to search() only sends one search request. A typical control point application may want to call search() periodically, using a Poco::Util::Timer.

A complete sample application can be found in the UPnP/samples/SSDPListener folder in the source code distribution.

Tips And Tricks

Timers

To perform its work, the Poco::UPnP::SSDP::SSDPResponder class needs a Poco::Util::Timer object. Since one timer object can be used to schedule many timer tasks, it makes sense to use a single timer instance for the SSDPResponder, and for other application specific task. Since each timer creates one thread, reducing the timer instances also reduces the number of threads the application uses. A Poco::Util::Timer instance can be passed to the SSDPResponder constructor. The SSDPResponder will use this timer instance instead of creating its own.

SSDP Server Name

When the SSDPResponder sends out advertisements, these advertisements (ssdp:alive and ssdp:byebye messages) contain a Server field that can be used to announce the name and version of the software implementing the SSDP protocol. The content of this field can be set with Poco::UPnP::SSDP::SSDPResponder::setServer().