libgupnp是一款开源的upnp软件。它采用gobject思想设计,实现了异步消息传递;用面向过程语言C实现了面向对象的框架。当然,最主要的是,它实现了通用即插即用协议UPNP,方便了很多数字家庭网络的应用开发者。
接下来,我先简单介绍下UPNP协议;接着从libgupnp的类设计结构图着手,以给大家提供框架上的认识;然后,我将分析upnp的设备发现部分在libgupnp中的设计与实现。
一、了解UPNP
UPnP 网络的基本架构分为设备、服务和控制点。
设备:指其他服务或者是设备的容器,一个设备可以包含 其它的逻辑设备。
服务:服务指一个逻辑功能单位,服务代表动作和使用状 态变量的物理设备的部分或所有状态
控制点:也就是一个控制器,它可以检索设备和服务描述,发送动作到服务,查询服务的状态变量和从服务接收事件,允许用户使用或动行一个设备,例如CD播放机的程序可以认为是控制点。
UPnP定义了设备之间、设备和控制点、控制点之间通讯的协议。完整的UPnP由设备寻址、设备发现、设备描述、设备控制、事件通知和基于Html的描述界面几部分构成。UPNP的协议栈如下图所示:
二、GUPNP的对象布局
gupnp的主要类图继承关系,简单表示如下:
1.GObject
+----GSSDPClient
+----GUPnPContext
2.
GObject
+----GSSDPResourceBrowser (它聚合gssdpclient,因此具有网络设备发现功能)
+----GUPnPControlPoint ( 聚合GUPnPResourceFactory)
+----DLNADmp (这个对象是我为了方便说明,实现的一个对象。libgupnp中无此对象)
3.
GObject
+----GUPnPServiceInfo(它聚合gupnpcontext)
+----GUPnPServiceProxy
4.
GObject
+----GUPnPDeviceInfo
+----GUPnPDeviceProxy
5.
GObject
+----GUPnPDeviceInfo
+----GUPnPDevice
+----GUPnPRootDevice
读到这里时,没必要关注太多细节。读者只需要知道这些对象之间存在的继承关系,以及这些对象与UPNP的元素架构及UPNP协议栈元素之间的对应关系即可。接下来,我将具体分析源码。
三、设备发现在LIBGUPNP中的实现
为了方便说明,我将自己添加一个控制点dlnadmp实现。该DLNADMP是一种控制点,是GUPNPCONTROLPOINT的子类。
首先,我们要创建一个dlnadmp对象,创建gmainloop对象,gmaincontext对象,以及创建一个DLNA线程,并初始化消息循环,启动mainloop。
创建dlna对象的代码示例如下:
dlna_dmp_new(context, "urn:schemas-upnp-org:device:MediaServer:1");
DLNADmp* dlna_dmp_new(GUPnPContext* context, const char* target)
{
DLNADmp * self;
g_return_val_if_fail(GUPNP_IS_CONTEXT(context), NULL);
g_return_val_if_fail(target != NULL, NULL);
self = g_object_new(DLNA_TYPE_DMP, "client",context,"target",target,NULL);---/*创建一个dlnadmp对象,并将其属性client指针成员赋初值为context。client的定义在其父类即gssdpreourcebrowser的成员gssdpclient *client。注意context实际上是ssdpclient的子类。
gssdpclienr return self;
}
其次,在发送搜寻设备的请求前,dlnadmp(从指点)需要做如下几个工作:
(1)设置请求socket回调函数。
(2)安装一些信号回调函数:
(3)信号回调的连接
用户在新建一个控制点时,会设置一些参数。如ST(搜寻目标)等,代码示例如下:
g_object_new(DLNA_TYPE_DMP, "client",context,"target",target,NULL);
我们先来看看GSSDPResourceBrowser 的定义:
根据如下的类图关系,我们知道,代码g_object_new(DLNA_TYPE_DMP, "client",context,"target",target,NULL);
实际上对对控制点client成员进行了赋值和target成员进行了赋值.
GObject
+----GSSDPResourceBrowser (它聚合gssdpclient,因此具有网络功能)
+----GUPnPControlPoint ( 聚合GUPnPResourceFactory)
+----DLNADmp
那么,对client成员赋值是如何进行的呢?(注意,成员变量client并不是一个字符串变量)。答案是gssdp_resource_browser_set_property:
在gssdp_resource_browser_set_client函数中,将会完成信号与回调的连接:
上面的代码完成了什么工作呢?当然,是一些重要属性的初始化,为设备发现作好准备。一是,成功设置了ssdoclient,即设备发现客户端;二是,成功设置了ST,即设备发现目标。
同时,该代码还触发了如下动作:安装几个信号处理器。 signals[RESOURCE_AVAILABLE] = g_signal_new ("resource-available"........ signals[RESOURCE_UNAVAILABLE] =
g_signal_new ("resource-unavailable",........
同时ssdpclient在初始化时会安装响应callback信号signals[MESSAGE_RECEIVED] =
g_signal_new ("message-received",。。。。。。。
在给DLNAdmp 的client赋初值context时,由于在初始化时安装好了属性
g_object_class_install_property
(object_class,
PROP_CLIENT,
g_param_spec_object
("client",
"Client",
"The associated client.",
GSSDP_TYPE_CLIENT,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
G_PARAM_STATIC_BLURB));
于是将调用gssdp_resource_browser_set_property函数,进而 将连接信号与闭包。
既然一些准备工作做好了,接下来我们就可以开始启动设备发现这一功能了。如何启动呢,调用函数gssdp_resource_browser_set_active (GSSDPResourceBrowser *resource_browser, gboolean active)即可。该函数主要作了如下工作:发送设备discovery请求;同时,修改自己的active属性g_object_notify (G_OBJECT(resource_browser), "active");
调用顺序:_gssdp_client_send_message-----------》start_discovery (resource_browser);
在调用start_discovery以后,用户就可以依靠之前安装的套接字的回调request_socket_source_cb和multicast_socket_source_cb来获取网络上得信息。
。这2个回调最终都将调用socket_source_cb (client->priv->multicast_socket, client);。在socket_source_cb中,将会调用parse_http_request对接收到的内容进行解析,如果解析成功,就发送相关信号通知ssdpclient点消息已经收到:
Message_received_cb函数会处理将调用 received_announcement (resource_browser, headers);
/* Check announcement type */
if (strncmp (header,
SSDP_ALIVE_NTS,
strlen (SSDP_ALIVE_NTS)) == 0)
resource_available (resource_browser, headers);
else if (strncmp (header,
SSDP_BYEBYE_NTS,
strlen (SSDP_BYEBYE_NTS)) == 0)
resource_unavailable (resource_browser, headers);
如果是alive消息,调用 resource_availbale函数,
可以在代码中看到,Resource_available函数在gupnp-resource-browser.c文件中有一个。那么是否是调用它呢?我认为不是。它应该是被覆盖了,最后调用的是位于gupnp_control_point.c里面的gupnp_control_point_resource_available函数。这里相当于实现了C++里的虚函数机制--多态:
gupnp_control_point.c里的 gupnp_control_point_resource_unavailable最后又调用load_description (control_point,
locations->data,
udn,
service_type);
函数,用以加载或解析设备URL。当设备描述文档下载完毕后,会调用description_loaded函数。description_loaded函数会调用
process_device_list去递归的搜索匹配的设备,并且会发送信号:
由于在dlna_dmp_instance_init(DLNADmp * self)函数时,我们已经设定:
static void dlna_dmp_instance_init(DLNADmp * self)
{
self->priv = DLNA_DMP_GET_PRIVATE(self);
self->priv->dev_list = NULL;
self->priv->dev_list_lock = g_mutex_new();
self->priv->didl_parser = gupnp_didl_lite_parser_new();
self->priv->curDevInfo = NULL;
self->priv->browse_data = NULL;
self->priv->cancelled = FALSE;
self->priv->cancel_mutex = g_mutex_new();
self->priv->cancel_cond = g_cond_new();
}
所以调用:
static void device_proxy_available_cb (GUPnPControlPoint *cp,
GUPnPDeviceProxy *proxy,gpointer user_data)
{
const char *type;
DLNADmp* self = user_data;
type = gupnp_device_info_get_device_type(GUPNP_DEVICE_INFO (proxy));
if(g_pattern_match_simple(MEDIA_SERVER, type))
{
add_media_server(self,proxy);
}
}
(*dmp_event_callbackDLNA_EVENT_DEVICE_ADDED_,gupnp_device_info_get_udn((GUPnPDeviceInfo*)proxy));
在device_proxy_available_cb中,用户可以把得到的server信息添加到缓存中,并通知用户发现了一个设备。