使用 UPnP Control Point API 编程控制点应用程序
本文介绍如何使用微软的 UPnP 控制点 API 来查找和控制 UPnP 设备,并包含一个简单库的描述,以方便在您自己的程序中应用控制点 API,同时附有一个 MFC 和 WinForms 应用程序的示例。
注意:这是文章的更新版本。在原始版本中,我展示了使用 ATL 库来简化 COM 编程的代码。ATL 是一个非常有用的库,但我认为许多程序员可能因为他们使用的 Visual Studio 版本(Express 版)中缺少这个库而无法使用它。因此,我创建了不依赖 ATL 的新版本 UPnPCpLib 库(当然,这使得 COM 编程更加困难 :))。作为替代,我使用了 STL 库。我的库的 ATL 版本(功能相同)仍然可以下载。 但这还不是全部。新库的版本更加面向对象,也更易于使用,我希望如此 :)。此外,我借助新版本(STL)的 UPnPCpLib 库和 Visual C# 创建了一个新的 Windows Forms "Finder" 应用程序。.NET 版的 "Finder" 使用了原生库的托管包装器——"FindManager.net"。您可以在任何 .NET 语言(如 C# 或 Visual Basic)编写的应用程序中使用这个类库。 |
下载
- UPnPCpLib 库源码 (STL 版本) - 22 kB
- UPnPCpLib 库源码 (ATL 版本) - 23 kB
- STL 版本示例 (VS 2003 Win32 控制台项目) - 107 kB
- ATL 版本示例 (VS 2003 Win32 控制台项目) - 116 kB
- “Finder” MFC 应用程序源码 (VS 2003 MFC 项目) - 126 kB
- “Finder” MFC 应用程序 (可执行文件) - 96 kB
- "Finder.net" WinForms 应用程序源码 (VC# 2008 Express Edition WinForms 项目) - 87 kB
- “Finder.net” WinForms 应用程序 (可执行文件) - 205 kB
- “FindManager.net” 类库源码 (VC++ 2008 Express Edition 项目) - 68 kB
- “FindManager.net” 类库 (dll) - 315 kB
目录
- 引言
- UPnP 技术
- 控制点 API
- UPnPCpLib 框架
- WinForms 控制点应用程序 (Finder.net)
- 历史
- 参考文献

引言
本文介绍如何使用微软的 UPnP 控制点 API [1] 来查找和控制 UPnP 设备,该 API 在 Windows Me、CE .Net、XP 及更高版本中可用。文章还描述了一个简单的类和函数库,其目的首先是为了让您在自己的应用程序中更容易地使用控制点 API,其次是为了帮助构建检测到的 UPnP 设备的结构模型。最后(为最有耐心的读者准备的 ;)),是一个简单的 MFC 和 WinForms 应用程序示例,旨在展示控制点 API 与该库结合使用的可能性,包括读取互联网网关设备的公共 IP 地址并配置其“端口转发”功能。需要注意的是,UPnP 控制点 API 既可用于嵌入在 HTML 页面中的脚本,也可用于用 C++ 或 Visual Basic 语言编写的应用程序。然而,本文主要面向 C++ 开发人员。我假设您已经掌握了使用 COM 接口和 STL 库的基础知识。在阅读本文时,手边最好有控制点 API 的文档 [1]。
在产生写这篇文章的想法之前,我研究了使用 Winsock 库进行网络编程的问题。然后,我对以编程方式读取家庭网络中路由器的公共 IP 地址的问题产生了兴趣。在这种情况下,我分析了各种可用的解决方案,例如通过 SNMP 协议(简单网络管理协议)[5],或者使用一个采用 STUN 协议(简单 UDP 穿越 NAT)[6] 的公共服务器,或者简单的报文分析。然而,我的探索倾向于寻找一个简单且独立的解决方案。我排除了 SNMP,因为我发现它开发起来很困难,尽管它允许与路由器直接通信。相比之下,专用服务器的服务虽然易于实现,但不满足直接通信的条件。然后,我对通用即插即用技术(UPnP)产生了兴趣。事实证明,UPnP 提供了与实现了该技术的网络设备直接通信的可能性,从而可以下载关于设备的各种有用信息,并通过共享服务来控制它。问题是,UPnP 技术是否满足了我的期望,并被证明是解决问题的好方案?我必须说,答案是否定的 :),但这项技术本身引起了我的足够兴趣,以至于我决定更深入地学习它,于是就产生了写这篇文章的想法。为什么我没有认为 UPnP 是完美的解决方案?首先,对 UPnP 技术的支持,就像 SNMP 协议一样,在 SOHO 网络设备中默认不是一个活动功能。要使用它,您必须正确配置设备。然而,UPnP 技术逐渐普及,越来越多的公司决定在他们的设备中实现它。SOHO 级网络设备(如路由器和接入点)中越来越多地可以找到 UPnP 功能。其次,需要准备操作系统。同样在这种情况下,操作系统中负责处理 UPnP 技术的元素也需要用户进行配置。看来,轻松使用 UPnP 技术面临着许多障碍。在本文的其余部分,我将描述如何处理这些问题。经过这番介绍,那些还没有对 UPnP 产生厌恶感的读者 ;) ,我邀请您继续阅读。
UPnP 技术
UPnP(通用即插即用)是一项技术,简而言之,它将我们熟知的个人电脑中的“即插即用”理念,应用到了能够通过网络进行通信的设备上。UPnP 允许不同的联网设备(如智能家电、无线设备和个人电脑)直接通信,以交换数据和控制设备。支持 UPnP 技术的设备连接到网络的过程是自动进行的,无需手动配置。设备会自行配置,识别其工作环境,然后宣告自己的存在和工作意愿,接着检测网络中存在的其他设备。UPnP 独立于物理介质、操作系统和编程语言。UPnP 架构基于现有的标准,如 XML 以及 IP、TCP、UDP、HTTP、SSDP 协议。关于 UPnP 技术的更精确和全面的定义及其架构的描述,可以在 UPnP 论坛组织的网站上找到 [3],该组织致力于 UPnP 技术的发展、为此架构制定标准以及设备认证。至于这项技术的用处,我认为,它是数字家庭理念的一个有趣实现。
在 Windows XP 中的 UPnP 实现和配置
UPnP 技术受以下 Windows 家族系统的支持:Millennium Edition、CE .Net、XP 及更新版本(即 Vista)。在 Windows XP 中的实现 [8] [9](称为 UPnP 框架)基于两个基本组件:系统服务和提供编程接口的 COM 库。与 UPnP 相关的系统服务是“Universal Plug and Play Device Host”(upnphost)和“SSDP Discovery Service”(ssdpsrv)。如您所见,系统中有两个与 UPnP 相关的不同服务。这是因为微软将与托管提供服务的 UPnP 设备相关的功能,与发现和控制 UPnP 设备的功能分开了,而且我们稍后会看到,在 API 方面也引入了类似的划分。
upnphost 服务通常允许运行扮演 UPnP 设备角色的应用程序,为它们提供符合 UPnP 架构的必要功能。因此,如果您希望运行 UPnP 设备应用程序,必须首先检查 upnphost 服务是否正在运行。相反,ssdpsrv 服务让您可以在系统中所有可用的网络上找到 UPnP 设备。该服务通过 SSDP 协议(简单服务发现协议)[7] 检测连接到网络的 UPnP 设备,并在设备连接或断开时通知客户端。为了让帮助发现和控制 UPnP 设备的应用程序能够正常运行,必须启动 ssdpsrv 服务。
在 Windows XP 中,有几个编程接口允许开发者在他们的应用程序中使用 UPnP 技术。它们是“网络地址转换遍历 API”、“UPnP 设备主机 API”和“UPnP 控制点 API”。所有这些接口都在 MSDN 中有描述 [1] [2]。我将简要解释前两个 API 的目的,因为在本文中我不会描述它们,而是将重点放在控制点 API 上。NAT 遍历 API [2] 允许在局域网中的计算机上运行的应用程序以编程方式配置互联网网关设备(IGD)类型的设备中的端口映射功能(端口转发),这些设备支持网络地址转换(NAT)和 UPnP 技术。IGD 可以是局域网中的硬件路由器,也可以是启用了互联网连接共享功能的 Windows XP 个人电脑。Mike O'Neill 撰写的一篇关于 NAT 遍历 API 的优秀文章,题为“使用 UPnP 进行程序化端口转发和 NAT 遍历” [4],可以在 CodeProject.com 上找到。另外两个 API:设备主机和控制点,与系统服务的情况一样,是为不同领域的编程设计的两个独立接口。设备主机 API 为创建托管在 Windows 上并充当 UPnP 设备的应用程序提供支持,这些设备向其他设备或控制点类型的应用程序展示其服务。设备主机 API 帮助开发者构建设备的基本功能,包括共享服务,并将它们与其余必要的功能(发现、控制和事件)集成,这些功能由 UPnP 框架和系统服务 upnphost 和 ssdpsrv 提供。
为了让 UPnP 应用程序与 Windows XP 顺利协作,除了系统服务外,还必须正确设置防火墙应用程序 [10]。SSDP 协议(ssdpsrv 服务)使用 UDP 端口 1900 进行广播,使用 TCP 端口 2869 进行事件通知。因此,为了使 ssdpsrv 服务以及因此控制点类型的应用程序能够正常运行,您应该在防火墙中解除对 UDP 端口 1900 和 TCP 端口 2869 的阻塞。在 Windows XP Service Pack 1 及更早的版本中,使用的是 TCP 端口 5000,而不是 TCP 端口 2869。
此时,值得一提的是通过系统注册表中的设置(配置设置)来配置 UPnP 框架的可能性 [1]。其中一些涉及 SSDP 协议参数,其余的涉及 UPnP 框架的整体功能。其中有一些有趣的选项,例如,可以更改负责最大跳数的 SSDP 协议数据包的 TTL 参数的默认值。另一个有趣的选项是在设备发现期间可接受的 IP 地址范围。有关配置设置的详细说明,请参阅 MSDN。
现在,我们对 Windows XP 中 UPnP 的配置有了更丰富的知识,可以继续讨论控制点 API 了。
控制点 API
UPnP 控制点 API 用于创建能够发现和控制 UPnP 设备的应用程序。必须注意的是,UPnP 设备可以是专用的硬件设备(如路由器)或在 PC 上运行的应用程序。发现的设备可以连接到任何物理网络介质,只要运行该应用程序的计算机可以访问该介质。UPnP 框架为应用程序提供了所有必要的查找和控制 UPnP 设备的机制,以及与设备生成的事件通知和设备在网络上的连接或断开相关的机制。提供这些机制的 UPnP 框架的主要元素是系统服务 ssdpsrv。通过控制点 API,可以构建不作为 UPnP 设备运行,而只执行所谓的“控制点”任务的应用程序,即发现设备并与之通信以使用服务和注册事件的应用程序。当然,设备应用程序也可以通过控制点的功能来丰富其功能。控制点接口可以在用 C++ 或 Visual Basic 编写的 MS Windows 应用程序中使用。也有可能在用 VBScript 脚本语言编写的、嵌入在 HTML 文件中的脚本中使用此 API,这样就可以为 Web 浏览器创建应用程序。这可能是创建控制点类型应用程序的最简单快捷的方式。但在本文中,我将从 C++ 程序员的角度关注控制点 API。控制点 API 所引用的类存储在一个名为 upnp.dll 的 COM 库中。为了无缝使用此库,最好先安装 Platform SDK,其中包含所需的头文件和库。在应用程序的源文件中必须包含“upnp.h”文件。
API 中的基本对象
使用控制点 API,我们将需要处理与 UPnP 设备架构密切相关的三个基本对象
- 设备查找器
- 设备
- 服务
设备查找器对象执行与在网络中发现设备相关的任务,并向应用程序提供找到的设备对象。设备对象代表一个 UPnP 设备的模型,它直接关系到 UPnP 论坛组织制定的 UPnP 设备架构标准。从程序员的角度来看,设备对象是一个物理 UPnP 设备,应用程序通过与之通信来使用其服务。在一个自治的设备对象(根设备)中,可以嵌入成员设备对象(子设备)。服务对象是设备对象不可分割的基本元素,其任务是向客户端(设备、控制点)提供服务。使用设备的服务依赖于控制点在服务对象上调用所谓的“动作”。一个设备对象可以包含多个服务对象。
为了更好地理解上述对象的功能以及控制点 API 中各个 COM 接口的目的和功能,让我们考虑一个 UPnP 设备的模型(对象),下图对其进行了示意性展示。

这是一张关于查找器、设备和服务对象之间关系的总体示意图,以及控制点类型应用程序功能的非常概括的示意图。这类应用程序的核心功能包括实现查找器对象,该对象将为应用程序提供设备对象,并与设备对象进行交互。该图展示了设备对象的结构,其基本要素是:成员设备集合——设备,和服务集合——服务。需要注意的是,设备集合中的每个设备对象都可能包含其自己的成员设备对象。这意味着设备对象的结构是一种树形结构。我们将在下一张图中看到一个名为“服务”的“黑匣子”,它将稍微揭示其内容。

服务对象,其结构如图所示,代表设备对象提供的服务。每个服务都有一组逻辑上连贯的动作,这些动作与“状态变量”密切相关。这些变量,在图上用符号“Var1”和“Var2”表示,代表了服务的特性。这些特性(即变量)接受特定的值,这些值清晰地定义了服务以及该服务所属设备的状态。如果状态变量的值发生了变化,那么这意味着设备的状态也发生了变化,这将通过事件通知来指示这一变化。控制点应用程序使用回调对象订阅并接收事件通知,通过这种方式监控设备的当前状态。知道了当前状态,应用程序可以做出适当的响应。设备的服务(即服务对象)提供了一组可以通过使用该服务来执行的动作。控制点应用程序使用这些动作来检索有关设备状况的信息,并命令其状况发生变化或执行某些任务,简而言之,就是控制设备。控制点应用程序通过动作影响状态变量,从而引起设备的一些特定反应,例如,降低光源的强度。每个动作可能与一个或多个状态变量相关联。也可能出现动作不归属于任何变量的情况。在上图中,我们有一个例子,其中动作“Action1”引起变量“Var1”的变化,动作“Action3”改变变量“Var2”,而动作“Action2”引起变量“Var1”和“Var2”的共同反应。实际上,控制点 API 允许使用 QueryStateVariable
方法直接读取状态变量的值,然而,UPnP 论坛组织不鼓励使用此函数,并建议使用动作和事件通知。
与设备查找器对象相关的接口
控制点 API 是一组几个 COM 接口,可以根据它们所关联的对象类型进行划分。这种划分涉及前面讨论的三个基本对象。第一组是引用“设备查找器”对象的接口,顾名思义,它负责发现设备,或者更准确地说,实现与监控网络中设备连接和断开过程相关的任务。
- IUPnPDeviceFinder
- IUPnPDeviceFinderCallback
IUPnPDeviceFinder
是控制点 API 的关键接口之一,用于创建设备查找器对象。通过它,您可以使用同步或异步方法,借助回调对象来查找设备。在许多情况下(例如 GUI 应用程序),异步方法会是更好的选择,因为它不会阻塞程序的用户界面。设备查找器对象发现设备的结果是,我们通过 IUPnPDevice
接口获得一个设备对象。还有另一种创建设备对象的方法,我将在稍后介绍 IUPnPDevice
接口时讨论。
请记住,在开始使用 COM 库和创建任何对象之前,您必须初始化该库,并在完成其工作后释放资源。如果您为多个 Windows 平台编写应用程序,应注意 COM 库的初始化方式。在这种情况下,API 文档建议使用单线程单元模型,以避免在 Windows Me 中选择多线程单元模型时可能出现的问题。
#define _WIN32_DCOM // for CoInitializeEx #include <objbase.h> HRESULT hr; hr = CoInitializeEx(0, COINIT_APARTMENTTHREADED); if(hr == S_OK) { // ... code using COM library } CoUninitialize();
对于控制台应用程序或专用于在 Windows Me 上运行的 GUI 应用程序,COM 库更适合的线程模型是单线程模型而非多线程模型。要初始化 COM 库以使用单线程模型,您应在 CoInitializeEx
函数调用中指定 COINIT_APARTMENTTHREADED
标志。另一方面,要使用 COM 库的多线程模型,则需要指定 COINIT_MULTITHREADED
标志。
让我们回到设备查找器对象。创建对象后,我们可以通过 IUPnPDeviceFinder
接口访问它。选择同步方法时,我们有两个属于此接口的函数可用。使用 FindByType
函数,您可以搜索特定类型的设备——如果您传递特定类型的名称(例如 urn:schemas-upnp-org:device:BinaryLight:1),或者传递所有 UPnP 设备共有的类型 "upnp:rootdevice" 来搜索任何类型。此函数以 IUPnPDevices
接口的形式返回找到的设备集合。第二个选项是 FindByUDN
函数,它接收设备的唯一名称 UDN(Unique Device Name)作为参数,不幸的是,您必须事先知道这个名称,而它以 IUPnPDevice
接口的形式返回设备对象。同步方法的缺点是等待结果的时间很长,这会使程序执行暂停长达几十秒。我不包含同步方法的示例,因为现在我们将讨论更有指导意义的异步方法,并将用示例进行说明。
异步查找包括四个主要步骤。为了更好地理解整个异步搜索过程,我将使用一张图,我希望这张图能胜过千言万语 ;)。

第一步是创建一个回调对象,它将接收搜索结果(第 1、2、3 项)。在下一步中,我们调用 CreateAsyncFind
函数,向其传递指向回调对象的指针(第 5 项)。接下来的步骤是使用 StartAsyncFind
函数开始实际的搜索(第 6 项)。最后一步是将搜索结果从 UPnP 框架返回给回调对象(第 7 项)。现在我将更详细地解释各个步骤。
要使用异步方法,您需要一个回调对象。要创建这样一个对象的实例,您必须首先准备一个实现 IUPnPDeviceFinderCallback
接口的类(第1项)。此时,您可以在自己的类中自行实现 IUnknown
接口及其后代 IUPnPDeviceFinderCallback
的所有函数,但是……ATL 库为我们带来了更简单的解决方案。如果我们的回调类继承自属于 ATL 的 CComObjectRootEx
类,那么我们将不必实现 IUnknown
接口的函数,只要我们以这种方式准备的类派生自 CComObject
模板类。多亏了这一点,开发者可以免于实现 COM 对象的通用细节,依赖 CComObject
类进行引用计数管理和对象实例创建,而可以专注于 IUPnPDeviceFinderCallback
接口的实现细节。让我们看一个我称之为 DevFinderCallback
的类的示例(不含 ATL 的版本)。
// IUPnPDeviceFinderCallback implementation, without ATL #include <upnp.h> class DevFinderCallback : public IUPnPDeviceFinderCallback { private: long _refcount; // object's reference counter public: DevFinderCallback() {_refcount = 0;} virtual ~DevFinderCallback() {} // IUnknown implementation virtual HRESULT _stdcall QueryInterface(const IID& riid, void** ppvObject) { HRESULT result = ppvObject == 0 ? E_POINTER : S_OK; if(result == S_OK) { if(riid == IID_IUnknown || riid == IID_IUPnPDeviceFinderCallback) { *ppvObject = static_cast<IUPnPDeviceFinderCallback*>(this); this->AddRef(); } else { *ppvObject = 0; result = E_NOINTERFACE; } } return result; } virtual unsigned long _stdcall AddRef() { return ::InterlockedIncrement(&_refcount); } virtual unsigned long _stdcall Release() { if(::InterlockedDecrement(&_refcount) == 0L) { delete this; return 0; // object deleted !!! } return _refcount; } // IUPnPDeviceFinderCallback implementation virtual HRESULT __stdcall DeviceAdded(long findid, IUPnPDevice* idev) { // process IUPnPDevice pointer when device was added return S_OK; } virtual HRESULT __stdcall DeviceRemoved(long findid, BSTR devudn) { // do something when device was removed return S_OK; } virtual HRESULT __stdcall SearchComplete(long findid) { // do something when search is complete return S_OK; } };
应在您自己的回调类中实现的 IUnknown
接口函数有
- QueryInterface,
- AddRef,
- Release。
这些函数在创建和删除对象时由 COM 库或用户调用。在 QueryInterface
中,根据带有指定正确接口标识符的请求,返回指向回调对象的指针。AddRef
和 Release
分别增加或减少对象的引用计数。由于 "Interlocked..." 函数,这些操作是线程安全的。当计数器等于零时,对象可以被删除。
应在您自己的回调类中实现的 IUPnPDeviceFinderCallback
接口函数有
- DeviceAdded,
- DeviceRemoved,
- SearchComplete。
这些函数在以下时刻由 UPnP 框架在回调对象中调用:当找到搜索的设备或该设备重新连接到网络时(DeviceAdded
),当先前找到的设备从网络断开时(DeviceRemoved
),或者当由 StartAsyncFind 函数调用发起的搜索终止时(SearchComplete
)。这些函数的返回值与 UPnP 框架无关。
下一个示例展示了如何创建一个 DevFinderCallback
类的回调对象(第2项)。
// Instantiate the callback object DevFinderCallback* cback = new DevFinderCallback(); // after object's creating reference counter is initially equal to zero // so, it is neccessary to increment it. // be sure to release on finish, // once created object should not be deleted directly (delete cback) // instead use Release function. cback->AddRef();
因此,我们已经创建了一个回调对象(第 3 项),现在我们可以关注设备查找器对象的创建了。我们使用 COM 库来创建这个对象(第 4 项)。
// Instantiate the device finder object #include <upnp.h> HRESULT hr; IUPnPDeviceFinder* _ifinder = 0; hr = CoCreateInstance(CLSID_UPnPDeviceFinder, 0, CLSCTX_SERVER, IID_IUPnPDeviceFinder, (void**)&_ifinder); if(hr == S_OK) { // use finder object here }
在创建设备查找器(第 4 项)和回调(第 3 项)对象之后,就可以开始搜索设备了。在我们开始实际搜索之前,有必要使用 IUPnPDeviceFinder
接口的 CreateAsyncFind
函数(第 5 项)来指定要搜索的设备类型(与 FindByType
函数的情况一样),向 UPnP 框架传递一个指向回调对象的指针,并传递一个 long
类型的空指针,它将作为会话标识符。在 CreateAsyncFind
函数返回后,我们得到会话标识符的值,这是调用设备查找器对象的另一个函数——StartAsyncFind
所必需的。这个函数调用开始了实际的搜索(第 6 项)。StartAsyncFind
函数会立即返回一个 HRESULT
类型的值,指示调用是否无误地执行,以及搜索请求是否已转发给 UPnP 框架。从那时起,回调对象就在等待结果。使用一次性收到的会话标识符,我们可以重复调用 StartAsyncFind
函数。如果我们使用 CreateAsyncFind
创建了更多的搜索会话(每次传递不同的会话标识符),那么回调对象可以通过使用它的标识符来识别每个会话,这个标识符将在返回搜索结果时由 UPnP 框架传递给回调对象。在调用 StartAsyncFind
函数后的任何时候,我们都可以通过使用查找器对象的 CancelAsyncFind
函数来中断搜索,将我们想要结束的会话的标识符作为参数传递。
// start asynchronous find for all root devices #include <upnp.h> HRESULT hr; // Device Finder object interface IUPnPDeviceFinder* _ifinder = 0; // device type to find std::wstring devtypestr(L"upnp:rootdevice"); BSTR devtype = SysAllocString(devtypestr.c_str()); // search identifier long _findhandle; hr = CoCreateInstance(CLSID_UPnPDeviceFinder, 0, CLSCTX_SERVER, IID_IUPnPDeviceFinder, (void**)&_ifinder); if(hr == S_OK) { // prepare search // cback is DevFinderCallack pointer obtained earlier hr = _ifinder->CreateAsyncFind(devtype, 0, cback, &_findhandle); if(hr == S_OK) { // start search hr = _ifinder->StartAsyncFind(_findhandle); if(hr == S_OK) { // do something if success or simply return } } } SysFreeString(devtype);
如果我们在应用程序中一次性创建了查找器对象并启动了搜索,那么 UPnP 框架将在我们的应用程序完成之前,一直为我们的应用程序监控网络并向回调对象中继结果。当找到特定类型的设备或连接到网络时,UPnP 框架将调用回调对象的 DeviceAdded
函数,并传递该设备对象的 IUPnPDevice
接口(第 7 项)。回调对象的 SearchComplete
函数在由查找器对象通过 StartAsyncFind
函数发起的搜索的初始阶段完成后由框架调用。此阶段的持续时间可能长达几分钟。SearchComplete
函数的调用并不意味着发现设备过程的结束。我们可以通过调用查找器的 CancelAsyncFind
函数来结束此过程。
当应用程序正在关闭并且我们使用了异步搜索方法时,请务必销毁已创建的 COM 对象,即设备查找器和回调。不要直接删除回调对象,而应使用 Release
函数。
// cancelling search and destroing objects // release DevFinderCallback object if(cback != 0) cback->Release(); // cancel search and release Device Finder object if(_ifinder != 0) { _ifinder->CancelAsyncFind(_finderhandle); _ifinder->Release(); }
与设备对象相关的接口
下一组与设备对象相关
- IUPnPDevice,
- IUPnPDevices,
- IUPnPDeviceDocumentAccess,
- IUPnPDescriptionDocument,
- IUPnPDescriptionDocumentCallback。
本组中最重要的是 IUPnPDevice
接口,通过它可以访问设备对象。无法直接创建此接口,因为它是 UPnP 框架作为搜索网络上可用设备的结果而提供的。通过接收到的设备对象接口指针,可以使用此接口的函数读取大量关于设备的信息(属性),例如其唯一名称、型号名称、制造商名称或 URL,该 URL 将在浏览器中打开以访问设备配置选项。
此外,IUPnPDevice
接口的函数允许检查设备的结构,即其内部逻辑结构
- get_HasChildren,
- get_Children,
- get_Services。
通过 get_HasChildren
函数,我们可以查明设备是否包含成员设备(子设备)。如果 get_HasChildren
函数返回逻辑值“true”,那么您可以通过调用 get_Children
获取成员设备的集合。成员设备的集合以 IUPnPDevices
接口的形式返回。请注意,设备的逻辑结构是树状的,因此,每个成员设备都可能包含其自己的成员设备集合。最方便的处理树状结构识别的方法是通过递归,即在辅助递归函数中调用 get_Children
函数。get_Services
函数返回给我们设备的服务集合——服务对象,以 IUPnPServices
接口的形式。关于与服务对象相关的接口的更多信息将在本文后面介绍。
让我们通过一个例子来看看识别设备结构的过程。
// examination of structure of root device #include <upnp.h> // _idevice is pointer to root device void EnumerateDevices(IUPnPDevice* _idevice) { IUPnPDevices* children = 0; // collection of member devices VARIANT_BOOL bcheck = 0; // first check if device has children if(_idevice->get_HasChildren(&bcheck) == S_OK && bcheck != 0) { // if device has children then get collection interface if(_idevice->get_Children(&children) == S_OK) { // device has children and collection's pointer is valid HRESULT hr = S_OK; IUnknown* ienum = 0; // for obtain the enumerator long devscount = 0; // count of member devices // get count of member devices children->get_Count(&devscount); // enumerate and add devices // get helper interface if(children->get__NewEnum(&ienum) == S_OK) { IEnumUnknown* icol = 0; // collection enumerator hr = ienum->QueryInterface(IID_IEnumUnknown, (void**)&icol); if(hr == S_OK) { IUnknown* iitem = 0; // collection’s item IUPnPDevice* ichild = 0; // member device // enumerate collection icol->Reset(); while(icol->Next(1, &iitem, 0) == S_OK) { // get member device interface hr = iitem->QueryInterface(IID_IUPnPDevice, (void**)&ichild); if(hr == S_OK) { // here we can do something with device's pointer // continue recursive EnumerateDevices(ichild); ichild->Release(); ichild = 0; } iitem->Release(); iitem = 0; } icol->Release(); } ienum->Release(); } children->Release(); } } }
描述文档
UPnP 设备架构中最重要和最基本的元素之一是“描述文档”。这只是一个 XML 文档(以下我将使用“文档”这个概念),其中记录了关于设备及其结构的所有信息。该文档是设备的必需元素,其格式和结构由 UPnP 论坛组织的标准 [3] 规定。XML 文件形式的文档由设备制造商准备并放置在设备中。
存储在文档中的有关设备的信息是设备的特定属性,与使用 IUPnPDevice
接口可以读取的属性相同。文档中最关键的是它包含了设备整个逻辑结构的完整描述,即成员设备树及其属性和服务集合的描述。由此得出一个重要结论:如果文档已被下载,那么通过分析它,您可以读取有关设备的所有信息,而无需访问设备对象和使用接口。
让我们看看如何获取对描述文档的访问。在这个任务中有用的接口是 IUPnPDeviceDocumentAccess
。它只有一个函数 GetDocumentURL
,正如您可能猜到的,它返回文档的 URL。我们将通过 IUPnPDevice
接口获得指向 IUPnPDeviceDocumentAccess
的指针。
// retrieving of description document URL #include <upnp.h> // idev is pointer to root device void GetDeviceDocumentAccessURL(IUPnPDevice* idev) { HRESULT hr; IUPnPDeviceDocumentAccess* idoc = 0; // URL will be stored into this variable BSTR btmp = 0; // query for IUPnPDeviceDocumentAccess hr = idev->QueryInterface(IID_IUPnPDeviceDocumentAccess, (void**)&idoc); if(hr == S_OK) { // get URL and write to BSTR idoc->GetDocumentURL(&btmp); // do something with retrieved URL // on finish release resources SysFreeString(btmp); idoc->Release(); } }
我上面提到,除了通过设备查找器对象之外,还有另一种获取设备对象的方法。它依赖于使用 IUPnPDescriptionDocument
接口。然而,只有当我们事先知道我们感兴趣的设备对象的描述文档的 URL 时,这样做才有意义。要获取指向 IUPnPDescriptionDocument
的指针,您需要通过 COM 库和 CoCreateInstance
函数创建一个新对象。IUPnPDescriptionDocument
接口具有以同步或异步方式将文档加载到内存中的函数,使用回调对象(实现 IUPnPDescriptionDocumentCallback
的类)。只有在将文档加载到内存后,您才能调用返回指向 IUPnPDevice
的指针的函数。遗憾的是,尽管通过此方法显然已将文档加载到内存中,但 IUPnPDescriptionDocument
接口并未提供允许直接访问文档内容的函数。
与服务对象相关的接口
属于控制点 API 的最后一组接口与服务对象相关。
- IUPnPService,
- IUPnPServices,
- IUPnPServiceCallback。
对服务对象的访问只能通过设备对象获得。服务对象的主要接口是 IUPnPService
。要获得给定设备的服务集合——服务对象,您应该调用 IUPnPDevice
接口的 get_Services
函数。该函数返回服务集合对象,我们可以通过 IUPnPServices
接口访问它。
// retrieving of service objects #include <upnp.h> // idev is pointer to root device void EnumerateServices(IUPnPDevice* idev) { HRESULT hr = S_OK; long srvcount = 0; // count of services IUnknown* ienum = 0; // for obtain the enumerator IUPnPServices* isrvs = 0; // the collection of services // get collection interface if(idev->get_Services(&isrvs) != S_OK) return; // get number of services isrvs->get_Count(&srvcount); // get helper interface if(isrvs->get__NewEnum(&ienum) == S_OK) { IEnumUnknown *icol = 0; // the enumerator of collection hr = ienum->QueryInterface(IID_IEnumUnknown, (void**)&icol); if(hr == S_OK) { IUnknown* iitem = 0; // collection’s item IUPnPService* isrv = 0; // service object BSTR btmp = 0; // enumerate collection icol->Reset(); while(icol->Next(1, &iitem, 0) == S_OK) { // get interface to service object hr = iitem->QueryInterface(IID_IUPnPService, (void**)&isrv); if(hr == S_OK) { // do something with service object // for example get service id isrv->get_Id(&btmp); SysFreeString(btmp); isrv->Release(); } iitem->Release(); } icol->Release(); } ienum->Release(); } isrvs->Release(); }
IUPnPService
接口有两个非常有趣的函数,允许与设备交互(控制设备),即使用服务和读取状态变量的值。第一个主要函数是 InvokeAction
,我将为其留出更多篇幅,第二个是 QueryStateVariable
,通过它可以简单地读取状态变量的当前值。
使用 InvokeAction
函数并不太容易,所以,首先,我请求引用该函数的声明
HRESULT InvokeAction ( BSTR bstrActionName, // [in] name of action to invoke VARIANT varInActionArgs, // [in] array of input arguments of action VARIANT* pvarOutActionArgs, // [out] array of main output values of action VARIANT* pvarRetVal // [out] one action’s output value );
您可以看到该函数需要四个参数。它们的详细定义可以在 MSDN 上的 API 文档中找到 [1]。但我想提请注意的是,调用此函数时,我们通常传递动作的名称(bstrActionName
- 必需参数)和动作的输入参数数组(varInActionArgs
- 必需参数),而剩下的两个参数指向 VARIANT
类型的空变量。当然,这不仅仅是格式,而是最常见的格式。
InvokeAction
函数的主要问题在于,在调用它之前,您不仅需要知道要调用的动作的名称,还需要知道其参数的数量和类型。从哪里获取这些信息呢?答案是:从服务对象的描述文档中!这里又出现了另一个问题,因为控制点 API 的所有接口都没有允许我们直接读取文档内容的函数。您必须自己解决这个问题。知道了文档的 URL,您可以单独使用 HTTP 协议从设备下载文档文件,然后进行解析。
每个服务,就像设备本身一样,都在 XML 文件——描述文档中进行描述,该文档专用于给定的服务。该文档的结构也必须与 UPnP 论坛组织的标准 [3] 兼容。服务的文档包括动作的数据、它们的名称和参数,以及与动作相关的状态变量的详细信息。服务文档的结构由两个列表组成:动作和状态变量。对于每个动作,都指定了其名称和参数列表。如果动作有参数,则会给出它们的名称、传递方向(输入/输出)以及相关状态变量的名称。对于状态变量,定义了诸如名称、类型、允许值、最小值和最大值、步长和默认值等数据。
服务的文档文件可以从设备下载。但这些文档的 URL 在哪里找呢?URL 以相对形式(通常但不一定)保存在设备的文档中,在与给定服务相关的部分,在一个名为 <SCPDURL> 的标签中。根据标准,URL 的基础部分应该放在设备的文档中,在一个名为 <URLBase> 的标签中。
因此,获取动作数据的场景可能如下所示:
- 以上文描述
IUPnPDeviceDocumentAccess
接口的方式读取设备文档的 URL, - 下载设备描述文档文件,
- 检查设备文档的内容,从 SCPDURL 标签中读取服务文档的 URL(如果 URL 是相对的,则将其附加到从 URLBase 标签中读出的基础 URL),
- 下载服务描述文档文件,
- 检查服务文档的内容,读取我们感兴趣的动作数据。
从这个场景可以看出,我们需要解析 XML 文件。幸运的是,有许多优秀且免费的 C++ XML 解析器。
在示例中,我将向您展示如何借助 HTTP 协议,仅使用 Winsock 库从设备下载文档文件。
// downloading description document // sizes of buffers and URL are exemplary #pragma comment(lib, "ws2_32") // required linked library #include <winsock2.h> #include <iostream> using namespace std; // initialize Winsock library WSADATA wsadata; WSAStartup(MAKEWORD(2, 2), &wsadata); // path of document from its URL string path("/document.xml"); // prepare inet address from URL of document sockaddr_in addr; addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // put here ip address of device addr.sin_family = AF_INET; addr.sin_port = htons(56616); // put here port for connection const int rbsize = 4096; // internal buffer size char rbuff[rbsize] = {0}; // internal temporary receive buffer int rbshift = 0; // index in internal buffer of current begin of free space int b = 0; // bytes curently received int tb = 0; // bytes totally received string respbuff; // response buffer string headertail("\r\n\r\n"); // request tail string document; // document content // prepare string of HTTP GET command ostringstream os; os << "GET " << path << " HTTP/1.1\r\nHost: " << inet_ntoa(addr.sin_addr) << ':' << ntohs(addr.sin_port) << headertail; // create TCP socket SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // connect socket connect(s, (sockaddr*)&_addr, sizeof(sockaddr_in)); // send request b = send(s, os.str().c_str(), os.str().length(), 0); // receive response data - document while((b = recv(s, rbuff + rbshift, rbsize - rbshift, 0)) != SOCKET_ERROR) { // finish loop if connection has been gracefully closed if(b == 0) break; // sum of all received chars tb += b; // sum of currently received chars rbshift += b; // temporary buffer has been filled // thus copy data to response buffer if(rbshift == rbsize) { respbuff.append(rbuff, rbshift); // reset current counter rbshift = 0; } } // close connection gracefully shutdown(s, SD_SEND); while(int bc = recv(s, rbuff, rbsize, 0)) if(bc == SOCKET_ERROR) break; closesocket(s); // analyse received data if(tb > 0) { // copy any remaining data to response buffer if(rbshift > 0) respbuff.append(rbuff, rbshift); // check response code in header if(respbuff.substr(0, respbuff.find("\r\n")).find("200 OK") != string::npos) { int cntlen_comp = 0; // computed response's content length int cntlen_get = 0; // retrieved response's content length string::size_type pos; string::size_type posdata = respbuff.find(headertail); if(posdata != string::npos) { // compute content length cntlen_comp = tb - (posdata + headertail.length()); if(cntlen_comp > 0) { if(b == 0) { // connection has been gracefully closed // thus received data should be valid cntlen_get = cntlen_comp; } else { // get content length from http header // to check if number of received data is equal to number of sent data string header = respbuff.substr(0, posdata); transform(header.begin(), header.end(), header.begin(), tolower); if((pos = header.find("content-length:")) != string::npos) istringstream(header.substr(pos, header.find("\r\n", pos)).substr(15)) >> cntlen_get; } // if number of received bytes is valid then save document content if(cntlen_comp == cntlen_get) document.assign(respbuff.begin() + posdata + headertail.length(), respbuff.end()); } } } } // cleanup Winsock library WSACleanup();
现在,我们知道如何获取我们想要调用的动作的数据了。现在我们可以回到 InvokeAction
函数。在调用之前,需要根据获取的动作数据准备要传递的参数。传递动作的名称很简单。varInActionArgs
参数是 VARIANT
类型,包含一个 SAFEARRAY
类型的数组,其元素是动作的输入参数。如果某个动作不需要传递输入参数,那么传递一个 VARIANT
类型的空变量就足够了。SAFEARRAY
类型的数组将单个元素(动作的参数)存储为 VARIANT
类型的变量。pvarOutActionArgs
参数指向一个 VARIANT
类型的空变量,而在函数返回后,它包含一个 SAFEARRAY
类型的数组,其元素(VARIANT
类型)是动作的输出值。这些元素的数量和类型与放置在服务描述文档中的动作数据一致。最后一个参数 pvarRetVal
也指向一个 VARIANT
类型的空变量,而在调用结束时,它包含一个动作返回的单个值。
在使用 InvokeAction
函数时,我们将不得不处理 VARIANT
和 SAFEARRAY
类型的变量。使用这些类型不是很方便,因为它们的变量需要初始化和释放资源。这里 ATL 库再次提供了帮助。多亏了 ATL 类型,首先,开发者不需要记住释放资源,并且初始化是自动完成的。对于 VARIANT
类型,我们有适当的包装器,称为 CComVariant
,对于 SAFEARRAY
,有相应的类型 CComSafeArray
。ATL 的 CComBSTR
类型包装了 BSTR
类型。ATL 类型的应用大大简化了代码,使其更清晰。但是出于我在文章开头注释中提到的原因,我将展示不带 ATL 类型的代码。
在下面的示例中,我将向您展示如何使用 InvokeAction
函数来执行一个示例动作。假设我想控制的设备是家庭网络中的一个路由器(IGD),并且我想使用的服务是 urn:schemas-upnp-org:service:WANPPPConnection:1 类型的服务。该服务除其他外,提供一个名为 GetSpecificPortMappingEntry 的动作,用于从路由器的端口映射列表中检索单个、特定的条目。该动作有三个输入参数,并返回五个包含列表条目数据的值。从服务文档中读取的动作数据如下表所示
状态变量名称 与参数相关 |
传递方向 参数的 |
参数类型 | 允许值 |
RemoteHost | in | 字符串 | 可以是空字符串 |
ExternalPort | in | ui2 | |
PortMappingProtocol | in | 字符串 | TCP, UDP |
InternalPort | out | ui2 | |
InternalClient | out | 字符串 | |
PortMappingEnabled | out | 布尔值 | |
PortMappingDescription | out | 字符串 | |
PortMappingLeaseDuration | out | ui4 | 0 表示永久 |
从服务文档中读取的动作输入参数的顺序很重要。它们应该以相同的顺序放置在传递给 InvokeAction
函数的 varInActionArgs
变量的数组中。关于传递给动作的参数类型的更多信息将在本文后面介绍。
我假设我之前从设备查找器对象收到了设备对象,形式是指向 IUPnPDevice
接口的指针,然后借助它从设备对象中获得了指向我感兴趣的服务的 IUPnPService
接口的指针。
// Invoking service’s action // _iservice is pointer to service’s interface (IUPnPService*) #include <upnp.h> // helper array for input arguments // first argument's type in pair - type of argument // second argument's type in pair - value of argument vector< pair<unsigned short, wstring> > _in; typedef vector< pair<unsigned short, wstring> >::const_iterator ArgsIterator; // helper array for output arguments vector< pair<unsigned short, wstring> > _out; // first argument of type string (VT_BSTR) – remote host _in.push_back(pair<unsigned short, wstring>(VT_BSTR, L"")); // second argument of type ui2 (VT_UI2) – external port _in.push_back(pair<unsigned short, wstring>(VT_UI2, L"8839")); // third argument of type string (VT_BSTR) – port mapping protocol _in.push_back(pair<unsigned short, wstring>(VT_BSTR, L"TCP")); // action name BSTR actionName = SysAllocString(L"GetSpecificPortMappingEntry"); HRESULT hr = S_OK; long inargscount = 0; // number of input arguments VARIANT inargs; // Invoke argument in VARIANT outargs; // Invoke argument out VARIANT retval; // Invoke argument ret VARIANT inval; // temp input value SAFEARRAY* arr_inargs = 0; // array for input arguments VARIANT outval; // temp output value SAFEARRAY* arr_outargs = 0; // array for output arguments long arr_index[1]; // current index of safe array // get number of arguments inargscount = _in.size(); // initialize variants VariantInit(&inargs); VariantInit(&outargs); VariantInit(&retval); // create input array SAFEARRAYBOUND bounds[1]; bounds[0].lLbound = 0; bounds[0].cElements = inargscount; arr_inargs = SafeArrayCreate(VT_VARIANT, 1, bounds); if(arr_inargs != 0) { // fill input safe array ArgsIterator argi = _in.begin(); for(long i = 0; i < inargscount; ++i, ++argi) { arr_index[0] = i; VariantInit(&inval); inval.vt = VT_BSTR; V_BSTR(&inval) = SysAllocString((*argi).second.c_str()); // value of arg if(V_BSTR(&inval) != 0) { hr = S_OK; VARTYPE vt = (*argi).first; // type of arg if(vt != VT_BSTR) // change type if other than VT_BSTR hr = VariantChangeType(&inval, &inval, VARIANT_NOUSEROVERRIDE, vt); if(hr == S_OK) SafeArrayPutElement(arr_inargs, arr_index, (void*)&inval); } VariantClear(&inval); } // assign input array to input argument inargs.vt = VT_ARRAY | VT_VARIANT; V_ARRAY(&inargs) = arr_inargs; // invoke action hr = _iservice->InvokeAction(actionName, inargs, &outargs, &retval); if(hr == S_OK) // invoke succeeded { // data from outargs - output arguments arr_outargs = V_ARRAY(&outargs); long arrsize = 0; VARTYPE vartype; // get size of array of output arguments if(SafeArrayGetUBound(arr_outargs, 1, &arrsize) == S_OK) { // read arguments from array for(long i = 0; i <= arrsize; ++i) { arr_index[0] = i; VariantInit(&outval); // get current argument from array hr = SafeArrayGetElement(arr_outargs, arr_index, (void*)&outval); if(hr == S_OK) { vartype = outval.vt; // change type to VT_BSTR (string) if(outval.vt != VT_BSTR) hr = VariantChangeType(&outval, &outval, VARIANT_ALPHABOOL, VT_BSTR); // put current argument to helper utput array if(hr == S_OK) _out.push_back(pair<unsigned short, wstring>(vartype, V_BSTR(&outval))); } VariantClear(&outval); } } // data from retval if(retval.vt != VT_EMPTY) { hr = S_OK; vartype = retval.vt; if(retval.vt != VT_BSTR) hr = VariantChangeType(&retval, &retval, VARIANT_ALPHABOOL, VT_BSTR); if(hr == S_OK) _out.push_back(pair<unsigned short, wstring>(vartype, V_BSTR(&retval))); } } else // invoke failed { // do something if error occured } // destroy input array SafeArrayDestroy(arr_inargs); // array has been destroyed thus set related variant to empty // to avoid destroing array again during clear of variant inargs.vt = VT_EMPTY; } // clear variants VariantClear(&inargs); VariantClear(&outargs); VariantClear(&retval); SysFreeString(actionName);
从服务的文档中读取任何动作的信息时,可能会注意到动作参数类型名称的表示法与在操作 VARIANT
类型和使用控制点 API 的 C++ 代码中使用的表示法不同。例如,我们遇到的表示法是“string”,而不是“VT_BSTR”。这是因为 UPnP 论坛的标准是独立于编程语言的,在类型方面是基于 XML 标准的。在这种情况下,一个“备忘单”很有用,它可以提示我们面对的是什么类型。在下表中,我完成了服务文档中使用的 XML 类型名称及其在控制点 API 中与 VARIANT
类型一起使用的等效名称的列表。这是在 MSDN 文档 [1] 中可以找到的表格的略微扩展(更方便)的版本。
XML 数据类型 |
IDL 数据类型 |
VARIANT 的类型 |
bin.base64 |
SAFEARRAY |
VT_ARRAY | VT_UI1 |
bin.hex |
SAFEARRAY |
VT_ARRAY | VT_UI1 |
布尔值 |
VARIANT_BOOL |
VT_BOOL |
char |
wchar_t |
VT_UI2 |
date |
DATE |
VT_DATE |
dateTime |
DATE |
VT_DATE |
dateTime.tz |
DATE |
VT_DATE |
fixed.14.4 |
CY |
VT_CY |
float |
float |
VT_R4 |
i1 |
char |
VT_I1 |
i2 |
short |
VT_I2 |
i4 |
long |
VT_I4 |
int |
long |
VT_I4 |
number |
BSTR |
VT_BSTR |
r4 |
float |
VT_R4 |
r8 |
double |
VT_R8 |
字符串 |
BSTR |
VT_BSTR |
时间 |
DATE |
VT_DATE |
time.tz |
DATE |
VT_DATE |
ui1 |
unsigned char |
VT_UI1 |
ui2 |
unsigned short |
VT_UI2 |
ui4 |
unsigned long |
VT_UI4 |
uri |
BSTR |
VT_BSTR |
uuid |
BSTR |
VT_BSTR |
在关于 InvokeAction
函数的讨论结束时,值得关注此函数以 HRESULT
形式返回的错误代码,因为它们在确定函数调用失败的原因时可能很有帮助。错误代码可以在 upnp.h 文件或文档 [1] 中找到。
我之前提到过 IUPnPService
接口的另一个有趣的函数:QueryStateVariable
。在某些情况下,当需要立即获取状态变量的值时,它可能很有用,而该值通常只有在生成相关事件后才可用。但总的来说,这种情况应该不多,所以读取状态变量值的更有效方法是在变量值改变时接收事件通知。
为了接收服务生成的事件通知,使用回调对象,该对象应从实现 IUPnPServiceCallback
接口的类创建。它有两个函数,其中最有趣的是 StateVariableChanged
函数,在服务状态改变(状态变量的值改变)时由 UPnP 框架调用。第二个函数是 ServiceInstanceDied
,用于通知控制点应用程序服务不可用。UPnP 框架调用 StateVariableChanged
函数以通知应用程序状态变量值的变化,传递一个指向相关服务对象(IUPnPService*
)的指针以及已更改变量的名称和当前值。我们像为设备查找器创建回调对象类一样,创建一个接收事件通知的对象类。
// IUPnPServiceCallback implementation #include <upnp.h> // IUPnPServiceCallback // IUPnPServiceCallback implementation class SrvEventCallback : public IUPnPServiceCallback { private: long _refcount; // object's reference counter public: SrvEventCallback() : _refcount(0) {} virtual ~SrvEventCallback() {} // IUnknown implementation virtual HRESULT _stdcall QueryInterface(const IID& riid, void** ppvObject) { HRESULT result = ppvObject == 0 ? E_POINTER : S_OK; if(result == S_OK) { if(riid == IID_IUnknown || riid == IID_IUPnPServiceCallback) { *ppvObject = static_cast<IUPnPServiceCallback*>(this); this->AddRef(); } else { *ppvObject = 0; result = E_NOINTERFACE; } } return result; } virtual unsigned long _stdcall AddRef() { return ::InterlockedIncrement(&_refcount); } virtual unsigned long _stdcall Release() { if(::InterlockedDecrement(&_refcount) == 0L) { delete this; return 0; // object deleted !!! } return _refcount; } // IUPnPServiceCallback implementation virtual HRESULT _stdcall StateVariableChanged(IUPnPService* isrv, LPCWSTR varname, VARIANT varvalue) { // process current value of state variable return S_OK; } virtual HRESULT _stdcall ServiceInstanceDied(IUPnPService* isrv) { // do something when service was died return S_OK; } };
IUPnPServiceCallback
接口函数的返回值应等于零 (S_OK)。
在回调对象能够接收事件通知之前,有必要在服务中注册此对象,即使用 IUPnPService
接口的 AddCallback
函数将指向回调对象的指针传递给其(服务)对象。
// Instantiate and registering the callback object for receiving events notifications // isrv is pointer to service’s interface (IUPnPService*) SrvEventCallback* cback = new SrvEventCallback(); if(cback != 0) { // reference counter is 0 thus increment // be sure to release on finish cback->AddRef(); // register callback with service object isrv->AddCallback(cback); }
正如您所见,通过分析上面的示例和 IUPnPService
接口函数,从哪个设备(如果我们控制多个设备并且只创建了一个回调对象)收到事件通知并不明显。这个疑问源于这样一个事实:在函数 IUPnPServiceCallback::StateVariableChanged
中,我们(从 UPnP 框架)只得到一个指向服务对象接口的指针。相反,服务对象没有返回指向宿主设备对象的指针的函数。因此,我们无法从其服务级别直接引用设备对象。在本文的下一部分,我将提出解决多个受控设备的回调对象(IUPnPServiceCallback
)管理问题的方案。
UPnPCpLib 框架
为了方便使用控制点 API,我创建了一个简单的框架,我希望 :),它能简化控制点应用程序的创建。它包含几个简单的类,定义了 UPnP 架构中最重要的对象,以及一组有助于完成控制点应用程序基本任务的实用函数、接口和结构。目前为止展示的所有示例都来自该框架的源文件,并且目前为止提出的所有问题都适用于该库的函数。
这个框架的主要思想是创建一个 UPnP 设备的模型,并提供处理这个模型的工具。该框架可用于构建控制台或 GUI 应用程序,以及原生 win32/MFC 或托管 WinForms 应用程序。主要的原生库由一个头文件 upnpcplib.h
和一个源文件 upnpcplib.cpp
组成。代码有注释,这应该会方便库的使用。对于 XML 文件的解析,我使用了 First Objective Software, Inc. 的 CMarkup 软件,遵循“CMarkup 评估许可协议”的条件。要编译该库,您必须将 markup.h
和 markup.cpp
文件添加到应用程序项目中。该库在 CMarkup 软件中使用 STL 字符串,因此您应在项目属性中指定预处理器定义 MARKUP_STL
。我请求注意,UPnPCpLib 框架不是商业产品,可以在您自己的应用程序中根据 CPOL 许可的规则使用,条件是遵守“CMarkup 评估许可协议”的条款。
我将在这里简要介绍库的关键元素,但我鼓励有兴趣的人查看源代码。在简短描述之后,我将给出示例。为了补充以下描述中包含的信息,请熟悉库源文件中的注释和实现细节。
UPnPCpLib 库包含四个主要类,定义了 UPnP 架构的四个最重要对象。
- FindManager - 指的是设备查找器对象。
- 设备 - 对应 UPnP 设备。
- Service - 对应 UPnP 设备的 service 对象。
- Action - 对应服务的动作
此外,还包含两个辅助类
- DevFinderCallback - 实现
IUPnPDeviceFinderCallback
接口。 - SrvEventCallback - 实现
IUPnPServiceCallback
接口。
此外,它还包含结构体 DocAccessData
,有助于解析描述文档。
下面是库对象之间关系的两个通用示意图。第一个示意图展示了一个集中式模型,其关键特征是设备集合由 FindManager 对象管理。

第二张图展示了一个分散模型,其关键特征是设备集合由 FindManager 外部的一个实现了 IFinderCallbackClient 接口的类的对象来管理。

FindManager 类
FindManager
是主类,用于管理设备查找器对象。它会自动创建和销毁查找器对象,并控制发现过程。它是唯一一个您可以自行创建对象的类。其余的对象由框架自动创建,并可通过其他对象的方法访问。
// ============== FindManager class ============== // // class for manage IUPnPFinder* object and devices collection. // To manage external device's collection outside this class // create your own class implementing IFinderCallbackClient interface // then call FindManager::Init passing pointer to your own object. // Else, pass pointer to client of type IFinderManagerClient // to manage collection internally and notify client about events. class FindManager : public IFinderCallbackClient, public IProcessDevice { public: FindManager(); virtual ~FindManager(); // devices collection will be managed internaly. // pass interface of client which will be notified about events related to collection bool Init(IFinderManagerClient* client, const wstring& devicetype = L"upnp:rootdevice"); // external collection. // pass pointer to your own class implementing IFinderCallbackClient. bool Init(IFinderCallbackClient* client, const wstring& devicetype = L"upnp:rootdevice"); // starts new search bool Start(); // stops current search bool Stop(); // access to devices collection // if collection is managed externally then throws exception const DeviceArray* GetCollection() const; DeviceArrayIterator GetCollectionBegin() const; DeviceArrayIterator GetCollectionEnd() const; const Device* GetDevice(unsigned int index) const; const Device* operator[] (unsigned int index) const; int GetCollectionCount() const; bool IsCollectionEmpty() const; // current search identifier long GetFindId(); // when devices collection is managed externally in your own class // then you should manually add callback to services events using Service::SetCallbackClient. // this function is used only if FindManager manages devices collection. // adds callback to services and notify given client about events related to service. // if client will be set to null pointer then callback won't be added to the collection // and client won't be notified about events. void SetServiceEventClientPtr(IServiceCallbackClient* client); // IFinderCallbackClient implementation // calls EnterCriticalSection and LeaveCriticalSection void Lock(); void UnLock(); // standard UPnP devices types static const wchar_t* device_type[]; static wstring GetRootDeviceType(); static int TypesCount(); private: bool Init(const wstring& devicetype); // releases finder callback void ReleaseCallback(); // IProcessDevice implementation // for assign callback's client to each service object in devices tree virtual void ProcessDevice(const Device* dev, void* param, int procid); // IFinderCallbackClient implementation virtual void DeviceAdded(long findid, IUPnPDevice* idev); virtual void DeviceRemoved(long findid, const wstring& devname); virtual void SearchComplete(long findid); void RemoveAllDevices(); private: DevFinderCallback* _findercallback; // IUPnPDeviceFinderCallback implementation IUPnPDeviceFinder* _ifinder; // IUPnPDeviceFinder interface DeviceArray _devs; // device's collection long _finderhandle; // IUPnPDeviceFinder find handle IFinderManagerClient* _findermanagerclient; // pointer to client receiving events related to changes // in devices collection managed internally by FindManager IFinderCallbackClient* _findercallbackclient; // pointer to client which manages devices collection IServiceCallbackClient* _srveventclient; // pointer to client receiving services events bool _externalcollection; // true if collection is managed externally CRITICAL_SECTION _cs; // for synchronize access to devices collection FindManager(const FindManager& srcobj) {} FindManager& operator= (const FindManager& srcobj) {return *this;} };
使用 FindManager
非常简单。创建对象后,只需调用适当的 Init
重载函数,然后调用 Start
。Start
和 Stop
函数分别运行和停止设备发现过程。FindManager
允许管理一个内部的已检测设备集合(Device
类的对象)。您也可以将由 FindManager
发现的设备集合存储在外部,在您自己的实现了 IFinderCallbackClient
接口的类中。如果我们希望 FindManager
管理设备集合,那么我们应该将指向 IFinderManagerClient
对象的指针传递给 FindManager::Init
函数。要访问内部集合,应调用 GetCollection
或 GetDevice
函数。管理集合的 FindManager
可以与实现 IFinderManagerClient
接口的客户端协作,这样客户端就会被通知发现过程的开始和结束,以及从集合中添加和移除对象的事件。FindManager
内部集合中的 Devices
对象接收物理 UPnP 设备中生成的事件通知。FindManager
可以将这些通知传递给实现了 IServiceCallbackClient
接口的外部客户端。但是,在此之前,您必须使用 FindManager::SetServiceEventClientPtr
函数传递一个指向客户端对象的指针。
如果我们希望 FindManager
只管理设备查找器对象,并且我们想将发现的设备集合放在外部,在您自己的类中,那么它应该实现 IFinderCallbackClient
接口。然后,在创建 FindManager
对象之后,只需调用其函数 Init
,并传递一个指向您自己类的对象的指针。
FindManager
使用 DevFinderCallback
类从 UPnP 框架(设备查找器对象)接收网络中设备的搜索结果。
Device 类
Device
是除 FindManager
之外的第二个类,定义了 UPnP 架构的基本元素之一——UPnP 设备。该类根据 UPnP 论坛组织的设备架构标准,反映了设备的树状结构及其功能。
// class representing UPnP device class Device { public: explicit Device(IUPnPDevice* idev, const Device* parentdev = 0); ~Device(); wstring GetUDN() const; wstring GetFriendlyName() const; wstring GetType() const; // access to list of services int GetServiceListCount() const; ServiceIterator GetServiceListBegin() const; ServiceIterator GetServiceListEnd() const; const Service* GetService(unsigned int index) const; // access to list of devices int GetDeviceListCount() const; DeviceIterator GetDeviceListBegin() const; DeviceIterator GetDeviceListEnd() const; const Device* GetDevice(unsigned int index) const; bool IsDeviceListEmpty() const; // access to list of icons int GetIconsListCount() const; IconIterator GetIconsListBegin() const; IconIterator GetIconsListEnd() const; const IconParam& GetIcon(unsigned int index) const; bool IsIconsListEmpty() const; // gets device icon URL // passing width & height equals to 0 function returns icon with standard parameters, // if it exists (mimetype = image/png). // otherwise icon is returned as specified, if any exists. // works on root device wstring GetDeviceIconURL(int iwidth, int iheight, int idepth) const; const Device* GetParentDevice() const; const Device* GetRootDevice() const; bool IsRoot() const; // with AddRef, don't forget to release interface when unused void GetInterface(IUPnPDevice** idev) const; // returns structure contains data which helps manipulate descr documents const DocAccessData* GetAccessData() const; // retrieves description document url from this UPnP device wstring GetDevDocAccessURL() const; // retrieves description document content from this UPnP device wstring GetDeviceDocument() const; // retrieves info about this UPnP device bool GetDeviceInfo(InfoData& data) const; // recursive process device object tree // see above instructions about IProcessDevice void EnumerateDevices(IProcessDevice* iproc, void* param, int procid) const; private: // enumerates member devices of this UPnP device // and stores members in collection // calls EnumSrv bool EnumDev(); // enumerates member services of this UPnP device // and stores collection in corresponding device object bool EnumSrv(); // creates new device object and add one to collection // with AddRef() void AddDevice(IUPnPDevice* idev); // creates new service object and add one to collection // with AddRef() void AddService(IUPnPService* isrv); bool SetUDN(); void GenerateFriendlyName(); wstring GetDocURL() const; bool SetType(); void RemoveAllDevices(); void RemoveAllServices(); private: IUPnPDevice* _idevice; // COM interface of UPnP device const Device* _parent; // parent device object wstring _udn; // Unique Device Name of UPnP device wstring _name; // friendly name of UPnP device wstring _type; // type of UPnP device ServiceList _services; // list of service objects representing hosted UPnP services DeviceList _devices; // list of device objects representing hosted UPnP devices DocAccessData _accessdata; // helper data to maniplulate description documents IconList _icons; // list of icon resources for UPnP device };
Device
类对象的结构。

Device
类包含三个集合
- DeviceList -
Device
类型对象的列表,代表成员设备, - ServiceList -
Service
类型对象的列表,代表成员服务, - IconList -
IconParam
类型结构的列表,包含图标参数。
此外,Device
包含有关设备名称的信息以及 DocAccessData
结构中有关描述文档的数据。它还存储一个指向相应设备对象接口的指针和指向父对象(如果不是根设备)的指针。
在 Service
对象中,存储了服务中可用动作对象的集合。关于 Service
类的更多信息稍后介绍。
Device
有许多用于操作上述集合的函数:添加、删除和枚举项。它还有一个 EnumerateDevices
函数,可以递归地枚举成员设备树结构中的对象。使用此函数必须与实现 IProcessDevice
接口(客户端)的类的对象协作,其函数 ProcessDevice
将在枚举期间被调用,以便将指向当前枚举的设备的指针传递给它进行处理。枚举可以从树的任何节点开始,并向结构深处继续。您可以向其传递任何对象,该对象将与设备对象一起处理。此外,您可以传递一个通用目的的整数类型参数,例如,用于控制函数的行为。
Service 类
Service
类代表 UPnP 设备的 service 对象。
// class representing UPnP service class Service { friend class Device; public: ~Service(); // access to list of actions // each service may have 0 or more actions int GetActionCount() const; ActionIterator GetCollectionBegin() const; ActionIterator GetCollectionEnd() const; const Action& GetAction(unsigned int index) const; const Action& GetAction(const wstring& aname) const; wstring GetServiceID() const; wstring GetServiceTypeID() const; // if returns zero then error occurred long GetLastTransportStatus() const; const Device& GetParentDevice() const; // with AddRef, don't forget to release interface when unused void GetInterface(IUPnPService** isrv) const; // adds callback for events and sets its client bool SetCallbackClient(IServiceCallbackClient* iclient); // returns descr document url of this service wstring GetScpdURL() const; // returns descr document content of this service wstring GetScpdContent() const; // returns structure contains data which helps manipulate descr documents const DocAccessData* GetAccessData() const; // retrieves info about this UPnP service bool GetServiceInfo(InfoData& data) const; // retrieves info about service's state variables // each service must have one or more state variables bool GetServiceVariables(VarData& data) const; private: Service(IUPnPService* isrv, const Device& parentdev); // retrieves scpd info, url and document content bool SetAccessData(); // reads actions names bool EnumActions(); bool SetServiceID(); wstring _name; // service Id wstring _typeid; // service type Id ActionList _actions; // list of UPnP service actions const Device& _parent; // parent device object IUPnPService* _iservice; // COM interface of UPnP service DocAccessData _accessdata; // uri and content of document describing UPnP service SrvEventCallback* _isrvcback; // IUPnPServiceCallback object Service(const Service& srcobj) : _parent(srcobj._parent) {} Service& operator= (const Service& srcobj) {return *this;} };
Service
类包含一个属于它所代表的服务的动作对象列表(ActionList
),并且还存储了服务描述文档的 URL 和内容,其中存储了执行该动作所需的所有数据。
每个 Service
对象都会创建自己的 SrvEventCallback
类实例(实现了 IUPnPServiceCallback
接口)来接收来自服务的事件通知。收到的通知会进一步发送给回调对象的客户端,即发送给实现了 IServiceCallbackClient
接口的类的对象。回调对象(SrvEventCallback
)从 UPnP 框架接收到关于服务中事件的通知时,同时也会收到一个指向服务对象(IUPnPService*
)的指针。但是,在将通知进一步传递给客户端时,会传递一个指向 Service
对象(其宿主)的指针,而不是收到的指针,以便客户端识别生成事件的设备。SrvEventCallback
对象的客户端在处理通知时,可以根据收到的指向 Service
对象的指针及其内部可用的指向父对象(Device
)的指针来确定事件的来源。
Action 类
Action
对象包含关于其输入参数的信息,并有一个 Invoke
函数,该函数在父服务上调用相应的动作。
// class representing member action of UPnP service class Action { friend class Service; public: // name of this action wstring GetName() const; const Service& GetParentService() const; // number of arguments (input & output) // returns -1 if error occured int GetArgsCount(); // number of input arguments // returns -1 if error occured int GetInArgsCount(); // retrieves this action info bool GetInfo(/*out*/InfoDataList& inflist) const; // sets array of input arguments types and values bool SetInArgs(const StrList& args); // sets value of argument at specified index in input arguments array bool SetInArgs(const wstring& arg, unsigned int index); // gets array of input arguments types and values bool GetInArgs(ArgsArray& args); // gets input argument at specified index in input arguments array bool GetInArgs(InfoDataItem& arg, unsigned int index); // invokes this action on parent UPnP service // argsout - array of values of output arguments or error message. // on error argsout contains only error messages of InvokeAction // return: -1 = error, >0 = number of output arguments + returned value (if any) // -2 = argsout contains only returned value int Invoke(ArgsArray& argsout) const; private: wstring _name; // action's name const Service* _parent; // this action's parent service int _argcount; // number of input arguments int _inargcount; // number of all arguments, input and output bool _complete; // true if number of arguments has been retrieved bool _inargscomplete; // true if number of input arguments has been retrieved ArgsArray _in; // array of input arguments (types and values) bool SetArgsCount(); bool InitInArgsList(); Action(const Service* srv, const wstring& name); };
实用函数
CString GetErrorMessage(HRESULT hr);
根据检索到的 COM 错误代码返回错误描述。
VARTYPE GetVariantType(const wstring& vtype);
将状态变量(动作参数)的类型名称转换为相应的 VARIANT
类型。
wstring GetTypeDescr(VARTYPE vtype);
将状态变量类型的描述转换为变体类型。
bool IsICSConnEnabled();
检查 ICS 功能是否已启用,以及 ICS 是否使用公共和私有连接。如果 ICS 已开启,您不应手动为 UPnP 框架启用 Windows 防火墙例外。这会关闭所有网络接口(包括 ICS 公共接口)上 UPnP 端口的防火墙保护。这可能会将计算机直接暴露在互联网上。
int CheckFirewallPortState(long number, transport_protocol protocol);
检查防火墙中给定端口的状态。如果函数返回零,表示发生了错误。值为 1 表示端口已解除阻塞,而值为 -1 表示端口被阻塞。您应该将 udp_protocol
值传递给 UDP 协议的 protocol 参数,将 tcp_protocol
值传递给 TCP 协议。
bool ControlUPnPPorts(bool open);
在防火墙中解除或阻止 UPnP 框架使用的端口:2869 TCP 和 1900 UDP。要使函数生效,应用程序必须以管理员权限运行。
void CheckServiceState(const wstring& srvname, /*out*/srvstate& state);
以描述的形式检查并返回指定系统服务的状态。
bool ControlSSDPService(bool start);
启动或停止 ssdpsrv 系统服务。要使函数生效,应用程序必须以管理员权限运行。
示例
众所周知,最好的学习方式是通过实例。所以现在是时候展示一些 UPnPCpLib 库的应用实例了。我将向您展示如何在 Win32 控制台应用程序中使用该框架,因为在这种情况下分析代码会更容易。显然,在 GUI 应用程序(Win32、MFC 或 WinForms)中使用该库会更有效。在本文的下一部分,将介绍在 WinForms 托管应用程序中使用该库的示例。所有展示的示例都可以以 Visual Studio 2003 项目的形式下载,这些项目可以轻松转换为 2008 版本。
// sample 1 // In this sample FindManager manages internal devices collection #define _WIN32_DCOM #include <iostream> #include "upnpcplib.h" using namespace UPnPCpLib; using namespace std; // This class hosts FindManager object and uses its ability to manage // devices collection, thus implements IFinderManagerClient interface // to receive notifications from FindManager about changes in collection // and start/stop events. To receive notifications about events // from device's services this class implements IServiceCallbackClient. class FinderClient : public IFinderManagerClient, public IServiceCallbackClient, public IProcessDevice { public: FinderClient() : _searching(false) { _fm = new FindManager(); // create new FindManager object // creates IUPnPDeviceFinderCallback object, // pointer to this class is passed, to manage devices internally // and receive notifications from FindManager _fm->Init(this); // sets receiver (this class) of services events _fm->SetServiceEventClientPtr(this); wcout << L"\nFinderClient was created successfully" << flush; } ~FinderClient() { if(IsSearching()) StopSearch(); // destroy FindManager object delete _fm; // when all objects were destroyed then // post WM_QUIT message to message loop, to finish it PostQuitMessage(0); wcout << L"\nFinderClient has been destroyed" << flush; } bool StartSearch() { // start finding devices // with default argument "upnp:rootdevice" return (_searching = _fm->Start()); } bool StopSearch() { // stop finding devices return !(_searching = !_fm->Stop()); } bool IsSearching() {return _searching;} void InvokeAction(unsigned int devindex, const wstring& actionName, const StrList& args) { _action = 0; // find service related to given action name // and if found then save pointer to requested action object. // see ProcessDevice function. const Device* dev = _fm->GetDevice(devindex); dev->EnumerateDevices(this, (void*)&actionName, 0); // if action found then invoke it if(_action != 0) { wcout << L"\nInvoke action: " << actionName << endl; wstring result; ArgsArray argsout; const_cast<Action*>(_action)->SetInArgs(args); _action->Invoke(argsout); for(ArgsArray::size_type i = 0; i < argsout.size(); ++i) result.append(argsout[i].second).append(L"\n"); wcout << L"action result:" << endl; // write result to console wcout << result; } else wcout << L"\nAction not found" << endl; } private: // IProcessDevice interface implementation. // This interface is implemented to use Device::EnumerateDevices function virtual void ProcessDevice(const Device* dev, void* param, int procid) { const wstring& actname = *(const wstring*)param; ServiceIterator srvi = dev->GetServiceListBegin(); int srvcount = dev->GetServiceListCount(); // enumerate member services of given device for(int i = 0; i < srvcount; ++i, ++srvi) { // check if current service contains requested action // if so, then save pointer. // btw, sorry for this "exceptional coding" style :) try { _action = &(*srvi)->GetAction(actname); break; } catch(...) { } } } // IFinderManagerClient interface implementation virtual void OnStartFindDevice(long findid) { // called by FindManager when finding was started successfully wcout << L"\nSearching started" << flush; } virtual void OnStopFindDevice(long findid, bool iscancelled) { // called by FindManager when finding was stopped successfully // display reason of calling wcout << L"\nSearching stopped because of " << (iscancelled ? L"cancel" : L"search complete") << flush; if(!iscancelled) { // when search is complete (not cancelled) // let's break loop and finish application wcout << L"\nSearching will be cancelled" << flush; this->StopSearch(); } } virtual void OnAddDevice(long findid, const Device* dev, int devindex) { // called by FindManager when device object has been added to collection // display device name wcout << L"\nDevice added: " << dev->GetFriendlyName() ; // display passed device object properties InfoData devdata; if(dev->GetDeviceInfo(devdata)) { for(InfoIterator ii = devdata.begin(); ii != devdata.end(); ++ii) wcout << endl << (*ii).first << L" = " << (*ii).second; } wcout << L"\nSearching is being continued" << flush; } virtual void OnRemoveDevice(long findid, const wstring& devudn, const wstring& friendlyname, int removedindex) { // called by FindManager when device object has been removed from collection // display removed device name wcout << L"\nDevice removed: " << friendlyname << L"\nSearching is being continued" << flush; } // IServiceCallbackClient interface implementation virtual void ServiceEventVariableChanged(const Service* srv, const wstring& varname, const wstring& varvalue) { // called by service callback when value of state variable has been changed // get pointer to parent device wstring devparent = srv->GetParentDevice().GetFriendlyName(); // display device's name, service's id, name of state variable // and its current value wcout << L"\n==> Event fired:\n\tfrom service id: " << srv->GetServiceID() << L"\n\tparent device: " << devparent << L"\n\tsource state variable name: " << varname << L"\n\tcurrent state variable value: " << varvalue << L"\nSearching is being continued" << flush; } virtual void ServiceEventInstanceDied(const Service* srv) { // called by service callback when service is not responding wcout << L"\nService died: " << srv->GetServiceID() << "\nIt was member of device: " << srv->GetParentDevice().GetFriendlyName() << "\nSearching is being continued" << flush; } private: bool _searching; // helps break message loop FindManager* _fm; // object of class from UPnPCpLib library const Action* _action; // requested action name }; int main() { // initialize COM library. // in console application, COM library is initialized // to use single threaded concurrency model if(CoInitializeEx(0, COINIT_APARTMENTTHREADED) != S_OK) { CoUninitialize(); return -1; } wcout << L"\nCOM library initialized"; // initialize Winsock library WSADATA wdata; WSAStartup(MAKEWORD(2,2), &wdata); wcout << L"\nWinsock library initialized"; wcout << L"\nCreating FinderClient object" << flush; // create FinderClient object which hosts FinderManager FinderClient* fclnt = new FinderClient(); // start finding all root devices wcout << L"\nStart finding\n" << flush; if(fclnt->StartSearch()) { // process messages MSG Message; while(GetMessage(&Message, 0, 0, 0)) { DispatchMessage(&Message); wcout << '.' << flush; // when FinderClient receives "search complete" notification // its function IsSearching returns false // and FinderClient will be destroyed and application finished if(!fclnt->IsSearching()) { // let's execute some actions wstring name; StrList args; // action without input arguments name = L"GetExternalIPAddress"; fclnt->InvokeAction(0, name, args); // it has one input argument of type ui2 name = L"GetGenericPortMappingEntry"; args.push_back(L"0"); // entry's index fclnt->InvokeAction(0, name, args); wcout << L"\nFinderClient finished work and will be destroyed\n" << flush; // in FinderClient destructor WM_QUIT message is posted // to break this loop delete fclnt; fclnt = 0; } } } delete fclnt; wcout << L"\nCleaning up Winsock library"; // free Winsock library WSACleanup(); wcout << L"\nCleaning up COM library"; // free COM library CoUninitialize(); wcout << L"\nExiting... bye" << flush; return 0; }
上述程序可以在控制台打印出以下结果
COM library initialized Winsock library initialized Creating FinderClient object FinderClient was created successfully Start finding Searching started............ Device added: Wireless-G ADSL Home Gateway (uuid:8c59ad37-1dd2-11b2-ada1-0018398b6752) Description = LINKSYS WAG200G Router Document URL = http://10.0.0.1:49152/gateway.xml Friendly name = Wireless-G ADSL Home Gateway (uuid:8c59ad37-1dd2-11b2-ada1-0018398b6752) Manufacturer = LINKSYS Manufacturer URL = http://www.linksys.com/ Model = Wireless-G ADSL Home Gateway Model URL = http://www.linksys.com/ Model number = WAG200G Presentation URL = http://10.0.0.1/index.htm Serial number = 123456789 Type = urn:schemas-upnp-org:device:InternetGatewayDevice:1 UDN = uuid:8c59ad37-1dd2-11b2-ada1-0018398b6752 UPC = WAG200G Searching is being continued......... ==> Event fired: from service id: urn:upnp-org:serviceId:L3Forwarding1 parent device: Wireless-G ADSL Home Gateway (uuid:8c59ad37-1dd2-11b2-ada1-0018398b6752) source state variable name: DefaultConnectionService current state variable value: Searching is being continued........... ==> Event fired: from service id: urn:upnp-org:serviceId:WANCommonIFC1 parent device: Internet Connection Sharing (uuid:8c59ad36-1dd2-11b2-ada1-0018398b6752) source state variable name: PhysicalLinkStatus current state variable value: Up Searching is being continued........... ==> Event fired: from service id: urn:upnp-org:serviceId:WANEthLinkC1 parent device: Internet Connection Sharing (uuid:8c59ad37-1dd2-11b2-ada0-0018398b6752) source state variable name: EthernetLinkStatus current state variable value: Up Searching is being continued........... ==> Event fired: from service id: urn:upnp-org:serviceId:WANPPPConn1 parent device: Internet Connection Sharing (uuid:8c59ad37-1dd2-11b2-ada0-0018398b6752) source state variable name: PossibleConnectionTypes current state variable value: IP_Routed Searching is being continued......... ==> Event fired: from service id: urn:upnp-org:serviceId:WANPPPConn1 parent device: Internet Connection Sharing (uuid:8c59ad37-1dd2-11b2-ada0-0018398b6752) source state variable name: ConnectionStatus current state variable value: Connected Searching is being continued......... ==> Event fired: from service id: urn:upnp-org:serviceId:WANPPPConn1 parent device: Internet Connection Sharing (uuid:8c59ad37-1dd2-11b2-ada0-0018398b6752) source state variable name: X_Name current state variable value: Local Area Connection Searching is being continued......... ==> Event fired: from service id: urn:upnp-org:serviceId:WANPPPConn1 parent device: Internet Connection Sharing (uuid:8c59ad37-1dd2-11b2-ada0-0018398b6752) source state variable name: ExternalIPAddress current state variable value: 57.153.248.16 Searching is being continued......... ==> Event fired: from service id: urn:upnp-org:serviceId:WANPPPConn1 parent device: Internet Connection Sharing (uuid:8c59ad37-1dd2-11b2-ada0-0018398b6752) source state variable name: PortMappingNumberOfEntries current state variable value: 2 Searching is being continued..... Searching stopped because of search complete Searching will be cancelled Searching stopped because of cancel. Invoke action: GetExternalIPAddress action result: 57.153.248.16 Invoke action: GetGenericPortMappingEntry action result: 11599 TCP 11599 10.0.0.2 True BitComet (10.0.0.2:11599) 11599 TCP 0 FinderClient finished work and will be destroyed FinderClient has been destroyed Cleaning up Winsock library Cleaning up COM library Exiting... bye
以及下一个示例
// sample 2 // In this sample devices collection is managed outside of FindManager #define _WIN32_DCOM #include <iostream> #include "upnpcplib.h" using namespace UPnPCpLib; using namespace std; // This class hosts FindManager object and DON'T uses its ability to manage // devices collection. Instead, this class on its own is managing the collection. // In this case FindManager is used only for finding devices. // Thus this class implements IFinderCallbackClient interface // to receive notifications from UPnP framework about found devices. // To receive notifications about events from device's services // this class implements IServiceCallbackClient. class FinderHost : public IFinderCallbackClient, public IServiceCallbackClient, public IProcessDevice { public: FinderHost() : _searching(false) , _finish(false) { // create IUPnPDeviceFinderCallback object // and pass pointer to FinderHost which will be managing devices collection _fm = new FindManager(); _fm->Init(this); wcout << L"FinderHost was created successfully" << endl; } ~FinderHost() { if(IsSearching()) StopSearch(); // remove devices collection RemoveAllDevices(); // destroy FindManager object delete _fm; // when all objects were destroyed then // post WM_QUIT message to message loop, to finish it //PostQuitMessage(0); wcout << L"\nFinderHost has been destroyed" << endl; } bool StartSearch() { // start finding devices // with default argument "upnp:rootdevice" return (_searching = _fm->Start()); } bool StopSearch() { // stop finding devices return !(_searching = !_fm->Stop()); } bool IsSearching() {return _searching;} void SetFinish() {_finish = true;} bool IsFinished() {return _finish;} // IFinderCallbackClient implementation // in single threading model it's unneccessary to synchronize access // to devices collection, thus function bodies are empty void Lock() {} void UnLock() {} void InvokeAction(int devindex, const wstring& actionName, const StrList& args) { _action = 0; // find service related to given action name // and if found then save pointer to requested action object. // see ProcessDevice function. const Device* dev = _devs[devindex]; dev->EnumerateDevices(this, (void*)&actionName, PID_FIND_ACTION); // if action found then invoke it if(_action != 0) { wcout << L"\nInvoke action: " << actionName << flush; wstring result; ArgsArray argsout; const_cast<Action*>(_action)->SetInArgs(args); _action->Invoke(argsout); for(ArgsArray::size_type i = 0; i < argsout.size(); ++i) result.append(argsout[i].second).append(L"\n"); wcout << L"\naction result:" << endl; // write result to console wcout << result << endl; } else wcout << L"\nAction not found" << endl; } private: // identifiers used in ProcessDevice static const int PID_ADD_SERVICE_CALLBACK = 0; static const int PID_FIND_ACTION = 1; // IProcessDevice interface implementation. // This interface is implemented for use Device::EnumerateDevices function virtual void ProcessDevice(const Device* dev, void* param, int procid) { switch(procid) { // for add pointer to client of services callbacks case PID_ADD_SERVICE_CALLBACK: { ServiceIterator srvi = dev->GetServiceListBegin(); int srvcount = dev->GetServiceListCount(); for(int i = 0; i < srvcount; ++i, ++srvi) const_cast<Service*>(*srvi)->SetCallbackClient(this); } break; // for find service containing given action name case PID_FIND_ACTION: { const wstring& actname = *(const wstring*)param; ServiceIterator srvi = dev->GetServiceListBegin(); int srvcount = dev->GetServiceListCount(); // enumerate member services of given device for(int i = 0; i < srvcount; ++i, ++srvi) { // check if current service contains requested action // if so, then save pointer. // btw, sorry for this "exceptional coding" style :) try { _action = &(*srvi)->GetAction(actname); break; } catch(...) { } } } break; } } // IFinderCallbackClient interface implementation virtual void DeviceAdded(long findid, IUPnPDevice* idev) { // called by UPnP framework when the new device was found // create new root Device object and build its structure Device* dev = new Device(idev); // add callback for services events dev->EnumerateDevices(this, 0, PID_ADD_SERVICE_CALLBACK); // add Device root object to collection _devs.push_back(dev); wcout << L"\nNew device added: " << dev->GetFriendlyName() << endl; // display info about added device InfoData devdata; if(dev->GetDeviceInfo(devdata)) { for(InfoIterator ii = devdata.begin(); ii != devdata.end(); ++ii) wcout << endl << (*ii).first << L" = " << (*ii).second; } wcout << L"\nSearching is being continued" << flush; } virtual void DeviceRemoved(long findid, const wstring& devname) { // called by UPnP framework when the device was removed from collection for(DeviceArray::size_type i = 0; i < _devs.size(); ++i) { if(devname == _devs[i]->GetUDN()) { wstring friendlyname = _devs[i]->GetFriendlyName(); // remove passed device from devices collection delete _devs[i]; _devs.erase(_devs.begin() + i); wcout << L"\nDevice was removed: " << friendlyname << L"\nSearching is being continued" << flush; break; } } } virtual void SearchComplete(long findid) { // called by UPnP framework when finding was completed successfully wcout << L"\nSearch is complete" << endl; // when search is complete (not cancelled) // let's break loop and finish application wcout << L"Searching will be cancelled" << endl; this->StopSearch(); } // IServiceCallbackClient interface implementation virtual void ServiceEventVariableChanged(const Service* srv, const wstring& varname, const wstring& varvalue) { // called by service callback when value of state variable has been changed // get parent device name wstring devparent = srv->GetParentDevice().GetFriendlyName(); // display device's name, service's id, name of state variable // and its current value wcout << L"\n==> Event fired:\n\tfrom service id: " << srv->GetServiceID() << L"\n\tparent device: " << devparent << L"\n\tsource state variable name: " << varname << L"\n\tcurrent state variable value: " << varvalue << endl; if(_searching) wcout << L"Searching is being continued" << flush; // when all notifications are received then // post WM_QUIT message to message loop, to finish it if(_finish) PostQuitMessage(0); } virtual void ServiceEventInstanceDied(const Service* srv) { // called by service callback when service is not responding wcout << L"\nService died: " << srv->GetServiceID() << L"\nIt was member of device: " << srv->GetParentDevice().GetFriendlyName() << L"\nSearching is being continued" << flush; } void RemoveAllDevices() { if(!_devs.empty()) { for(vector<Device*>::iterator di = _devs.begin(); di != _devs.end(); ++di) { delete *di; *di = 0; } _devs.clear(); } } private: bool _searching; // helps break message loop bool _finish; // helps break message loop FindManager* _fm; // object of class from UPnPCpLib library DeviceArray _devs; // devices collection const Action* _action; // requested action name }; int _tmain(int argc, _TCHAR* argv[]) { // initialize COM library. // in console application, COM library is initialized // to use single threaded concurrency model if(CoInitializeEx(0, COINIT_APARTMENTTHREADED) != S_OK) { CoUninitialize(); return -1; } wcout << L"COM library initialized" << endl; // initialize Winsock library WSADATA wdata; WSAStartup(MAKEWORD(2, 2), &wdata); wcout << L"Winsock library initialized\n" L"Creating FinderHost object" << endl; // create FinderHost object which hosts FinderManager FinderHost* fhost = new FinderHost(); // start finding all root devices wcout << L"Start finding" << flush; if(fhost->StartSearch()) { // process messages MSG Message; while(GetMessage(&Message, NULL, 0, 0)) { DispatchMessage(&Message); wcout << '.' << flush; // when FinderHost receives "search complete" notification // its function IsSearching returns false // and FinderHost will be destroyed and application finished if(!fhost->IsSearching() && !fhost->IsFinished()) { wcout << L"\nSearching has been cancelled successfully" << endl; // let's execute some actions wstring name; StrList args; // this action doesn't return any output arguments // but fires event from state variable PortMappingNumberOfEntries name = L"AddPortMapping"; args.push_back(L"any"); // remote host, type: string args.push_back(L"11200"); // external port, type: ui2 args.push_back(L"UDP"); // protocol, type: string args.push_back(L"11200"); // internal port, type: ui2 args.push_back(L"192.168.0.10"); // internal client, type: string args.push_back(L"true"); // enabled, type: boolean args.push_back(L"add port mapping example"); // description, type: string args.push_back(L"0"); // lease duration (0 = permanent), type: ui4 fhost->InvokeAction(0, name, args); // this action also doesn't return any output arguments // but fires event from state variable PortMappingNumberOfEntries name = L"DeletePortMapping"; args.clear(); args.push_back(L"any"); // remote host, type: string args.push_back(L"11200"); // external port, type: ui2 args.push_back(L"UDP"); // protocol, type: string fhost->InvokeAction(0, name, args); wcout << L"\nFinderHost finished work and will be destroyed" << endl; // in FinderHost ServiceEventVariableChanged WM_QUIT message is posted // to break this loop fhost->SetFinish(); //delete fhost; //fhost = 0; } } } delete fhost; wcout << L"Cleaning up Winsock library" << endl; // free Winsock library WSACleanup(); wcout << L"Cleaning up COM library" << endl; // free COM library CoUninitialize(); wcout << L"Exiting... bye" << endl; return 0; }
上述程序可以在控制台打印出以下结果
COM library initialized Winsock library initialized Creating FinderHost object FinderHost was created successfully Start finding............ New device added: Wireless-G ADSL Home Gateway (uuid:8c59ad37-1dd2-11b2-ada1-0018398b6752) Description = LINKSYS WAG200G Router Document URL = http://10.0.0.1:49152/gateway.xml Friendly name = Wireless-G ADSL Home Gateway (uuid:8c59ad37-1dd2-11b2-ada1-0018398b6752) Manufacturer = LINKSYS Manufacturer URL = http://www.linksys.com/ Model = Wireless-G ADSL Home Gateway Model URL = http://www.linksys.com/ Model number = WAG200G Presentation URL = http://10.0.0.1/index.htm Serial number = 123456789 Type = urn:schemas-upnp-org:device:InternetGatewayDevice:1 UDN = uuid:8c59ad37-1dd2-11b2-ada1-0018398b6752 UPC = WAG200G Searching is being continued......... ==> Event fired: from service id: urn:upnp-org:serviceId:L3Forwarding1 parent device: Wireless-G ADSL Home Gateway (uuid:8c59ad37-1dd2-11b2-ada1-0018398b6752) source state variable name: DefaultConnectionService current state variable value: Searching is being continued........... ==> Event fired: from service id: urn:upnp-org:serviceId:WANCommonIFC1 parent device: Internet Connection Sharing (uuid:8c59ad36-1dd2-11b2-ada1-0018398b6752) source state variable name: PhysicalLinkStatus current state variable value: Up Searching is being continued........... ==> Event fired: from service id: urn:upnp-org:serviceId:WANEthLinkC1 parent device: Internet Connection Sharing (uuid:8c59ad37-1dd2-11b2-ada0-0018398b6752) source state variable name: EthernetLinkStatus current state variable value: Up Searching is being continued........... ==> Event fired: from service id: urn:upnp-org:serviceId:WANPPPConn1 parent device: Internet Connection Sharing (uuid:8c59ad37-1dd2-11b2-ada0-0018398b6752) source state variable name: PossibleConnectionTypes current state variable value: IP_Routed Searching is being continued......... ==> Event fired: from service id: urn:upnp-org:serviceId:WANPPPConn1 parent device: Internet Connection Sharing (uuid:8c59ad37-1dd2-11b2-ada0-0018398b6752) source state variable name: ConnectionStatus current state variable value: Connected Searching is being continued......... ==> Event fired: from service id: urn:upnp-org:serviceId:WANPPPConn1 parent device: Internet Connection Sharing (uuid:8c59ad37-1dd2-11b2-ada0-0018398b6752) source state variable name: X_Name current state variable value: Local Area Connection Searching is being continued......... ==> Event fired: from service id: urn:upnp-org:serviceId:WANPPPConn1 parent device: Internet Connection Sharing (uuid:8c59ad37-1dd2-11b2-ada0-0018398b6752) source state variable name: ExternalIPAddress current state variable value: 57.154.192.91 Searching is being continued......... ==> Event fired: from service id: urn:upnp-org:serviceId:WANPPPConn1 parent device: Internet Connection Sharing (uuid:8c59ad37-1dd2-11b2-ada0-0018398b6752) source state variable name: PortMappingNumberOfEntries current state variable value: 2 Searching is being continued..... Search is complete Searching will be cancelled . Searching has been cancelled successfully Invoke action: AddPortMapping action result: Invoke action: DeletePortMapping ==> Event fired: from service id: urn:upnp-org:serviceId:WANPPPConn1 parent device: Internet Connection Sharing (uuid:8c59ad37-1dd2-11b2-ada0-0018398b6752) source state variable name: PortMappingNumberOfEntries current state variable value: 3 action result: FinderHost finished work and will be destroyed ..... ==> Event fired: from service id: urn:upnp-org:serviceId:WANPPPConn1 parent device: Internet Connection Sharing (uuid:8c59ad37-1dd2-11b2-ada0-0018398b6752) source state variable name: PortMappingNumberOfEntries current state variable value: 2 . FinderHost has been destroyed Cleaning up Winsock library Cleaning up COM library Exiting... bye
WinForms 控制点应用程序 (Finder.net)
最后,我想展示一个用 C# 编写的、带有 Windows Forms GUI 的控制点应用程序示例。这个 .net 应用程序通过一个 .net 类库(FindManager.net dll)来使用原生的 UPnPCpLib 库,该类库包装了原生代码。然而,由于代码量比上面列出的简单 Win32 示例要大得多,我将展示应用程序的屏幕截图并介绍其功能,而不是代码。应用程序和库的源代码可以以 Visual Studio 2008 项目(Finder.net 和 FindManager.net)的形式下载。此外,还附有应用程序的可执行文件。除了 WinForms 应用程序,MFC 版本也可以下载(Finder)。更多关于编程控制点的示例可以在 MSDN [1] 和 Platform SDK 中找到(默认路径为 %ProgramFiles%\Microsoft Platform SDK\Samples\NetDS\upnp\GenericUCP\cpp)。此外,您还可以从英特尔网站 [11] 下载许多代码示例、工具和其他辅助资料。
在示例 WinForms 应用程序中,我试图尽可能多地涵盖 UPnP 控制点的功能范围。我的意图也是创建一个既简单又有用的应用程序。我是否成功实现了这个意图?让读者(用户)来决定吧。
应用程序的功能
- 按类型发现设备(异步),
- 创建与已发现设备的结构和功能相对应的对象,
- 管理设备对象集合,
- 展示对象结构,
- 显示有关设备对象元素的信息,
- 通过动作控制设备,
- 接收服务生成的事件通知,
- 查看和保存描述文档,
- 打开与设备和服务相关的 URL,
- 管理 ssdpsrv 系统服务,
- 管理 ssdpsrv 服务使用的端口,
- 编辑系统注册表中的配置项。
该程序搜索指定类型的设备。如果您选择类型“upnp:rootdevice”,搜索范围将涵盖所有基本类型的设备。在图片中,我们可以看到程序找到了几种不同类型的设备。我们还可以看到(显示在“事件日志”窗口中)来自检测到新设备时收到的事件的信息。

将要搜索的设备类型可以通过过滤器来指定。

让我们以一个互联网网关设备(路由器)为例,看看如何调用动作。这些动作涉及“端口转发”功能的配置。首先,我向端口映射表添加一个新条目。

步骤顺序已在图中标出。(1) 选择动作名称,(2) 写入输入参数的值(参数的名称和类型显示在两个标记的窗口中)。(3) 执行动作后,在“事件日志”窗口中,检查动作状态(200 OK)和表中当前的条目数——“变量值:3”。在确定参数类型时,上面的表格可能会很有用。
现在,通过指定条目编号来检查映射表中新条目的内容。之前我们读取了条目数(3个条目),因此最后一个将是编号 2(第一个是编号 0)。

在动作的响应中,我们收到了所需条目的内容——在“属性”窗口的“输出参数”类别下,或在“动作的参数”窗口中。
最后,删除先前添加的条目的操作。

这个场景与添加条目的情况类似。(1) 选择相应的动作后,(2) 在“属性”窗口中写入输入参数的值。(3) 调用动作后,在事件窗口中检查动作调用的状态(200 OK)和当前的条目数——“变量值:2”。
在选项窗口中,您可以更改存储在系统注册表中的 UPnP 框架的配置参数。

UPnPCpLib 库的托管包装器 (FindManager.net)
FindManager.net 是一个封装了原生 UPnPCpLib 库的类库。这个类库可以与任何 .net 应用程序一起使用。要做到这一点,只需添加对 FindManager.net.dll 的引用并声明“FindManagerWrapper”命名空间名称。该类库由四个主要类组成
- FindManagerNet,
- DeviceNet,
- ServiceNet,
- ActionNet。
这些类对应于它们的原生“兄弟”,分别是 FindManager、Device、Service 和 Action。基本类是 FindManagerNet,它用作设备查找器并管理设备对象的集合。关于这些类的一般功能的详细信息包含在上面对 UPnPCpLib 框架的描述中。
FindManagerNet 类
- 构造函数
FindManagerNet() | 默认构造函数。适用于当 COM 库已初始化为使用单线程模型时的控制台应用程序。建议避免在 GUI 应用程序中使用此构造函数。 |
FindManagerNet(SynchronizationContext) | 在 GUI 应用程序中,当 COM 库初始化为使用多线程模型时,请使用此构造函数。在这种情况下,由 UPnP 框架调用的库函数将在单独的线程中执行。多亏了这个构造函数,在单独线程中执行的事件处理程序可以安全地访问在主 GUI 线程中创建的控件,而无需使用 InvokeRequired。 |
- 属性
Device[int] | 从集合中返回指定索引处的设备对象。 |
CollectionSize | 获取集合中设备的数量。 |
IsCollectionEmpty | 检查集合中是否有任何设备。 |
FindId | 获取搜索会话标识符。 |
DeviceTypeCount | 获取数组中设备类型名称的数量。(静态) |
DeviceTypes | 返回设备类型名称的数组。(静态) |
RootDeviceType | 获取设备的基本类型名称。(静态) |
DeviceType[int] | 获取数组中指定索引处的类型名称。(静态) |
- 方法
Init(String) | 初始化 FindManager 对象,并在搜索设备时使用指定的设备类型。如果成功,则返回 true。 |
Init() | 初始化 FindManager 对象,并在搜索设备时使用 "upnp:rootdevice" 类型的设备。将搜索所有基本类型。如果成功则返回 true。 |
Start | 开始搜索。如果成功则返回 true。 |
停止 | 取消搜索。如果成功则返回 true。 |
GetEnumerator | 返回设备集合的枚举器。 |
GetCollection | 返回检测到的设备集合。 |
- 事件
OnStartFind | 当设备搜索开始时发生。向处理程序传递 StartFindArgs 类型的参数,其中包含有关搜索会话标识符的信息。 |
OnStopFind | 当设备搜索被取消时发生。向处理程序传递 StopFindArgs 类型的参数,其中包含搜索会话标识符和取消搜索原因的信息。 |
OnAddDevice | 当新设备添加到集合时发生。向处理程序传递 AddDeviceArgs 类型的参数,其中包含搜索会话的标识符、设备对象以及此对象在集合中的索引。 |
OnRemoveDevice | 当设备从集合中移除时发生。向处理程序传递 RemoveDeviceArgs 类型的参数,其中包含搜索会话标识符、设备对象的唯一名称和友好名称,以及此对象在集合中的最后索引。 |
OnServiceVariableChanged | 当状态变量的值发生变化时发生。向处理程序传递 ServiceVariableChangedArgs 类型的参数,其中包含服务对象以及状态变量的名称和值。 |
OnServiceInstanceDied | 当服务无响应时发生。向处理程序传递 ServiceInstanceDiedArgs 类型的参数,其中包含服务对象。 |
- 静态辅助方法
GetErrorMessage | 将动作调用的 hresult 转换为消息字符串。(HRESULT 到字符串) |
GetVariantType | 将状态变量类型的描述转换为变体类型。(string 到 VARTYPE) |
GetTypeDescription | 将变体类型转换为状态变量类型的描述。(VARTYPE 到 string) |
IsICSConnectionEnabled | 检查互联网连接共享(ICS)是否已启用连接。 |
CheckSystemServiceState | 检查指定的系统服务状态。 |
ControlSSDPService | 启动或停止 ssdp 系统服务。 |
CheckFirewallPortState | 检查指定的防火墙端口状态。返回:0 - 错误,1 - 启用,-1 - 禁用。 |
ControlUPnPPorts | 打开或关闭 UPnP 端口 (2869 TCP, 1900 UDP)。 |
DeviceNet 类
- 属性
UniqueName | 返回设备的唯一设备名称 (UDN)。 |
FriendlyName | 返回设备的描述性名称。 |
Type | 获取设备类型。 |
ParentDevice | 返回父设备的对象。对于根设备则为 Null。 |
RootDevice | 返回层级结构中所有设备的父级。 |
IsRoot | 检查设备是否为根设备。 |
DocumentURL | 获取描述文档的 URL。 |
Info | 获取设备属性。 |
Device[int] | 返回设备集合中指定索引处的设备。 |
DeviceCount | 获取集合中设备的数量。 |
IsDeviceCollectionEmpty | 检查集合中是否有任何设备。 |
Service[int] | 返回服务集合中指定索引处的服务。 |
ServiceCount | 获取集合中服务的数量。 |
Icon[int] | 返回图标集合中指定索引处的图标。 |
IconCount | 获取集合中图标的数量。 |
IsIconCollectionEmpty | 检查集合中是否有任何图标。 |
EnumeratorSwitch | 获取或设置枚举 EnumSwitch 的值(Devices、Services、Icons 之一),该值指示将通过 GetEnumerator 方法获取的枚举器来枚举哪个集合(分别为设备、服务或图标)。 |
- 方法
GetEnumerator | 返回由 EnumeratorSwitch 属性的当前值指示的集合的枚举器。当前枚举哪个集合取决于枚举器开关。 |
GetDevices | 返回设备对象数组。 |
GetServices | 返回服务对象数组。 |
GetIcons | 返回图标对象数组。 |
GetDocumentContent | 返回描述文档的内容。 |
GetIconURL | 返回具有指定参数(宽度、高度、深度)的图标的 URL。 |
EnumerateDevices | 处理设备结构,并为每个成员设备引发 OnProcessDevice 事件。您可以向此方法传递两个通用辅助参数,这些参数将作为参数传递给 OnProcessDevice 事件的处理程序。两个参数都可以为 null。 |
- 事件
OnProcessDevice | 在使用 EnumerateDevices 方法处理根设备结构时,为每个成员设备引发。向处理程序传递 ProcessDeviceArgs 类型的参数,该参数包含设备对象和两个通用参数。 |
ServiceNet 类
- 属性
Action[int] | 获取操作数组中指定索引处的操作对象。 |
Action[String] | 获取操作数组中具有指定名称的操作对象。 |
ActionsCount | 获取操作的数量。 |
StateVariables | 获取状态变量的集合。集合的元素包含变量的名称和指示该变量是否发送事件的布尔值。 |
Info | 获取服务的属性。 |
ServiceID | 获取服务的名称。 |
ServiceTypeID | 获取服务的类型。 |
LastTransportStatus | 获取最后一个操作的状态码。 |
ParentDevice | 获取父设备。 |
ScpdURL | 获取描述文档的 URL。 |
- 方法
GetEnumerator | 返回操作集合的枚举器。 |
GetActions | 返回操作对象数组。 |
GetScpdContent | 返回描述文档的内容。 |
ActionNet 类
- 属性
Name | 获取操作的名称。 |
ArgumentCount | 获取操作的输入和输出参数的数量。 |
ArgumentInCount | 获取操作的输入参数的数量。 |
ArgumentsIn | 设置输入参数。 |
Argument | 设置输入参数数组中指定索引处的输入参数。 |
Arguments | 获取输入参数。 |
Info | 获取操作的属性。 |
ParentService | 获取父服务对象。 |
- 方法
Invoke | 在父 UPnP 服务上调用此操作。argsout - 输出参数值和返回值的数组或错误消息。出错时,argsout 仅包含错误消息。返回:-1 表示错误,>0 表示输出参数的数量 + 返回值(如果有),-2 表示 argsout 仅包含返回值。 |
使用 FindManagerNet
首先,我想指出 FindManagerNet
对象使用异步方法搜索设备,这意味着从系统 UPnP 框架引发并由 FindManagerNet
对象接收的事件可以在单独的线程中执行。因此,当 COM 库配置为使用多线程并发模型时,最好使用 FindManagerNet
。默认情况下,.net 应用程序使用的 COM 库初始化为使用单线程并发模型。使用哪种模型取决于应用于应用程序入口点的属性。在 C# 和 Visual Basic 中,此入口点是 Main
方法。要使用多线程模型,属性应设置为 [MTAThread]
,而当属性设置为 [STAThread]
时,则使用默认的单线程模型。
C# 应用程序的 COM 线程模型默认设置 - 单线程(在 Program.cs 文件中)。
static class Program { // The main entry [STAThread] static void Main() { // ... body of Main } }
C# 应用程序的 COM 线程模型设置 - 多线程(在 Program.cs 文件中)。
static class Program { // The main entry [MTAThread] static void Main() { // ... body of Main } }
在 GUI 应用程序中,当 COM 线程模型设置为多线程时,您应使用带有 SynchronizationContext
类型参数的构造函数来创建 FindManagerNet
对象。 díky tomuto konstruktoru mohou obsluhy událostí spuštěné v samostatných vláknech bezpečně přistupovat k ovládacím prvkům vytvořeným v hlavním vlákně GUI bez nutnosti použití InvokeRequired
。FindManagerNet
对象应在使用它的主窗体线程内创建。关键在于,使用构造函数 FindManagerNet(SynchronizationContext)
时,FindManagerNet
对象不能是静态的。应在主 GUI 线程中调用 System.Threading.SynchronizationContext.Current
属性,在该线程中,来自 FindManagerNet
对象(如 OnAddDevice)的事件处理程序可以访问窗体上的控件。
要创建 FindManagerNet
对象,首先,在窗体类中定义 FindManagerNet
的引用
public partial class Form1 : Form { FindManagerNet finder; // ... rest of class body }
接下来,在窗体的构造函数中,借助带有 SynchronizationContext
类型参数的构造函数实例化 FindManagerNet
对象
public Form1() { InitializeComponent(); finder = new FindManagerNet(SynchronizationContext.Current); // ... remaining instructions }
完成上述基本步骤后,您可以设置事件处理程序并初始化 FindManagerNet
对象,如下例所示,该示例将调用添加和删除互联网网关设备端口映射表条目的操作。
using FindManagerWrapper; namespace test_finder { public partial class Form1 : Form { // define finder object's reference FindManagerNet _fm; // service which contains needed actions // for adding and deleting port entries ServiceNet _srv; public Form1() { InitializeComponent(); // create finder and set up needed handlers _fm = new FindManagerNet(SynchronizationContext.Current); _fm.OnServiceVariableChanged += new ServiceVariableChangedEvent(_fm_OnServiceVariableChanged); _fm.OnAddDevice += new AddDeviceEvent(_fm_OnAddDevice); } void _fm_OnAddDevice(object sender, AddDeviceArgs e) { // when first device has been found // then stop searching and find appropriate service _fm.Stop(); // get found device object DeviceNet dev = e.Device; string requestedService = "urn:upnp-org:serviceId:WANPPPConn1"; // enumerate member devices for find requested service dev.OnProcessDevice += new ProcessDeviceEvent(dev_OnProcessDevice); dev.EnumerateDevices(requestedService, 0); dev.OnProcessDevice -= new ProcessDeviceEvent(dev_OnProcessDevice); } void dev_OnProcessDevice(object sender, ProcessDeviceArgs e) { // if requested service has been found // then save reference string requestedService = (string)e.Param; // switch collection which will be enumerated // default collection of devices is enumerated e.Device.EnumeratorSwitch = EnumSwitch.Services; foreach(ServiceNet srv in e.Device) if (srv.ServiceID == requestedService) { _srv = srv; break; } } void _fm_OnServiceVariableChanged(object sender, ServiceVariableChangedArgs e) { // display current number of port mappings if(e.VarName == "PortMappingNumberOfEntries") labelNumber.Text = e.VarValue; } // start private void button1_Click(object sender, EventArgs e) { _fm.Init(FindManagerNet.DeviceTypes[4]); // InternetGatewayDevice _fm.Start(); } // stop private void button2_Click(object sender, EventArgs e) { _fm.Stop(); } // add entry private void button3_Click(object sender, EventArgs e) { if (_srv != null) { ActionNet act = _srv.get_Action("AddPortMapping"); // required input arguments act.ArgumentsIn = new List<string> {"any", "11200", "UDP", "11200", "10.0.0.2", "true", "test add entry", "0"}; // this action doesn't returns any output arguments // this action causes change of state variable PortMappingNumberOfEntries // and raise of event, thus OnServiceVariableChanged will be called List<KeyValuePair<string, string>> outs = null; act.Invoke(ref outs); } } // delete entry private void button4_Click(object sender, EventArgs e) { if (_srv != null) { ActionNet act = _srv.get_Action("DeletePortMapping"); // required input arguments act.ArgumentsIn = new List<string> { "any", "11200", "UDP" }; // this action doesn't returns any output arguments // this action causes change of state variable PortMappingNumberOfEntries // and raise of event, thus OnServiceVariableChanged will be called List<KeyValuePair<string, string>> outs = null; act.Invoke(ref outs); } } } }
历史
- 2009年7月7日
修复了当发现的设备是与使用 UPnPCpLib 库的控制点应用程序在同一台计算机上的“软件”设备时,查找设备时间过长的问题。添加了 FindManagerNet 类的使用说明。 - 2009年6月17日
重写了库的代码。添加了新的 STL 版本的库。添加了原生库的托管包装器。添加了“Finder”应用程序的托管版本和新版本的 MFC 应用程序。 - 2008年6月28日
初始发布。
参考文献
- UPnP APIs, MSDN, http://msdn.microsoft.com/en-us/library/aa382303(VS.85).aspx[^]
- NAT API, MSDN, http://msdn.microsoft.com/en-us/library/aa366187(VS.85).aspx[^]
- UPnP Forum, http://www.upnp.org/[^]
- Using UPnP for Programmatic Port Forwardings and NAT Traversal, Mike O'Neill, PortForward.aspx[^]
- RFC 1067 (SNMP), IETF, http://tools.ietf.org/html/rfc1067[^]
- RFC 3489 (STUN), IETF, http://tools.ietf.org/html/rfc3489[^]
- SSDP, IETF, http://tools.ietf.org/html/draft-cai-ssdp-v1-03[^]
- Description of Universal Plug and Play Features in Windows XP, Microsoft, http://support.microsoft.com/kb/323713[^]
- Universal Plug and Play in Windows XP, MS TechNet, http://technet.microsoft.com/en-us/library/bb457049.aspx[^]
- How Windows Firewall affects the UPnP framework in Windows XP Service Pack 2, Microsoft, http://support.microsoft.com/kb/886257[^]
- Intel Software for UPnP Technology, Intel, http://www.intel.com/cd/ids/developer/asmo-na/eng/downloads/upnp/index.htm[^]
- CMarkup software, First Objective Software, Inc., http://www.firstobject.com/dn_markup.htm[^]