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/。