0%

DLNA使用到UPnP协议介绍

DLNA(Digital Living Network Alliance),熟悉又陌生的名字,熟悉是因为很久很久之前就听过这个名词了,印象中就是一个投屏的东西,陌生是因为虽然知道它是用来投屏的但却一直没认真用过,也不了解它是怎么实现的。最近有时间研究了下这个协议,记录下这个过程。

DLNA协议标准

DLNA协议的设备发现和控制主要是使用UPnP(Universal Plug and Play)协议实现的,在openconnectivity.org可以找到UPnP协议的技术标准,其中UPnP Device Architecture version 2.0详细介绍了UPnP协议的技术标准、UPnP AV Architecture:2详细介绍了使用UPnP做局域网流媒体播放、控制的技术标准,这两个协议基本就概括了平常我们使用的DLNA音视频投屏功能。

DLNA的具体实现

网上有很多的DLNA投屏的实现,我主要参考Cling的实现,因为它对控制端和服务端的实现比较完善,适合对照着UPnP协议文档学习。虽然Cling也没有更新维护了,但是它对UPnP协议的具体实现方式非常值得我学习,接下来我将基于Cling实现一个简单的Demo来学习、验证UPnP协议。

UPnP协议中主要的名词

UPnP协议中的缩略词比较多,这里例举了主要的名词

缩略词 含义 用途
UDA UPnP Device Architecture UPnP设备架构
Device Device 设备,一个主机可以有多个实现了UPnP协议的设备
Service Service 服务,由Device提供,一个Device可以有多个Service
DeviceType 设备类型 设备的具体类型,设备可以有很多类型,比如提供流媒体播放设备的叫MediaRenderer
ServiceType 服务类型 服务的具体类型,服务也可以有很多类型,比如控制流媒体播放的服务叫AVTransport
SSDP Simple Service Discovery Protocol 用于发现设备的文本协议
SOAP Simple Object Access Protocol 用于控制服务的文本协议
GENA General Event Notification Architecture 用于事件订阅的文本协议
CP Control Point 相对于服务提供方的控制方
SCPD Service Control Protocol Description 描述Service的文本协议
USN Unique Service Name Service唯一标识符,是一个uuid+Service类型的字段

实现双向交互的SeekBar

实现同一个局域网中使用Android手机A上的SeekBar控制另外一个Android手机B上的SeekBar,并且反过来B也可以将自己的变化通知到A,实现双向交互。全部代码在github上upnp_sample项目中,整个Project分为4个模块:

  • cling模块,是cling的源码,直接放进工程是为了方便调试。
  • seekbarserver模块,是服务端代码,运行在B手机上
  • seekbarclient模块,是控制端代码,运行在A手机上
  • common模块,包含了服务端和控制端共用的字段,比如Device类型、Service类型、变量字段,这些都是硬编码字段,两边都要用到。

按照UPnP Device Architecture version 2.0文档介绍,全部过程主要包括发现描述控制事件订阅这4个步骤,我将按照这个顺序,结合UPnP协议内容和SeekBar Demo介绍UPnP协议。

发现

局域网中设备的相互发现主要使用多播通信。事先约定好多播地址和端口,设备启动后向该地址广播自己的信息,同时设备也监听这个地址。UPnP协议使用SSDP协议作为设备的发现协议,SSDP协议约定的多播地址是:239.255.255.250:1900。SSDP协议还规定文本格式基于HTTPU协议,这是一种类似HTTP协议的文本协议,也就说文本格式类似于HTTP协议,但是它是基于UDP协议通信的,而不是TCP协议,所以后缀带U。整个设备发现的网络栈是:UPnP协议 -> SSDP协议 -> UDP协议 -> IP协议。
UPnP协议规定,设备发现主要有两只方式,如图:

  1. 只通过多播的方式,服务端向多播地址广播自己的信息,控制端加入多播地址并监听广播信息。控制端就能收到服务端的广播,发现这个服务端设备。
  2. 多播和单播结合的方式,服务端加入多播地址并监听广播信息,控制端向多播地址广播自己的信息,服务端收到信息后,自然就知道了控制端的地址,再通过UDP通信的方式单独向这个地址发送信息,由于控制端监听这个UDP地址,也就能收到这条信息,也能发现这个设备。
只通过多播的方式

协议规定的服务端广播的内容格式如下:

1
2
3
4
5
6
7
8
NOTIFY * HTTP/1.1
HOST: 239.255.255.250:1900
CACHE-CONTROL: max-age = seconds until advertisement expires
LOCATION: URL for UPnP description for root device
NT: notification type
NTS: ssdp:alive
SERVER: OS/version UPnP/2.0 product/version
USN: composite identifier for the advertisement

SeekBar服务端广播的内容格式如下:

1
2
3
4
5
6
7
8
NOTIFY * HTTP/1.1
CACHE-CONTROL: max-age=1800
LOCATION: http://192.168.1.104:40243/upnp/dev/32799174-d1b4-6581-0000-00001d15406d/desc
NT: urn:schemas-upnp-org:service:SeekBarServiceType:1
HOST: 239.255.255.250:1900
NTS: ssdp:alive
USN: uuid:32799174-d1b4-6581-0000-00001d15406d::urn:schemas-upnp-org:service:SeekBarServiceType:1
SERVER: Linux/5.4.147-qgki-g223a5b5ef8ad UPnP/1.0 Cling/2.0

文本的信息包括:

  • CACHE-CONTROL,表示收到此条消息之后1800秒内没再收到SeekBar服务端的广播信息,那可以认为SeekBar服务端已经离线
  • LOCATION,是服务端提供的一个HTTP地址,用于描述该设备的详细内容和能力,比如设备的有什么服务,服务提供什么能力等。
  • NT,表示服务端的这个服务的具体类型,客户端可以筛选类型,比如只处理ServiceType为SeekBarServiceType的设备广播等。
  • NTS( Notification Sub Type),表示这条SSDP协议的NOTIFY消息的子类型,除ssdp:alive外还有ssdp:byebye。
  • USN,是这个设备的唯一标识符,用于控制端区分不同的设备。
多播和单播结合的方式
控制端发送消息

首先,控制端向多播地址发送消息,协议规定的内容格式如下:

1
2
3
4
5
6
M-SEARCH * HTTP/1.1
HOST: 239.255.255.250:1900
MAN: "ssdp:discover"
MX: seconds to delay response
ST: search target
USER-AGENT: OS/version UPnP/2.0 product/version

SeekBar控制端广播的内容如下:

1
2
3
4
5
M-SEARCH * HTTP/1.1
MX: 3
ST: ssdp:all
HOST: 239.255.255.250:1900
MAN: "ssdp:discover"

信息内容包括:

  • M-SEARCH,表示该条SSDP消息是控制端发的搜索信息,如果同样是控制端的设备,那就不处理此条消息,只有服务端才会处理这条消息。
  • MX,最大等待时间,服务端要延后发送信息给控制端,保证控制端不立马收到大量信息,延后的时间是一个随机值,介于0到MX秒。
  • ST,搜索的设备/服务的类型,这里是all,表示所有基于SSDP协议的设备都响应这条消息。
服务端发送消息

然后,SeekBar服务端收到了这条消息,解析后得知:

  1. 局域网中有一个控制端正在主动搜索设备,并且自己符合搜索类型ST的要求。
  2. 由于多播消息是基于UDP发送的,SeekBar服务端也能拿到对端的IP地址和端口。
    这样,服务端就有条件向SeekBar控制端发送UDP单播消息了,协议规定的服务端单播的内容格式如下:
    1
    2
    3
    4
    5
    6
    7
    8
    HTTP/1.1 200 OK
    CACHE-CONTROL: max-age = seconds until advertisement expires
    DATE: when response was generated
    EXT:
    LOCATION: URL for UPnP description for root device
    SERVER: OS/version UPnP/2.0 product/version
    ST: search target
    USN: composite identifier for the advertisement
    SeekBar服务端单播的内容如下:
    1
    2
    3
    4
    5
    6
    7
    HTTP/1.1 200 OK
    CACHE-CONTROL: max-age=1800
    LOCATION: http://192.168.1.104:39839/upnp/dev/32799174-d1b4-6581-0000-00001d15406d/desc
    EXT:
    ST: urn:schemas-upnp-org:service:SeekBarServiceType:1
    USN: uuid:32799174-d1b4-6581-0000-00001d15406d::urn:schemas-upnp-org:service:SeekBarServiceType:1
    SERVER: Linux/5.4.147-qgki-g223a5b5ef8ad UPnP/1.0 Cling/2.0
    文本的信息包括:
  • HTTP/1.1 200 OK,表示这是一个SSDP协议的响应头。
  • 其他内容和多播返回的关键信息一样。
设备消失的情况
  1. 服务端主动断开,通过多播地址广播ssdp:byebye
  2. 服务端被动断开,比如crash、断网等,控制端在maxAgeSeconds内没有收到ssdp:alive的信息,则主动移除这个服务端
    ,控制端优先取自己配置的maxAgeSeconds,没有设置的情况下则设置为ssdp:alive中带的CACHE-CONTROL值

描述

通过发现阶段,控制端已经知道了这个局域网中有哪些可以使用的Device,但是如果我们要进一步使用这些Device,只知道这些信息是远远不够的,必须进一步了解这些Device提供的能力。在发现阶段,服务端的Device返回的信息中有一个LOCATION字段,这个字段是一个HTTP地址,控制端访问这个地址就能知道Device提供的信息。UPnP协议规定,Device描述通过HTTP通信实现,如图:

请求分为两步,第一次获取Device描述信息,第二次遍历设备中的所有Service获取所有Service的描述信息

Device描述

一个Device可以有多个Service,能力由Service提供。在发现阶段,控制端会分别收到Device和Service的SSDP Notify信息,但它们的uuid是一样的,所以可以辨别为一个Device,而且它们LOCATION是一样的,都是Device描述的地址。
描述阶段Device描述使用的是XML文本格式,以下是访问LOCATION字段URL的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
http://192.168.1.104:39839/upnp/dev/32799174-d1b4-6581-0000-00001d15406d/desc

<root xmlns="urn:schemas-upnp-org:device-1-0">
<specVersion>
<major>1</major>
<minor>0</minor>
</specVersion>
<device>
<deviceType>urn:schemas-upnp-org:device:SeekBarDeviceType:1</deviceType>
<friendlyName>DLNA SeekBar Demo</friendlyName>
<manufacturer>CXX</manufacturer>
<modelDescription>DLNA SeekBar Demo</modelDescription>
<modelName>SeekBarDeviceType</modelName>
<modelNumber>v1</modelNumber>
<UDN>uuid:32799174-d1b4-6581-0000-00001d15406d</UDN>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:SeekBarServiceType:1</serviceType>
<serviceId>urn:upnp-org:serviceId:SeekBarServiceId</serviceId>
<SCPDURL>/upnp/dev/0b6dd07e-07c4-442e-0000-00001d15406d/svc/upnp-org/SeekBarServiceId/desc</SCPDURL>
<controlURL>/upnp/dev/0b6dd07e-07c4-442e-0000-00001d15406d/svc/upnp-org/SeekBarServiceId/action</controlURL>
<eventSubURL>/upnp/dev/0b6dd07e-07c4-442e-0000-00001d15406d/svc/upnp-org/SeekBarServiceId/event</eventSubURL>
</service>
</serviceList>
</device>
</root>

XML文本里面有这个Device的版本、设备等信息,最关键的是****,它描述了这个Device提供的哪些Service,以及每个Service的详细信息:

  • serviceType,Service的类型
  • serviceId,Service的Id
  • SCPDURL,描述Service具体能力的相对地址,和域名地址拼接得到绝对地址
  • controlURL,用于控制阶段的地址
  • eventSubURL,用于订阅阶段订阅消息的地址
Service描述

拿到Device描述后,还会继续解析拿到每个Service提供的信息,Service的描述使用的是SCPD文本协议,以下是SeekBarService的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
http://192.168.1.101:35321/upnp/dev/d04e9f33-4602-2f48-0000-00001d15406d/svc/upnp-org/SeekBarServiceId/desc

<scpd xmlns="urn:schemas-upnp-org:service-1-0">
<specVersion>
<major>1</major>
<minor>0</minor>
</specVersion>
<actionList>
<action>
<name>GetProgress</name>
<argumentList>
<argument>
<name>RetProgress</name>
<direction>out</direction>
<relatedStateVariable>Progress</relatedStateVariable>
</argument>
</argumentList>
</action>
<action>
<name>SetProgress</name>
<argumentList>
<argument>
<name>NewProgress</name>
<direction>in</direction>
<relatedStateVariable>Progress</relatedStateVariable>
</argument>
</argumentList>
</action>
</actionList>
<serviceStateTable>
<stateVariable sendEvents="yes">
<name>Progress</name>
<dataType>i4</dataType>
<defaultValue>0</defaultValue>
</stateVariable>
</serviceStateTable>
</scpd>

SeekBarService比较简单,它只定义了一个变量,名称为Progress,类型是i4(4 bytes int);两个action,第一个action名为RetProgress,数据方向是out,表示用于控制端获取数据,第二个action是SetProgress,设置的参数叫NewProgress,数据方向为in,表示用于控制端设置数据。

控制

通过描述阶段,控制端知道了服务端的Service提供的具体变量以及能够使用的方法。接下来就要通过调用action去控制服务端的SeekBar,控制使用的是SOAP协议,SOAP协议基于HTTP协议,以下是SeekBarClient设置进度为57时的请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /upnp/dev/d04e9f33-4602-2f48-0000-00001d15406d/svc/upnp-org/SeekBarServiceId/action HTTP/1.1
Host: 192.168.1.101:39603
Soapaction: "urn:schemas-upnp-org:service:SeekBarServiceType:1#SetProgress"
Content-Type: text/xml;charset="utf-8"
Content-Length: 329
User-Agent: Android/7.1.1 UPnP/1.0 Cling/2.0

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<u:SetProgress xmlns:u="urn:schemas-upnp-org:service:SeekBarServiceType:1">
<NewProgress>57</NewProgress>
</u:SetProgress>
</s:Body>
</s:Envelope>

控制端收到后回复:

1
2
3
4
5
6
7
8
9
10
11
12
13
Ext:
Server: Linux/4.4.21-perf-g6a9ee37d-06242-g8326f14 UPnP/1.0 Cling/2.0
Date: Sat, 11 Feb 2023 10:24:41 GMT
Content-type: text/xml;charset=UTF-8
Content-length: 293

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<u:SetProgressResponse xmlns:u="urn:schemas-upnp-org:service:SeekBarServiceType:1"/>
</s:Body>
</s:Envelope>

以上就实现了一次控制端对服务端的一次单向的设置SeekBar进度条动作。

事件订阅

UPnP协议规定事件订阅基于GENA协议,它也是基于HTTP协议实现的,并且将事件订阅分为订阅订阅内容更新更新订阅取消订阅 4种,如图:

控制端事件订阅

在描述阶段,Service的SCPD文本中的标签里有一个sendEvents=yes字段,它表示这个变量可以被订阅。订阅不是强制的,控制端可以选择是否订阅,也可以在订阅后随时取消订阅。以下是SeekBarClient向服务端订阅Progress变量的HTTP请求头:

1
2
3
4
5
SUBSCRIBE /upnp/dev/d04e9f33-4602-2f48-0000-00001d15406d/svc/upnp-org/SeekBarServiceId/event HTTP/1.1
Host: 192.168.1.101:39603
Timeout: Second-10
Nt: upnp:event
Callback: <http://192.168.1.103:38897/upnp/dev/d04e9f33-4602-2f48-0000-00001d15406d/svc/upnp-org/SeekBarServiceId/event/cb>

虽然基于HTTP协议,但是它的请求头是自定义的,这里是SUBSCRIBE,表示这是一个订阅请求。请求头还包括:

  • Timeout,如果300秒内服务端没收到控制端的订阅更新,则认为订阅过期。
  • Callback,如果服务端的变量发生变化,服务端使用这个url通知控制端。

订阅成功后,服务端返回的数据:

1
2
3
4
5
Sid: uuid:35e14714-0a6d-496e-894a-6b1a041cadbe
Timeout: Second-300
Content-length: 0
Date: Tue, 07 Feb 2023 16:06:18 GMT
Server: Linux/4.19.157-perf UPnP/1.0 Cling/2.0

其中Sid是服务端为本次订阅生成的uuid,后续的更新、取消、通知都使用这个id作为标识。

服务端的通知

一旦服务端的变量发生变化,服务端调用Callback通知控制端,此时Progress变为20,通知的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Nts: upnp:propchange
User-agent: Android/11 UPnP/1.0 Cling/2.0
Sid: uuid:35e14714-0a6d-496e-894a-6b1a041cadbe
Nt: upnp:event
Content-length: 175
Host: 192.168.1.103:37084
Content-type: text/xml
Seq: 8

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">
<e:property>
<Progress>20</Progress>
</e:property>
</e:propertyset>
控制端更新订阅

控制端事件订阅时设置的Timeout为10,所以控制端需要在10秒内向服务端发送以下订阅请求,否则服务端将移除此Sid的订阅

1
2
3
4
SUBSCRIBE /upnp/dev/d04e9f33-4602-2f48-0000-00001d15406d/svc/upnp-org/SeekBarServiceId/event HTTP/1.1
Host: 192.168.1.101:39603
Sid: uuid:35e14714-0a6d-496e-894a-6b1a041cadbe
Timeout: Second-10
控制端取消订阅

控制端直接取消订阅的请求:

1
2
3
UNSUBSCRIBE /upnp/dev/d04e9f33-4602-2f48-0000-00001d15406d/svc/upnp-org/SeekBarServiceId/event HTTP/1.1
Host: 192.168.1.101:39603
Sid: uuid:35e14714-0a6d-496e-894a-6b1a041cadbe

服务端收到后会取消掉该Sid的订阅。

总结

以上只是使用了一个小Demo验证了DLNA使用的UPnP协议的基本功能,实际的DLNA投屏过程使用的Service和变量比这个要多很多,但基本的原理就是这些。通过本篇文章的技术分析,以及结合实际的使用体验,我们可以发现DLNA投屏的一些问题,比如:

  1. 没有连接状态的概念。
  2. 由于没有连接状态的概念,服务端不能主动和控制端断开连接,只有等待控制端主动不投了,那才算断开了。
  3. 由于是公有协议,不管服务端还是控制端对协议的实现都会有些差异,实际使用中总会有小bug。

另外,UPnP协议中多次提到要考虑一台主机可能存在多个IP地址的情况,比如控制端、服务端所在的主机都可能同时存在可用的eth0、wlan、p2p网络等,需要周全的考虑到各种多IP地址的情况。