5

UPNP(一)

 2 years ago
source link: http://antkillerfarm.github.io/technology/2016/03/11/upnp.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

官方的协议规范参见:

http://www.upnp.org/specs/

上图是UPNP的协议栈。详细的解释参见:

http://www.h3c.com.cn/MiniSite/Technology_Circle/Net_Reptile/The_Five/Home/Catalog/201206/747039_97665_0.htm

UPnP基本原理以及在NAT中的应用。这是H3C的一个系列教程中的一篇。

libupnp

libupnp是UPNP协议的一个实现库。它最早由英特尔开发并开源,是目前Linux平台最流行的实现库,其官网为:

http://pupnp.sourceforge.net/

和一般的Linux应用库不同,libupnp没有采用搭积木的方式构建库,而是自己集成了HTTP处理、XML处理、HTTP服务器、线程池等功能(当然,这些功能做的都比较简单,是个理想的教材库)。估计这与它的跨平台目标有关。

安装方法:

sudo apt install libupnp-dev

上图是libupnp体系结构图,其参考教程可参见:

http://blog.csdn.net/braddoris/article/details/41646789

upnp协议简介(一)

这里仅对上文中的内容,做一个补充:

1.GENA协议规范

https://tools.ietf.org/id/draft-cohen-gena-p-base-01.txt

2.其他参考资料

http://max.book118.com/html/2014/0811/9385832.shtm

基于DLNA的GENA协议的研究与开发毕业设计论文.doc

http://blog.csdn.net/hqyhqyhq/article/details/17921797

upnp协议demo

http://download.csdn.net/detail/liguangshou06/2685789

libupnp for linux UPnP编程指南

libupnp示例详解

libupnp自带的示例在upnp/sample路径下。编译之后,可生成三个可执行文件:

1.tv_device。UPNP设备端实现,即UPNP的服务提供者。

2.tv_ctrlpt。UPNP控制端实现。

3.tv_combo。前两者的混合体。

实际测试使用中,可以同时运行tv_ctrlpt和tv_device,以观察两者之间的交互。

tv_device代码流程详解

upnp/sample/linux/tv_device_main.c: main

upnp/sample/common/tv_device.c: device_main

upnp/sample/common/tv_device.c: TvDeviceStart

upnp/src/api/upnpapi.c: UpnpInit

upnp/src/api/upnpapi.c: UpnpInitStartServers

upnp/src/genlib/miniserver/miniserver.c: StartMiniServer

这是整个初始化过程中,最重要的函数。它负责创建Web Server和线程池。UPNP的所有功能都在这些线程(而不是主线程)中实现。

事件处理分成两步

1.注册事件处理回调函数

upnp/src/api/upnpapi.c: TvDeviceStart

upnp/src/api/upnpapi.c: UpnpRegisterRootDevice

这一步向一个中间结构注册回调函数。

upnp/src/api/upnpapi.c: UpnpInitPreamble

这一步调用SetGenaCallback函数,将上一步注册到中间结构中的回调函数,注册到GENA事件处理线程中。

UpnpRegisterRootDevice有若干不同的变种:UpnpRegisterRootDevice2、UpnpRegisterRootDevice3、UpnpRegisterRootDevice4。其中,对于动态生成设备描述的方式来说,UpnpRegisterRootDevice2更好用一些。

2.事件处理流程

upnp/src/genlib/miniserver/miniserver.c: RunMiniServer

upnp/src/genlib/miniserver/miniserver.c: web_server_accept

upnp/src/genlib/miniserver/miniserver.c: schedule_request_job

这一步从线程池中启动一个线程来处理事件。

upnp/src/genlib/miniserver/miniserver.c: handle_request

upnp/src/genlib/miniserver/miniserver.c: dispatch_request

这一步根据事件类型进行分发。具体到GENA就是调用:

upnp/src/gena/gena_callback2.c: genaCallback

这一步根据角色的不同,调用gena_process_subscription_request(device)或gena_process_notification_event(control point)。

upnp/src/gena/gena_device.c: gena_process_subscription_request

这里会最终调用注册的事件处理函数。

1.设备发现

设备发现过程使用SSDP协议,其端口为:

#define SSDP_PORT 1900

1)主动告知

NOTIFY * HTTP/1.1 NTS: ssdp:alive

函数调用:

upnp/src/genlib/miniserver/miniserver.c: RunMiniServer

upnp/src/genlib/miniserver/miniserver.c: ssdp_read

upnp/src/ssdp/ssdp_server.c: readFromSSDPSocket

upnp/src/ssdp/ssdp_server.c: ssdp_event_handler_thread

upnp/src/ssdp/ssdp_device.c: ssdp_handle_device_request

upnp/src/ssdp/ssdp_device.c: advertiseAndReplyThread

upnp/src/ssdp/ssdp_server.c: AdvertiseAndReply

upnp/src/ssdp/ssdp_device.c: DeviceAdvertisement

upnp/src/ssdp/ssdp_device.c: CreateServicePacket msg_type=MSGTYPE_ADVERTISEMENT

2)查询消息

M-SEARCH * HTTP/1.1 MAN: ssdp:discover

函数调用:

upnp/sample/common/tv_ctrlpt.c: TvCtrlPointCommandLoop

upnp/sample/common/tv_ctrlpt.c: TvCtrlPointProcessCommand

upnp/sample/common/tv_ctrlpt.c: TvCtrlPointRefresh

upnp/src/api/upnpapi.c: UpnpSearchAsync

upnp/src/ssdp/ssdp_ctrlpt.c: SearchByTarget

upnp/src/ssdp/ssdp_ctrlpt.c: CreateClientRequestPacket

3)再见消息

NOTIFY * HTTP/1.1 NTS: ssdp:byebye

函数调用:

从RunMiniServer到DeviceAdvertisement的步骤和主动告知相同。

upnp/src/ssdp/ssdp_device.c: CreateServicePacket msg_type=MSGTYPE_SHUTDOWN

4)添加设备、服务

upnp/sample/common/tv_ctrlpt.c: TvCtrlPointCallbackEventHandler

upnp/sample/common/tv_ctrlpt.c: TvCtrlPointAddDevice

upnp/sample/common/sample_util.c: SampleUtil_FindAndParseService

upnp/sample/common/sample_util.c: SampleUtil_PrintEvent

这是个事件打印函数,对于分析UPNP的交互流程很有帮助。

threadutil/src/ThreadPool.c: TPJobXXX

这是一系列和线程池操作相关的函数。

upnp/src/uuid/uuid.c: uuid_create

生成UUID的函数。

upnp/src/api/UpnpString.c

这个文件中,有个String的C语言实现,代码写得不错。

tv_ctrlpt代码流程详解

1.初始化

upnp/sample/linux/tv_ctrlpt_main.c: main

common/tv_ctrlpt.c: TvCtrlPointStart

upnp/src/api/upnpapi.c: UpnpInit

以下与tv_device相同。

2.注册客户端

common/tv_ctrlpt.c: TvCtrlPointStart

upnp/src/api/upnpapi.c: UpnpRegisterClient

其他部分与tv_device注册事件的流程相同。

common/tv_ctrlpt.c: TvCtrlPointCallbackEventHandler

这是tv_ctrlpt的事件处理函数。

gmediarender的UPNP流程详解

gmediarender使用libupnp库进行DLNA协议的交互。这个项目的难度中等,可作为libupnp库的进阶教程。由于大部分的内容和tv_device类似,因此,这里只列出差异的部分。

MediaRenderer包含三个服务:

1)RenderingControl。代码在src/upnp_control.c中。

2)ConnectionManager。代码在src/upnp_connmgr.c中。

3)AVTransport。代码在src/upnp_transport.c中。

注意:MediaRenderer和所包含的服务都有版本的概念。比如MediaRenderer在XML中一般表示为urn:schemas-upnp-org:device:MediaRenderer:1,其中最后的数字1就是版本号。

此外,MediaRenderer的版本和所包含服务的版本,有一定的对应关系。比如MediaServer:4中的ScheduledRecording:2。因为ScheduledRecording是在MediaServer:2中引入的,因此它的版本号就不可能和MediaServer的版本号一致。MediaRenderer也是一样的情况。

一个服务可以提供若干功能。这些功能主要包括三方面:

1.状态变量(State Variables)。状态变量可以进行查询操作。

2.动作(Action)。get类的Action可以实现和查询状态变量类似的效果,但比后者要复杂一些。

3.事件(Eveting)。事件是设备(Device)主动推送给控制点(Control Point)的消息,需要后者订阅(SUBSCRIBE)或退订(UNSUBSCRIBE)。

src/main.c: main

src/upnp_device.c: upnp_device_init

src/upnp_device.c: initialize_device

upnp/src/api/upnpapi.c: UpnpInit

动作(Action)处理

1.数据结构

相关数据结构按从大到小的顺序,依次为:

服务->动作->参数->值类型->基本数据类型

这里从最小的基本数据类型说起。由于MediaRenderer的三个服务在这里的细节都是类似的,因此下面仅以RenderingControl为例。

1)基本数据类型

src/upnp.h: param_datatype定义了可用的基本数据类型,比如STRING、BOOLEAN、I2、I4、UI2、UI4等。后四种都是整数类型,I表示整数,U表示无符号,2表示2个字节的宽度。

2)值类型

src/upnp.h: var_meta定义了值类型的数据结构。该类型的实例是src/upnp_control.c: control_var_meta。var_meta的sendevents成员的含义是,如果该值发生改变时,发送消息则设置为SENDEVENT_YES(默认值),否则设为SENDEVENT_NO。

src/upnp.h: argument定义了参数的数据结构。典型示例如下:

{ "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }

一个动作包含若干参数,因此动作的数据结构是一个argument数组。以SetVolume动作为例,它的参数结构的实例为src/upnp_control.c: arguments_set_vol。

一个服务包含若干动作,因此服务的数据结构是一个argument的二维数组。在这里是src/upnp_control.c: argument_list。

服务不仅包含动作,还包含了其他一些东西,这些都被统一组织到src/upnp.h: service结构中。该结构的实例是src/upnp_control.c: control_service_。

功能发布用于向外界宣布本服务所支持的功能。

gmediarender功能发布的内容是动态生成的,其函数为:src/upnp.c: gen_scpd

src/upnp_device.c: event_handler

返回值处理

src/upnp_device.h: upnp_add_response

调用这个函数或者它的派生函数,生成返回值的xml。

状态变量处理

gmediarender定义了一个变量容器来维护相关的状态变量。这个容器的代码在src/variable-container.c中。

代码中用的比较多的是replace_var和get_var这两个进一步封装后的接口函数。

gmediarender的播放处理代码在src/output_gstreamer.c中。其中最关键的结构是output_module。

gmediarender的URI主要有两个即uri和next_uri。在pipeline的about-to-finish事件中,会做相应的处理。

gmediaserver

除了gmediarender项目之外,GNU还有个名叫gmediaserver的项目,也是使用libupnp库,结构和gmediarender十分相似,它的官网是:

http://www.nongnu.org/gmediaserver/

自制的Control Point示例

和gmediarender相比,libupnp的sample写的并不好。这主要体现在以下方面:

1.封装的层次太多。虽然这样一来,main函数看起来很简单,细节都被隐藏了起来。但隐藏的先决条件,是对用户使用的透明。而sample显然做不到这一点,于是,用户扩展业务功能的时候,还要翻越层层封装,才能找到需要修改的地方。

2.使用不方便。设备功能的XML描述文件居然是写死的,扩展极为不易。(gmediarender的XML描述文件是动态生成的。)

针对这些问题,我打算模仿gmediarender的写法,做一个Control Point的示例。

其代码重构的核心是:将用户需要扩展的业务功能,抽象为数据结构,并将这些数据结构的内容定义放在一起,以便于用户的修改。换句话说,用户只需要修改数组的内容,而不必修改代码,即可扩展业务功能。

在功能上,为了使这个示例更有意义,这里选择gmediarender作为和示例配套的Device程序。因为,gmediarender实现的是一个有实用价值的协议规范,而非demo,所需处理的情况也比demo复杂的多。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK