DLNA设备、服务的注册及发现(依赖开源库cling)

本文是跟踪代码的记录,因为wifi网络不太好,不能debug跟踪,后面在能够但不跟踪时,会理一下,设备之间的连接过程,及音视频数据的传递过程。

DLNA中设备的注册、发现主要基于UPNP协议实现,这是微软推行的一个标准。Upnp最大的愿景是希望任何设备只要一接入网络,所有网上的设备马上就能知道有新设备加入,这些设备之间就可以彼此通信。

以下代码分析是基于Cling这个开源库。

首先看设备注册:

  • DLNA设备、服务的注册

设备跟服务的注册流程是一样的,以Device的注册为例,注册的过程实际就是通知router这个设备已经存在了,所以会有一个alive类型的notify到多播地址:239.255.255.250:1900

  1. 设备实例的创建

LocalDevice

第一个参数是设备ID,其中的UDN是全球唯一的标识符,无论是根设备还是其中的嵌入式设备,而且要保持不变,即使设备重启。这个UND将在SSDP中被使用,有统一格式,前缀是uuid:,后面是Upnp厂商指定的UUID后缀。

如:uuid:b7c7c900-6983-f00b-0000-0000264ce182,其中后面的数字实际是一段hashcode,根据自定义的名字加设备标识生成的hashcode。

第二个参数是设备类型,设备类型有固定的命名空间schemas-upnp-org,然后才是具体的类型,如果是渲染端为MediaRenderer,最后是版本号。

完整的设备类型是命名空间+设备类型+版本号。

如:urn:schemas-upnp-org:device:MediaRenderer:1

第三个参数是设备详情,其中friendlyname是给设备起的一个比较友好的显示名字,另外一个列表DLNACaps,标识DLNA的能力,通常是"av-upload", "image-upload", "audio-upload"。

最后一个参数是服务列表,就是这个设备需要支持哪些服务,比如渲染设备要支持:

ConnectionManagerService,AVTransportService,AudioRenderingControl。

  1. 获取UpnpService实例,

Android环境下对应的是AndroidUpnpService接口类型的。这也是Android Upnp应用服务组件的接口。所以UpnpService是以后台服务的形式运行,这样即使应用的Activity退出,后台的服务依然可以继续工作。

应用以bindservice的形式启动运行UpnpService的服务,具体是AndroidUpnpServiceImpl实例。这里提供了一个带有android配置的Upnp堆栈,任何想要访问Upnp stack的activity都要绑定和解绑定这个AndroidUpnpServiceImpl。

这个service的默认实现需要配置一些权限,需要指出的是其中的:<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"/>

获取wifi多播锁的权限,按照开源库的规范api,设备需要获取多播锁,才能正常发送搜索的多播消息,及宣告存在的多播消息。但是咨询了wifi同事,wifi在工作时默认所有数据包都会发送,不会过滤组播包,所以这里对多播锁的获取不是必须的。另外,app本身不需要明确去获取组播锁,这个操作在AndroidRouter工作时已经做了。(测试时,把组播锁相关的代码注释掉,也是可以正常使用的)

Android环境下需要重写对Upnp stack的配置数据,通过AndroidUpnpServiceConfiguration来实现,使用jetty构建streamClient,streamServer。

通过getRegistryMaintenanceIntervalMillis()可以重写registry 维护线程周期间隔,这个维护线程会周期性的更新设备列表,这个函数可以设置间隔多久去更新一次,实际就是每隔多长时间让registry线程运行一次。

通过getExclusiveServiceTypes()过滤掉你不感兴趣的message。

创建AndroidUpnpServiceImpl实例的过程中,会构建RegistryImpl实例,设备的添加就是通过RegistryImpl来完成的,这个RegistryImpl会启动一个后台的维护线程,执行移除过期设备,执行pending的操作等。

创建AndroidUpnpServiceImpl实例的过程中,会构建RouterImpl 实例,具体有AndroidUpnpServiceImpl 完成创建AndroidRouter实例,会配置流监听端口,xml解析库等(AndroidUpnpServiceConfiguration)。AndroidRouter监听网络状态的变化。

到这里UpnpService实例创建完成,后面可以通过UpnpService添加设备。

  1. 通过RegistryImpl的addDevice来完成设备注册。

接着调用LocalItems的add方法继续干活,先是判断是否已经注册过了,如果已经注册过,不重复执行,直接返回。然后进一步完成添加设备的过程。

没有添加过,一步步完成添加过程:

第一步,添加设备下的资源,根据命名空间/upnp下提供的资源,主要有DeviceDescriptor,ServiceDescriptor,ServiceControl,ServiceEvent等资源。

第二步,生成RegistryItem对象,添加到DeviceItems集合中。

第三步,如果需要通告设备存在,发出存在的通知advertiseAlive()。

第四步,回调监听,告诉RegistryListener有本地localDevice设备添加。监听类通常继承DefaultRegistryListener类,并重写其中的设备添加,设备删除方法,具体是在应用的activity中,upnpService实例构建完成,通过upnpService获取到其中的RegistryImpl来添加监听。

DefaultRegistryListener的实现类,通常需要实现:

localDeviceAdded(),

localDeviceRemoved(),

remoteDeviceAdded(),

remoteDeviceRemoved()等方法。

  1. 宣告设备存在

LocalItems.java中的advertiseAlive(),处理设备存在的通知。

这里是通过异步的方式提交一个通知任务,通常任务先睡眠100毫米后在执行,避免对网络造成拥塞。

接着通过RegistryImpl中创建的ProtocolFactoryImpl实例生成一个通知消息。具体是SendingNotificationAlive实例。

这个通知的类型是NotificationSubtype.ALIVE,也就是ALIVE("ssdp:alive"),

SendingNotification这个类,实际是一个Runnable实例,是为注册的本地设备发送一个通知消息。

最后,看下这个通知存在的消息发给了谁?

在确定消息发给谁之前,先看是如何发现本机的网络地址的?也就是本机在局域网中的IP地址。

调用堆栈是:

new UpnpServiceImpl() @ AndroidUpnpServiceImpl.java  >

new AndroidRouter().enable() @UpnpServiceImpl.java  >

enable() @RouterImpl.java  >

new AndroidNetworkAddressFactory() @ AndroidUpnpServiceConfiguration.java

discoverNetworkInterfaces() @NetworkAddressFactoryImpl.java

通过NetworkInterface.getNetworkInterfaces()枚举出当前设备所有的网络接口,包括虚拟的,物理的,从中过滤出本地的IP地址:

tworkAddressFactoryImpl: Discovered usable network interface: wlan0

tworkAddressFactoryImpl: Discovered usable network interface address: 10.21.247.171

enable() @ RouterImpl.java

startInterfaceBasedTransports()在本机的IP地址上初始化一个多播消息的接受端MulticastReceiver。

startAddressBasedTransports()在本地的IP地址上初始化一个StreamServer。

之所以要分析上面的本机IP地址的获取,是因为发送通知消息时会涉及到。应用程序仅仅将数据包发送给组播地址,然后路由器将确保数据包被发送到组播组中的全部主机。

继续看前面的遗留问题,通知存在的消息发给了谁?

Execute() @SendingNotification.java

第一步,获取本机IP地址上初始化的streamServer,

List<NetworkAddress> activeStreamServers =

getUpnpService().getRouter().getActiveStreamServers(null);

NetworkAddress.java类型,包含了三个属性,本机IP地址,端口,本机物理地址(MAC)。

第二步,封装一个带有本地设备(渲染器,非当前手机),本机上streamServer的描述对象Location。

List<Location> descriptorLocations = new ArrayList();

for (NetworkAddress activeStreamServer : activeStreamServers) {

descriptorLocations.add(

new Location(

activeStreamServer,

getUpnpService().getConfiguration().getNamespace().getDescriptorPath(getDevice())

)

);

}

其中的参数,getDevice(),是最开始创建的LocalDevice 实例。

Location实例包含了两个属性,一个是本机当前可用的streamServer,一个是本地设备对应的URI值。Web上可用的每一种资源都有一个通用资源标识符URI进行定位。

本地设备(DMR)对应的

URI:/upnp/dev/b7c7c900-6983-f00b-0000-0000264ce182/desc

第三步,发送消息,因为是基于udp协议发送,默认间隔150毫秒,重复发送三次。

Execute()@SendingNotification.java

for (Location descriptorLocation : descriptorLocations) {

sendMessages(descriptorLocation);

}

把descriptorLocation添加上localdevice包装成OutgoingNotificationRequest消息类型。

这个过程会添加消息头:

UpnpHeader.Type.NT: (RootDeviceHeader) 'upnp:rootdevice'

UpnpHeader.Type.USN: (USNRootDeviceHeader) 'uuid:b7c7c900-6983-f00b-0000-0000264ce182'

如有物理地址:

UpnpHeader.Type.EXT_IFACE_MAC: (InterfaceMacHeader) '00:0A:F5:06:0F:24'

  1. 数据包IO口的初始化(负责数据包的发送)

从上一步转入RouterImpl.java中的send()方法,这里用DatagramIO完成数据包的发送。

先看DatagramIO的初始化,因为绑定到一个IP地址上,这个过程是在startAddressBasedTransports()中,获取到本机IP地址后完成的。

DatagramIOImpl是具体的类实现,构造实例时,可以配置发送参数,如跳数,发送数据包的最大值。

数据包内容的读写通过DatagramProcessorImpl.java来完成。

接着看DatagramIOImpl的初始化。

Init() @ DatagramIOImpl.java

在本机IP地址的基础上封装端口:

localAddress = new InetSocketAddress(bindAddress, 0);

封装一个多播端口,以便把数据包发送到多个client端:

socket = new MulticastSocket(localAddress);

最后数据包的发送是通过MulticastSocket完成。

send() @ DatagramIOImpl.java

发送的数据包会通过DatagramProcessorImpl.java的write方法来构建DatagramPacket对象。

数据包发给谁了呢?这要看message中目标地址destinationAddress是谁?

消息报的目标地址,是在OutgoingNotificationRequest.java的构造函数中设置的。

前面说过,要发送的消息会被包装成OutgoingNotificationRequest类型,或者其子类型OutgoingNotificationRequestRootDevice, OutgoingNotificationRequestUDN,或OutgoingNotificationRequestDeviceType,具体是在:

createDeviceMessages()@SendingNotification.java

最后看OutgoingNotificationRequest.java的构造函数:

super(

new UpnpRequest(UpnpRequest.Method.NOTIFY),

ModelUtil.getInetAddressByName(Constants.IPV4_UPNP_MULTICAST_GROUP),

Constants.UPNP_MULTICAST_PORT

);

其中,消息类型是NOTIFY,

相应的目标地址:IPV4_UPNP_MULTICAST_GROUP = "239.255.255.250";

目标端口:UPNP_MULTICAST_PORT = 1900;

这是IANA(互联网数字分配组织)保留的多播地址。

  • DLNA设备的发现

这个过程是从发出search请求开始,到收到匹配设备返回的响应消息,最后,应用程序根据响应消息显示出RemoteDevice信息。

1,发送请求

首先,注册监听DefaultRegistryListener.java,可以统一来用添加、删除、更新本地或远端设备。

upnpService.getRegistry().addListener(deviceListRegistryListener);

它的实现类可以被多个线程并发调用,所以他应该是线程安全的。其中监听的方法都是在一个单独的线程被调用的。

如remoteDeviceAdded(),在一个新发现的设备的完整元数据可用时被调用。

然后,从应用中的upnpService.getControlPoint().search();开始。通过ControlPointImpl.java发起搜索。

不带参数的search()发起搜索类型是:STAllHeader() > "ssdp:all"。

通过DefaultUpnpServiceConfiguration.java中的,ClingExecutor实例将搜索任务被放入一个线程池。

具体搜索任务的类型,通过ProtocolFactoryImpl.java 中的createSendingSearch()来创建。

接着看SendingSearch.java,搜索数据包的信息,搜索请求消息没间隔500毫秒,发送一次,共发送5次。

execute()@SendingSearch.java

OutgoingSearchRequest.java

super(

new UpnpRequest(UpnpRequest.Method.MSEARCH),

ModelUtil.getInetAddressByName(Constants.IPV4_UPNP_MULTICAST_GROUP),

Constants.UPNP_MULTICAST_PORT

);

搜索请求,类型:"M-SEARCH"

目标地址:IPV4_UPNP_MULTICAST_GROUP = "239.255.255.250";

目标端口:UPNP_MULTICAST_PORT = 1900;

getUpnpService().getRouter().send(msg); 转到RouterImpl.java通过数据包IO接口实例发出消息。

Send()@DatagramIOImpl.java

通过DatagramProcessorImpl.java的write完成发送数据包的填充。

最后通过MulticastSocket的send完成发送。

2,接受响应

MulticastSocket是绑定了本机IP地址的套接字,能发送单播、多播数据包,也能接受单播数据包。

在RouterImpl.java中的startAddressBasedTransports()方法,对DatagramIOImpl初始化后,会将其放入线程池运行,所以直接看DatagramIOImpl的run方法。

run() @DatagramIOImpl.java

这里的socket是MulticastSocket对象,负责监听所有发送到本机IP的UDP数据包。

byte[] buf = new byte[getConfiguration().getMaxDatagramBytes()];

DatagramPacket datagram = new DatagramPacket(buf, buf.length);

socket.receive(datagram);

这里的router是RouterImpl.java对象,处理前还是由DatagramProcessorImpl.java中的read方法解读DatagramPacket,转成IncomingDatagramMessage类型对象。

router.received(datagramProcessor.read(localAddress.getAddress(), datagram));

然后看RouterImpl.java如何把接收的响应信息,回到到监听处。

根据消息类型,ProtocolFactoryImpl.java中createReceivingAsync方法负责构建搜索响应的实例。

具体是createReceivingSearchResponse(incomingResponse)方法,最终创建的ReceivingSearchResponse实例,这个实例来处理搜索响应消息的接收。

同样的,ReceivingSearchResponse实例,也会被添加到线程池执行,具体看其execute方法。

execute()@ReceivingSearchResponse.java

这里获取返回搜索响应的远端设备的描述信息,

RemoteDeviceIdentity rdIdentity = new RemoteDeviceIdentity(getInputMessage());

根据远端设备的描述信息,构建RemoteDevice对象。

rd = new RemoteDevice(rdIdentity);

因为这个时候并不知道,这个远端设备是根设备,还是嵌入设备,所以还要去解析它的描述符,然后在处理添加。具体是:

new RetrieveRemoteDescriptors(getUpnpService(), rd)

这个runnable也会在线程池被执行。

run()@RetrieveRemoteDescriptors.java

其中describe()的调用非常耗时,需要确保每次的调用都是必须的,且尽量少的调用。

出去描述符文件等的解析外,跟设备添加有关的调用是:

getUpnpService().getRegistry().addDevice(hydratedDevice);

addDevice(RemoteDevice remoteDevice) @ RegistryImpl.java

add(final RemoteDevice device) @ RemoteItems.java

如下循环完成远端设备的添加:

for (final RegistryListener listener : registry.getListeners()) {

registry.getConfiguration().getRegistryListenerExecutor().execute(

new Runnable() {

public void run() {

listener.remoteDeviceAdded(registry, device);

}

}

);

}

通过RegistryListener的具体实现类,把远端设备添加到应用列表中。如DefaultRegistryListener.java,通常应用程序都会集成这个类,并实现其中的接口:

remoteDeviceAdded()

remoteDeviceUpdated()

等。

https://www.cnblogs.com/guxia/p/8076099.html Jetty使用

https://blog.csdn.net/qq_37878579/article/details/78404931 Jetty使用

https://blog.csdn.net/hknock/article/details/44243675 multicastSocket使用

DLNA设备、服务的注册及响应相关推荐

  1. JAVA层HIDL服务的注册原理-Android10.0 HwBinder通信原理(八)

    摘要:本节主要来讲解Android10.0 JAVA层HIDL服务的注册原理 阅读本文大约需要花费22分钟. 文章首发微信公众号:IngresGe 专注于Android系统级源码分析,Android的 ...

  2. Native层HIDL服务的注册原理-Android10.0 HwBinder通信原理(六)

    摘要:本节主要来讲解Android10.0 Native层HIDL服务的注册原理 阅读本文大约需要花费23分钟. 文章首发微信公众号:IngresGe 专注于Android系统级源码分析,Androi ...

  3. 服务发现 注册中心 consul 的介绍、部署和使用

    什么是服务发现 微服务的框架体系中,服务发现是不能不提的一个模块.我相信了解或者熟悉微服务的童鞋应该都知道它的重要性.这里我只是简单的提一下,毕竟这不是我们的重点.我们看下面的一幅图片: 图中,客户端 ...

  4. 微服务2——服务的注册,调用(Nacos服务注册中心+服务调用+调用负载均衡)sca-comsumersca-provider

    一.Nacos的安装和构建  以及启动 其官网地址如下: Nacos官网 1.安装前提: 第一:确保你电脑已配置JAVA_HOME环境变量(Nacos启动时需要),例如: 第二:确保你的MySQL版本 ...

  5. 注册表 关闭打印机服务器,Win7系统添加打印机无Print Spooler服务无注册表解决方法...

    win764位系统刚装几天发现笔记本无法安装虚拟打印机,因为我经常使用PDF打印,比如cutePDF打印机.开始搜索各种经验,发现都无法解决问题, 总有各种疏漏,在此总结一下,供自己回顾,同时希望能惠 ...

  6. 微服务:注册中心ZooKeeper、Eureka、Consul 、Nacos对比

    前言 服务注册中心本质上是为了解耦服务提供者和服务消费者.对于任何一个微服务,原则上都应存在或者支持多个提供者,这是由微服务的分布式属性决定的.更进一步,为了支持弹性扩缩容特性,一个微服务的提供者的数 ...

  7. 边缘计算开源框架EdgeXFoundry的部署应用开发(三)设备服务开发

    边缘计算开源框架EdgeXFoundry的部署应用开发(三)设备服务开发 使用SDK开发真实设备接入服务 着手编写一个温湿度设备接入 准备相关文件及目录 脚本可选,用于单文件编译测试 编写温湿度设备接 ...

  8. 蓝牙基带分配编号(设备/服务类型)详解

    基带分配编号 为基带分配的编号标识了查询访问代码和设备/服务类别(CoD)字段. 通用和特定于设备的查询访问代码(DIAC) 该查询访问码(IAC)是寻找过滤的第一级  的蓝牙®  设备和服务.定义多 ...

  9. 一文吃透何为微服务、网关、服务发现/注册?

    点击上方"Java基基",选择"设为星标" 做积极的人,而不是积极废人! 每天 14:00 更新文章,每天掉亿点点头发... 源码精品专栏 原创 | Java ...

最新文章

  1. C++ 笔记(32)— 预处理、文件包含include、宏替换define、条件包含ifndef、define
  2. UI设计培训分享:2021年UI设计风格新风向标主要体现在哪些方面
  3. 【CTF大赛】陇剑杯-机密内存-解题过程分析
  4. 牛客网 对称平方数【回文数的判断 两个vector是否相等】
  5. java提高篇之理解java的三大特性——多态
  6. Wordpress:将图片、post等的URL转换为相对路径
  7. Bye Bye Embed-再见了Embed,符合web标准的媒体播放器代码
  8. 【Vue.js学习】生命周期及数据绑定
  9. 征稿 | “健康知识图谱”投稿通道开启
  10. 计算机jsp外文文献,计算机 JSP web 外文翻译 外文文献 .doc
  11. python报考软考哪个比较好_软考高级考哪个好?哪个比较热门?
  12. Mysql数据库设计规范之四数据库操作行为规范
  13. SQL运行速度慢?查查中间件
  14. Query Designer中的特征限制(Characteristic Restrictions)、缺省值(Default Values)、自由特性(Free Characteristics)...
  15. CentOS6.9 minimal版本安装图形化界面
  16. iphone6主板注释
  17. f2fs学习笔记 - 4. f2fs文件系统组件说明
  18. Linux自学之旅-基础命令(一)
  19. 51单片机延时程序(以延时30ms为例)
  20. 使用sessionStorage实现页面间传值与传对象

热门文章

  1. ubuntu16.04安装浏览器
  2. 论文写作课的收获与体会
  3. CSS快速记忆笔记(三)
  4. AI自动播——AI虚拟主播帮你实现24小时直播带货技术分享
  5. [R]第一节 初始R语言
  6. tcpdump命令个人笔记
  7. 软件测试面试题:缺陷提交的流程
  8. C语言:一只公鸡值五钱,一只母鸡值三钱,三只小鸡值一钱,现在要用百钱买百鸡,请问公鸡、母鸡、小鸡各多少只?
  9. C#将html导出到word(基于wps)
  10. Mac版酷狗在线播放器