UPnP简析

发布于2012年12月12日 -

UPnP的全称是Universal Plug and Play,是由UPnP论坛提出并完善的一套网络协议。注意这里使用了“一套”这个词语——UPnP并不是一个协议,而是一组网络协议的集合。如果你还不了解UPnP是做什么的,微软的这篇文章描述了一个使用UPnP的场景,这篇文章写于2001年,但文中描写的场景即使在今天看来,也足够人们期待。这也从另一方面说明,这项技术还有待进一步发展和普及。

本文将从协议的角度,并尽量使用通俗的语言,来简单分析一下UPnP的工作原理。

一、UPnP的结构

UPnP由发现、描述、控制、事件和展示几部分组成。每部分功能如下:

  • 发现:在这个阶段,网络中的服务提供者(称作“设备”)和使用服务者(称作“控制点”)将互相发现对方。
  • 描述:当控制点需要访问一个设备时,它需要知道设备的信息,设备通过XML的方式描述自己的信息、服务等。
  • 控制:当控制点得到设备的描述信息后,即可对设备进行控制。
  • 事件:当设备的状态改变时,如果控制点订阅了这个消息,将会收到事件通知。
  • 展示:设备可以通过浏览器展示自己。(这个功能通常不会使用,本文也不加以讨论)

在逐一分析前四个部分之前,我们还需要明确一个事实:UPnP的基础是,每个设备或控制点都已经处在一个网络中,并且这个网络内允许组播(或称之为多播,以下称组播)。

此外,UPnP使用239.255.255.250:1900这个组播地址。

二、发现

UPnP使用简单发现协议(SSDP)完成发现,这是一个工作在UDP上的HTTP协议。

当一个设备加入网络,它将向组播组发送类似这样的消息:

NOTIFY * HTTP/1.1
Host: 239.255.255.250:1900
Cache-control: max-age=1800
Location: http://192.168.0.1:49152/des.xml
Nt: upnp:rootdevice
Nts: ssdp:alive
Usn: uuid:de5d6118-bfcb-918e-0000-00001eccef34::upnp:rootdevice

这里,各项含义如下:

  • Host:这里必须使用IANA(Internet Assigned Numbers Authority)为SSDP预留的组播地址:239.255.255.250:1900
  • Cache-control:max-age的数值表明设备将在这段时间(单位为秒)后失效,因此,设备应当在失效前,重发这样的消息。标准指出,这里的数值应当不小于于1800秒(30分钟),但实际上,这里的取值范围取决于UPnP的实现。
  • Location:设备描述文件的URL。
  • Nt:Notification Type的缩写,这里的值(upnp:rootdevice)表明这是一个“根设备”。每个设备可以有自己的子设备,本文只考虑设备独立的情形。
  • Nts:Notification Sub Type的缩写,标准规定必须是ssdp:alive
  • Usn:Unique Service Name的缩写,是一个设备实例的标识符。

通过这样的消息,控制点便可以知道网络中的设备。

与之对应的是,当控制点加入网络时,它也将组播一个消息,用来发现设备,例如:

M-SEARCH * HTTP/1.1
Host: 239.255.255.250:1900
Man: "ssdp:discover"
Mx: 5
ST: ssdp:rootdevice

各项含义如下:

  • Host:同上。
  • Man:必须是"ssdp:discover",注意这里的引号不能省略。
  • Mx:1到5之间的一个值,表示最大的等待响应的秒数。
  • ST:Search Target的缩写,表示搜索的节点类型。

而设备可以根据这样的搜索产生响应,格式如下:

HTTP/1.1 200 OK
Cache-control: max-age=1800
Ext:
Location: http://192.168.0.1:2345/xx.xml
Server: Microsoft-Windows-NT/5.1 UPnP/1.0 UPnP-Device-Host/1.0
ST: ST:urn:schemas-upnp-org:service:ContentDirectory:1
USN: uuid:60b2e186-b084-44af-ac09-1c64ea1bb364::urn:schemas-upnp-org:service:ContentDirectory:1

通过这样的流程,控制点即可完成对设备的发现。

三、描述

设备的描述通过HTTP协议完成,获取描述的URL已经在上一节给出。

由于设备描述的标签很多,这里仅以一例作为演示:

<?xml version="1.0" encoding="utf-8"?>
<root xmlns="urn:schemas-upnp-org:device-1-0">
  <specVersion>
    <major>1</major>
    <minor>1</minor>
  </specVersion>

  <device>
    <deviceType>urn:schemas-upnp-org:device:BinaryLight:1</deviceType>
    <friendlyName>Kitchen Lights</friendlyName>
    <manufacturer>OpenedHand</manufacturer>
    <modelName>Virtual Light</modelName>
    <UDN>uuid:cc93d8e6-6b8b-4f60-87ca-228c36b5b0e8</UDN>

    <serviceList>
      <service>
        <serviceType>urn:schemas-upnp-org:service:SwitchPower:1</serviceType>
        <serviceId>urn:upnp-org:serviceId:SwitchPower:1</serviceId>
        <SCPDURL>/SwitchPower1.xml</SCPDURL>
        <controlURL>/SwitchPower/Control</controlURL>
        <eventSubURL>/SwitchPower/Event</eventSubURL>
      </service>
    </serviceList>
  </device>
</root>

本例中,specVersion节点的内容是由标准规定的,所以我们仅讨论device节点的内容:

  • deviceType:设备类型,格式为"urn:schemas-upnp-org:device:deviceType:v",这里deviceTypev是由设备定义的。
  • friendlyName:一个更友好的设备名。
  • manufacturer:制造商。
  • modelName:型号。
  • UDN:Unique Device Name的缩写,设备的UUID。
  • serviceList:服务列表。

对于每一个服务,信息包括:

  • serviceType:与deviceType类似,这里的后两段由服务定义。
  • serviceId:服务ID,通常与serviceType对应。
  • SCPDURL:服务描述的URL。
  • controlURL:用于控制的URL。
  • eventSubURL:用于订阅事件的URL。

一个服务描述的示例如下:

<?xml version="1.0" encoding="utf-8"?>
<scpd xmlns="urn:schemas-upnp-org:service-1-0">
  <specVersion>
    <major>1</major>
    <minor>1</minor>
  </specVersion>
  <actionList>
    <action>
      <name>SetTarget</name>
      <argumentList>
        <argument>
          <name>NewTargetValue</name>
          <relatedStateVariable>Target</relatedStateVariable>
          <direction>in</direction>
        </argument>
      </argumentList>
    </action>
    <action>
      <name>GetTarget</name>
      <argumentList>
        <argument>
          <name>RetTargetValue</name>
          <relatedStateVariable>Target</relatedStateVariable>
          <direction>out</direction>
        </argument>
      </argumentList>
    </action>
    <action>
      <name>GetStatus</name>
      <argumentList>
        <argument>
          <name>ResultStatus</name>
          <relatedStateVariable>Status</relatedStateVariable>
          <direction>out</direction>
        </argument>
      </argumentList>
    </action>
  </actionList>
  <serviceStateTable>
    <stateVariable sendEvents="no">
      <name>Target</name>
      <dataType>boolean</dataType>
      <defaultValue>0</defaultValue>
    </stateVariable>
    <stateVariable sendEvents="yes">
      <name>Status</name>
      <dataType>boolean</dataType>
      <defaultValue>0</defaultValue>
    </stateVariable>
  </serviceStateTable>
</scpd>

这里,actionList定义了“行为”,serviceStateTable定义了“状态变量”。

每个行为都由一个名称(name)和若干参数(argument)组成。而参数由名字(name)、传递方向(direction,取值inout)及关联的状态变量(relatedStateVariable)组成。

状态变量之所以叫做状态变量,是用来标明UPnP设备或程序的一些状态的。一个程序可以订阅(subscribe)状态的变化,从而得到通知。而一个参数之所以必须关联一个状态变量,是因为状态变量的类型决定了参数的类型,因此,对于某些情形,我们可以使用“哑”的状态变量。

四、控制

UPnP使用SOAP完成控制。SOAP工作在HTTP上,使用XML来描述远程调用并返回结果。

以下是一个控制请求的例子:

POST /control/url HTTP/1.1
HOST: hostname:portNumber
CONTENT-TYPE: text/xml; charset="utf-8"
CONTENT-LENGTH: length of body
USER-AGENT: OS/version UPnP/1.1 product/version
SOAPACTION: "urn:schemas-upnp-org:service:serviceType:v#actionName"

<?xml version="1.0"?>
<s:Envelope
 xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
 s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
    <s:Body>
        <u:actionName xmlns:u="urn:schemas-upnp-org:service:serviceType:v">
            <argumentName>in arg value</argumentName>
        </u:actionName>
    </s:Body>
</s:Envelope>

这里,actionNameargumentName就是在协议描述中定义的行为名称和参数名称。

而调用的结果同样以XML方式返回:

HTTP/1.1 200 OK
CONTENT-TYPE: text/xml; charset="utf-8"
DATE: when response was generated
SERVER: OS/version UPnP/1.1 product/version
CONTENT-LENGTH: bytes in body

<?xml version="1.0"?>
<s:Envelope
 xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
 s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
    <s:Body>
        <u:actionNameResponse xmlns:u="urn:schemas-upnp-org:service:serviceType:v">
            <argumentName>out arg value</argumentName>
        </u:actionNameResponse>
    </s:Body>
</s:Envelope>

五、事件

通过事件这一机制,控制点可以监听设备状态的变化。需要指出的是,事件的通讯通常是单播的。

订阅与退订

订阅的URL是在服务描述中给出的。一个订阅的例子如下:

SUBSCRIBE publisher path HTTP/1.1
HOST: publisher host:publisher port
USER-AGENT: OS/version UPnP/1.1 product/version
CALLBACK: <delivery URL>
NT: upnp:event

此处,NT(Notification Type)取upnp:event表示订阅事件;CALLBACK的值是回调的URL。

与之对应的是订阅的响应:

HTTP/1.1 200 OK
DATE: when response was generated
SERVER: OS/version UPnP/1.1 product/version
SID: uuid:subscription-UUID
CONTENT-LENGTH: 0
TIMEOUT: Second-1800

这里有两个参数比较重要:

  • SID:本订阅的标识符,通常使用UUID。
  • TIMEOUT:这里的值表示本订阅的有效期(秒)。这意味着在超时前,必须续订。

续订的请求与订阅类似,只是附加了SID:

SUBSCRIBE publisher path HTTP/1.1
HOST: publisher host:publisher port
SID: uuid:subscription UUID

退订也是类似的:

UNSUBSCRIBE publisher path HTTP/1.1
HOST: publisher host:publisher port
SID: uuid:subscription UUID

事件消息

我们直接来分析数据包:

NOTIFY delivery path HTTP/1.1
HOST: delivery host:delivery port
CONTENT-TYPE: text/xml; charset="utf-8"
NT: upnp:event
NTS: upnp:propchange
SID: uuid:subscription-UUID
SEQ: event key
CONTENT-LENGTH: bytes in body

<?xml version="1.0"?>
<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">
    <e:property>
        <variableName>new value</variableName>
    </e:property>
</e:propertyset>

这里,每个产生事件的状态变量对应一个e:property标签,variableName是变量名。

此外,SEQ是消息的顺序号,从0开始。

当订阅者收到消息之后,必须在30秒内发送确认:

HTTP/1.1 200 OK

如果订阅者没有确认,设备仍然会发送之后的消息,直到本次订阅超时。

六、实现

从前面的介绍来看,UPnP的原理并不复杂。幸运的是,在网上可以找到很多UPnP的SDK。

UPnP论坛上提供了一系列SDK,包括UPnP论坛成员提供的解决方案和一些开源的实现,这些内容可以在这里找到:http://upnp.org/sdcps-and-certification/resources/sdks/

参考资料

  1. UPnP Hacks: UPnP background
  2. UPnP Device Architecture version 1.1
  3. Writing a UPnP Service