http://antkillerfarm.github.io/
官方的协议规范参见:
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
这是H3C的一个系列教程中的一篇。
libupnp是UPNP协议的一个实现库。它最早由英特尔开发并开源,是目前Linux平台最流行的实现库,其官网为:
http://pupnp.sourceforge.net/
和一般的Linux应用库不同,libupnp没有采用搭积木的方式构建库,而是自己集成了HTTP处理、XML处理、HTTP服务器、线程池等功能(当然,这些功能做的都比较简单,是个理想的教材库)。估计这与它的跨平台目标有关。
安装方法:
sudo apt-get install libupnp-dev
上图是libupnp体系结构图,其参考教程可参见:
http://blog.csdn.net/braddoris/article/details/41646789
这里仅对上文中的内容,做一个补充:
1.GENA协议规范
https://tools.ietf.org/id/draft-cohen-gena-p-base-01.txt
2.其他参考资料
http://max.book118.com/html/2014/0811/9385832.shtm
http://blog.csdn.net/hqyhqyhq/article/details/17921797
http://download.csdn.net/detail/liguangshou06/2685789
libupnp自带的示例在upnp/sample路径下。编译之后,可生成三个可执行文件:
1.tv_device。UPNP设备端实现,即UPNP的服务提供者。
2.tv_ctrlpt。UPNP控制端实现。
3.tv_combo。前两者的混合体。
实际测试使用中,可以同时运行tv_ctrlpt和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语言实现,代码写得不错。
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使用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
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。
3)参数
src/upnp.h: argument定义了参数的数据结构。典型示例如下:
{ "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }
3)动作
一个动作包含若干参数,因此动作的数据结构是一个argument数组。以SetVolume动作为例,它的参数结构的实例为src/upnp_control.c: arguments_set_vol。
4)服务
一个服务包含若干动作,因此服务的数据结构是一个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事件中,会做相应的处理。
除了gmediarender项目之外,GNU还有个名叫gmediaserver的项目,也是使用libupnp库,结构和gmediarender十分相似,它的官网是:
http://www.nongnu.org/gmediaserver/
和gmediarender相比,libupnp的sample写的并不好。这主要体现在以下方面:
1.封装的层次太多。虽然这样一来,main函数看起来很简单,细节都被隐藏了起来。但隐藏的先决条件,是对用户使用的透明。而sample显然做不到这一点,于是,用户扩展业务功能的时候,还要翻越层层封装,才能找到需要修改的地方。
2.使用不方便。设备功能的XML描述文件居然是写死的,扩展极为不易。(gmediarender的XML描述文件是动态生成的。)
针对这些问题,我打算模仿gmediarender的写法,做一个Control Point的示例。
其代码重构的核心是:将用户需要扩展的业务功能,抽象为数据结构,并将这些数据结构的内容定义放在一起,以便于用户的修改。换句话说,用户只需要修改数组的内容,而不必修改代码,即可扩展业务功能。
在功能上,为了使这个示例更有意义,这里选择gmediarender作为和示例配套的Device程序。因为,gmediarender实现的是一个有实用价值的协议规范,而非demo,所需处理的情况也比demo复杂的多。
这次,我打算从头开始搭建Control Point示例。也就是从main函数出发,逐步完善相关功能。这一步的代码在:
https://github.com/antkillerfarm/antkillerfarm_crazy/tree/master/helloworld/upnp/step1
该程序主要实现:
1.基本框架。包括初始化和注册Control Point。
2.通过SSDP的搜索功能,搜索网络设备。
这一步的代码在:
https://github.com/antkillerfarm/antkillerfarm_crazy/tree/master/helloworld/upnp/step2