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"
,这里deviceType
和v
是由设备定义的。 - 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
,取值in
或out
)及关联的状态变量(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>
这里,actionName
和argumentName
就是在协议描述中定义的行为名称和参数名称。
而调用的结果同样以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/。