<?xml version="1.0"?>
<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN" "http://www.oasis-open.org/docbook/xml/4.4/docbookx.dtd">
<chapter id="client-tutorial">
<title>Writing a UPnP Client</title>
<simplesect>
<title>Introduction</title>
<para>
This chapter explains how to write an application which fetches the
external IP address from an UPnP-compliant modem. To do this a
<glossterm>Control Point</glossterm> is created, which searches for
services of the type
<literal>urn:schemas-upnp-org:service:WANIPConnection:1</literal> (part of
the <ulink url="http://upnp.org/standardizeddcps/igd.asp">Internet Gateway
Device</ulink> specification). As services are discovered
<firstterm>Service Proxy</firstterm> objects are created by GUPnP to allow
interaction with the service, on which we can invoke the action
<function>GetExternalIPAddress</function> to fetch the external IP
address.
</para>
</simplesect>
<simplesect>
<title>Finding Services</title>
<para>
First, we initialize GUPnP and create a control point targeting the
service type. Then we connect a signal handler so that we are notified
when services we are interested in are found.
</para>
<programlisting>#include <libgupnp/gupnp-control-point.h>
static GMainLoop *main_loop;
static void
service_proxy_available_cb (GUPnPControlPoint *cp,
GUPnPServiceProxy *proxy,
gpointer userdata)
{
/* … */
}
int
main (int argc, char **argv)
{
GUPnPContext *context;
GUPnPControlPoint *cp;
/* Required initialisation */
#if !GLIB_CHECK_VERSION(2,35,0)
g_type_init ();
#endif
/* Create a new GUPnP Context. By here we are using the default GLib main
context, and connecting to the current machine's default IP on an
automatically generated port. */
context = gupnp_context_new (NULL, NULL, 0, NULL);
/* Create a Control Point targeting WAN IP Connection services */
cp = gupnp_control_point_new
(context, "urn:schemas-upnp-org:service:WANIPConnection:1");
/* The service-proxy-available signal is emitted when any services which match
our target are found, so connect to it */
g_signal_connect (cp,
"service-proxy-available",
G_CALLBACK (service_proxy_available_cb),
NULL);
/* Tell the Control Point to start searching */
gssdp_resource_browser_set_active (GSSDP_RESOURCE_BROWSER (cp), TRUE);
/* Enter the main loop. This will start the search and result in callbacks to
service_proxy_available_cb. */
main_loop = g_main_loop_new (NULL, FALSE);
g_main_loop_run (main_loop);
/* Clean up */
g_main_loop_unref (main_loop);
g_object_unref (cp);
g_object_unref (context);
return 0;
}</programlisting>
</simplesect>
<simplesect>
<title>Invoking Actions</title>
<para>
Now we have an application which searches for the service we specified and
calls <function>service_proxy_available_cb</function> for each one it
found. To get the external IP address we need to invoke the
<literal>GetExternalIPAddress</literal> action. This action takes no in
arguments, and has a single out argument called "NewExternalIPAddress".
GUPnP has a set of methods to invoke actions (which will be very familiar
to anyone who has used <literal>dbus-glib</literal>) where you pass a
<constant>NULL</constant>-terminated varargs list of (name, GType, value)
tuples for the in arguments, then a <constant>NULL</constant>-terminated
varargs list of (name, GType, return location) tuples for the out
arguments.
</para>
<programlisting>static void
service_proxy_available_cb (GUPnPControlPoint *cp,
GUPnPServiceProxy *proxy,
gpointer userdata)
{
GError *error = NULL;
char *ip = NULL;
gupnp_service_proxy_send_action (proxy,
/* Action name and error location */
"GetExternalIPAddress", &error,
/* IN args */
NULL,
/* OUT args */
"NewExternalIPAddress",
G_TYPE_STRING, &ip,
NULL);
if (error == NULL) {
g_print ("External IP address is %s\n", ip);
g_free (ip);
} else {
g_printerr ("Error: %s\n", error->message);
g_error_free (error);
}
g_main_loop_quit (main_loop);
}</programlisting>
<para>
Note that gupnp_service_proxy_send_action() blocks until the service has
replied. If you need to make non-blocking calls then use
gupnp_service_proxy_begin_action(), which takes a callback that will be
called from the mainloop when the reply is received.
</para>
</simplesect>
<simplesect>
<title>Subscribing to state variable change notifications</title>
<para>
It is possible to get change notifications for the service state variables
that have attribute <literal>sendEvents="yes"</literal>. We'll demonstrate
this by modifying <function>service_proxy_available_cb</function> and using
gupnp_service_proxy_add_notify() to setup a notification callback:
</para>
<programlisting>static void
external_ip_address_changed (GUPnPServiceProxy *proxy,
const char *variable,
GValue *value,
gpointer userdata)
{
g_print ("External IP address changed: %s\n", g_value_get_string (value));
}
static void
service_proxy_available_cb (GUPnPControlPoint *cp,
GUPnPServiceProxy *proxy,
gpointer userdata)
{
g_print ("Found a WAN IP Connection service\n");
gupnp_service_proxy_set_subscribed (proxy, TRUE);
if (!gupnp_service_proxy_add_notify (proxy,
"ExternalIPAddress",
G_TYPE_STRING,
external_ip_address_changed,
NULL)) {
g_printerr ("Failed to add notify");
}
}</programlisting>
</simplesect>
<simplesect>
<title>Generating Wrappers</title>
<para>
Using gupnp_service_proxy_send_action() and gupnp_service_proxy_add_notify ()
can become tedious, because of the requirement to specify the types and deal
with GValues. An
alternative is to use <xref linkend="gupnp-binding-tool"/>, which
generates wrappers that hide the boilerplate code from you. Using a
wrapper generated with prefix 'ipconn' would replace
gupnp_service_proxy_send_action() with this code:
</para>
<programlisting>ipconn_get_external_ip_address (proxy, &ip, &error);</programlisting>
<para>
State variable change notifications are friendlier with wrappers as well:
</para>
<programlisting>static void
external_ip_address_changed (GUPnPServiceProxy *proxy,
const gchar *external_ip_address,
gpointer userdata)
{
g_print ("External IP address changed: '%s'\n", external_ip_address);
}
static void
service_proxy_available_cb (GUPnPControlPoint *cp,
GUPnPServiceProxy *proxy
gpointer userdata)
{
g_print ("Found a WAN IP Connection service\n");
gupnp_service_proxy_set_subscribed (proxy, TRUE);
if (!ipconn_external_ip_address_add_notify (proxy,
external_ip_address_changed,
NULL)) {
g_printerr ("Failed to add notify");
}
}</programlisting>
</simplesect>
</chapter>