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协议规定,设备发现主要有两只方式,如图:
- 只通过多播的方式,服务端向多播地址广播自己的信息,控制端加入多播地址并监听广播信息。控制端就能收到服务端的广播,发现这个服务端设备。
- 多播和单播结合的方式,服务端加入多播地址并监听广播信息,控制端向多播地址广播自己的信息,服务端收到信息后,自然就知道了控制端的地址,再通过UDP通信的方式单独向这个地址发送信息,由于控制端监听这个UDP地址,也就能收到这条信息,也能发现这个设备。
只通过多播的方式
协议规定的服务端广播的内容格式如下:
1 | NOTIFY * HTTP/1.1 |
SeekBar服务端广播的内容格式如下:
1 | NOTIFY * HTTP/1.1 |
文本的信息包括:
- CACHE-CONTROL,表示收到此条消息之后1800秒内没再收到SeekBar服务端的广播信息,那可以认为SeekBar服务端已经离线
- LOCATION,是服务端提供的一个HTTP地址,用于描述该设备的详细内容和能力,比如设备的有什么服务,服务提供什么能力等。
- NT,表示服务端的这个服务的具体类型,客户端可以筛选类型,比如只处理ServiceType为SeekBarServiceType的设备广播等。
- NTS( Notification Sub Type),表示这条SSDP协议的NOTIFY消息的子类型,除ssdp:alive外还有ssdp:byebye。
- USN,是这个设备的唯一标识符,用于控制端区分不同的设备。
多播和单播结合的方式
控制端发送消息
首先,控制端向多播地址发送消息,协议规定的内容格式如下:
1 | M-SEARCH * HTTP/1.1 |
SeekBar控制端广播的内容如下:
1 | M-SEARCH * HTTP/1.1 |
信息内容包括:
- M-SEARCH,表示该条SSDP消息是控制端发的搜索信息,如果同样是控制端的设备,那就不处理此条消息,只有服务端才会处理这条消息。
- MX,最大等待时间,服务端要延后发送信息给控制端,保证控制端不立马收到大量信息,延后的时间是一个随机值,介于0到MX秒。
- ST,搜索的设备/服务的类型,这里是all,表示所有基于SSDP协议的设备都响应这条消息。
服务端发送消息
然后,SeekBar服务端收到了这条消息,解析后得知:
- 局域网中有一个控制端正在主动搜索设备,并且自己符合搜索类型ST的要求。
- 由于多播消息是基于UDP发送的,SeekBar服务端也能拿到对端的IP地址和端口。
这样,服务端就有条件向SeekBar控制端发送UDP单播消息了,协议规定的服务端单播的内容格式如下:SeekBar服务端单播的内容如下:1
2
3
4
5
6
7
8HTTP/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文本的信息包括:1
2
3
4
5
6
7HTTP/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协议的响应头。
- 其他内容和多播返回的关键信息一样。
设备消失的情况
- 服务端主动断开,通过多播地址广播ssdp:byebye
- 服务端被动断开,比如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 | http://192.168.1.104:39839/upnp/dev/32799174-d1b4-6581-0000-00001d15406d/desc |
XML文本里面有这个Device的版本、设备等信息,最关键的是**
- serviceType,Service的类型
- serviceId,Service的Id
- SCPDURL,描述Service具体能力的相对地址,和域名地址拼接得到绝对地址
- controlURL,用于控制阶段的地址
- eventSubURL,用于订阅阶段订阅消息的地址
Service描述
拿到Device描述后,还会继续解析
1 | http://192.168.1.101:35321/upnp/dev/d04e9f33-4602-2f48-0000-00001d15406d/svc/upnp-org/SeekBarServiceId/desc |
SeekBarService比较简单,它只定义了一个变量,名称为Progress,类型是i4(4 bytes int);两个action,第一个action名为RetProgress,数据方向是out,表示用于控制端获取数据,第二个action是SetProgress,设置的参数叫NewProgress,数据方向为in,表示用于控制端设置数据。
控制
通过描述阶段,控制端知道了服务端的Service提供的具体变量以及能够使用的方法。接下来就要通过调用action去控制服务端的SeekBar,控制使用的是SOAP协议,SOAP协议基于HTTP协议,以下是SeekBarClient设置进度为57时的请求:
1 | POST /upnp/dev/d04e9f33-4602-2f48-0000-00001d15406d/svc/upnp-org/SeekBarServiceId/action HTTP/1.1 |
控制端收到后回复:
1 | Ext: |
以上就实现了一次控制端对服务端的一次单向的设置SeekBar进度条动作。
事件订阅
UPnP协议规定事件订阅基于GENA协议,它也是基于HTTP协议实现的,并且将事件订阅分为订阅、订阅内容更新、更新订阅、取消订阅 4种,如图:
控制端事件订阅
在描述阶段,Service的SCPD文本中的
1 | SUBSCRIBE /upnp/dev/d04e9f33-4602-2f48-0000-00001d15406d/svc/upnp-org/SeekBarServiceId/event HTTP/1.1 |
虽然基于HTTP协议,但是它的请求头是自定义的,这里是SUBSCRIBE,表示这是一个订阅请求。请求头还包括:
- Timeout,如果300秒内服务端没收到控制端的订阅更新,则认为订阅过期。
- Callback,如果服务端的变量发生变化,服务端使用这个url通知控制端。
订阅成功后,服务端返回的数据:
1 | Sid: uuid:35e14714-0a6d-496e-894a-6b1a041cadbe |
其中Sid是服务端为本次订阅生成的uuid,后续的更新、取消、通知都使用这个id作为标识。
服务端的通知
一旦服务端的变量发生变化,服务端调用Callback通知控制端,此时Progress变为20,通知的内容如下:
1 | Nts: upnp:propchange |
控制端更新订阅
控制端事件订阅时设置的Timeout为10,所以控制端需要在10秒内向服务端发送以下订阅请求,否则服务端将移除此Sid的订阅
1 | SUBSCRIBE /upnp/dev/d04e9f33-4602-2f48-0000-00001d15406d/svc/upnp-org/SeekBarServiceId/event HTTP/1.1 |
控制端取消订阅
控制端直接取消订阅的请求:
1 | UNSUBSCRIBE /upnp/dev/d04e9f33-4602-2f48-0000-00001d15406d/svc/upnp-org/SeekBarServiceId/event HTTP/1.1 |
服务端收到后会取消掉该Sid的订阅。
总结
以上只是使用了一个小Demo验证了DLNA使用的UPnP协议的基本功能,实际的DLNA投屏过程使用的Service和变量比这个要多很多,但基本的原理就是这些。通过本篇文章的技术分析,以及结合实际的使用体验,我们可以发现DLNA投屏的一些问题,比如:
- 没有连接状态的概念。
- 由于没有连接状态的概念,服务端不能主动和控制端断开连接,只有等待控制端主动不投了,那才算断开了。
- 由于是公有协议,不管服务端还是控制端对协议的实现都会有些差异,实际使用中总会有小bug。
另外,UPnP协议中多次提到要考虑一台主机可能存在多个IP地址的情况,比如控制端、服务端所在的主机都可能同时存在可用的eth0、wlan、p2p网络等,需要周全的考虑到各种多IP地址的情况。