UPnP的全稱是Universal Plug and Play,是由UPnP論壇提出並完善的一套網路協議。注意這裡使用了“一套”這個詞語——UPnP並不是一個協議,而是一組網路協議的集合。如果你還不瞭解UPnP是做什麼的,微軟的這篇文章描述了一個使用UPnP的場景,這篇文章寫於2001年,但文中描寫的場景即使在今天看來,也足夠人們期待。這也從另一方面說明,這項技術還有待進一步發展和普及。
本文將從協議的角度,並儘量使用通俗的語言,來簡單分析一下UPnP的工作原理。
UPnP由發現、描述、控制、事件和展示幾部分組成。每部分功能如下:
在逐一分析前四個部分之前,我們還需要明確一個事實: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
這裡,各項含義如下:
通過這樣的消息,控制點便可以知道網路中的設備。
與之對應的是,當控制點加入網路時,它也將組播一個消息,用來發現設備,例如:
M-SEARCH * HTTP/1.1
Host:239.255.255.250:1900
Man:"ssdp:discover"
Mx:5
ST:ssdp:rootdevice
各項含義如下:
而設備可以根據這樣的搜索產生響應,格式如下:
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節點的內容:
對於每一個服務,資訊包括:
一個服務描述的示例如下:
<?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:
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/。
之後,我還將再寫一篇文章,介紹Cling這個庫。