DLNA设备、服务的注册及响应
DLNA设备、服务的注册及发现(依赖开源库cling)
本文是跟踪代码的记录,因为wifi网络不太好,不能debug跟踪,后面在能够但不跟踪时,会理一下,设备之间的连接过程,及音视频数据的传递过程。
DLNA中设备的注册、发现主要基于UPNP协议实现,这是微软推行的一个标准。Upnp最大的愿景是希望任何设备只要一接入网络,所有网上的设备马上就能知道有新设备加入,这些设备之间就可以彼此通信。
以下代码分析是基于Cling这个开源库。
首先看设备注册:
- DLNA设备、服务的注册
设备跟服务的注册流程是一样的,以Device的注册为例,注册的过程实际就是通知router这个设备已经存在了,所以会有一个alive类型的notify到多播地址:239.255.255.250:1900
- 设备实例的创建
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。
- 获取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添加设备。
- 通过RegistryImpl的addDevice来完成设备注册。
接着调用LocalItems的add方法继续干活,先是判断是否已经注册过了,如果已经注册过,不重复执行,直接返回。然后进一步完成添加设备的过程。
没有添加过,一步步完成添加过程:
第一步,添加设备下的资源,根据命名空间/upnp下提供的资源,主要有DeviceDescriptor,ServiceDescriptor,ServiceControl,ServiceEvent等资源。
第二步,生成RegistryItem对象,添加到DeviceItems集合中。
第三步,如果需要通告设备存在,发出存在的通知advertiseAlive()。
第四步,回调监听,告诉RegistryListener有本地localDevice设备添加。监听类通常继承DefaultRegistryListener类,并重写其中的设备添加,设备删除方法,具体是在应用的activity中,upnpService实例构建完成,通过upnpService获取到其中的RegistryImpl来添加监听。
DefaultRegistryListener的实现类,通常需要实现:
localDeviceAdded(),
localDeviceRemoved(),
remoteDeviceAdded(),
remoteDeviceRemoved()等方法。
- 宣告设备存在
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'
- 数据包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设备、服务的注册及响应相关推荐
- JAVA层HIDL服务的注册原理-Android10.0 HwBinder通信原理(八)
摘要:本节主要来讲解Android10.0 JAVA层HIDL服务的注册原理 阅读本文大约需要花费22分钟. 文章首发微信公众号:IngresGe 专注于Android系统级源码分析,Android的 ...
- Native层HIDL服务的注册原理-Android10.0 HwBinder通信原理(六)
摘要:本节主要来讲解Android10.0 Native层HIDL服务的注册原理 阅读本文大约需要花费23分钟. 文章首发微信公众号:IngresGe 专注于Android系统级源码分析,Androi ...
- 服务发现 注册中心 consul 的介绍、部署和使用
什么是服务发现 微服务的框架体系中,服务发现是不能不提的一个模块.我相信了解或者熟悉微服务的童鞋应该都知道它的重要性.这里我只是简单的提一下,毕竟这不是我们的重点.我们看下面的一幅图片: 图中,客户端 ...
- 微服务2——服务的注册,调用(Nacos服务注册中心+服务调用+调用负载均衡)sca-comsumersca-provider
一.Nacos的安装和构建 以及启动 其官网地址如下: Nacos官网 1.安装前提: 第一:确保你电脑已配置JAVA_HOME环境变量(Nacos启动时需要),例如: 第二:确保你的MySQL版本 ...
- 注册表 关闭打印机服务器,Win7系统添加打印机无Print Spooler服务无注册表解决方法...
win764位系统刚装几天发现笔记本无法安装虚拟打印机,因为我经常使用PDF打印,比如cutePDF打印机.开始搜索各种经验,发现都无法解决问题, 总有各种疏漏,在此总结一下,供自己回顾,同时希望能惠 ...
- 微服务:注册中心ZooKeeper、Eureka、Consul 、Nacos对比
前言 服务注册中心本质上是为了解耦服务提供者和服务消费者.对于任何一个微服务,原则上都应存在或者支持多个提供者,这是由微服务的分布式属性决定的.更进一步,为了支持弹性扩缩容特性,一个微服务的提供者的数 ...
- 边缘计算开源框架EdgeXFoundry的部署应用开发(三)设备服务开发
边缘计算开源框架EdgeXFoundry的部署应用开发(三)设备服务开发 使用SDK开发真实设备接入服务 着手编写一个温湿度设备接入 准备相关文件及目录 脚本可选,用于单文件编译测试 编写温湿度设备接 ...
- 蓝牙基带分配编号(设备/服务类型)详解
基带分配编号 为基带分配的编号标识了查询访问代码和设备/服务类别(CoD)字段. 通用和特定于设备的查询访问代码(DIAC) 该查询访问码(IAC)是寻找过滤的第一级 的蓝牙® 设备和服务.定义多 ...
- 一文吃透何为微服务、网关、服务发现/注册?
点击上方"Java基基",选择"设为星标" 做积极的人,而不是积极废人! 每天 14:00 更新文章,每天掉亿点点头发... 源码精品专栏 原创 | Java ...
最新文章
- C++ 笔记(32)— 预处理、文件包含include、宏替换define、条件包含ifndef、define
- UI设计培训分享:2021年UI设计风格新风向标主要体现在哪些方面
- 【CTF大赛】陇剑杯-机密内存-解题过程分析
- 牛客网 对称平方数【回文数的判断 两个vector是否相等】
- java提高篇之理解java的三大特性——多态
- Wordpress:将图片、post等的URL转换为相对路径
- Bye Bye Embed-再见了Embed,符合web标准的媒体播放器代码
- 【Vue.js学习】生命周期及数据绑定
- 征稿 | “健康知识图谱”投稿通道开启
- 计算机jsp外文文献,计算机 JSP web 外文翻译 外文文献 .doc
- python报考软考哪个比较好_软考高级考哪个好?哪个比较热门?
- Mysql数据库设计规范之四数据库操作行为规范
- SQL运行速度慢?查查中间件
- Query Designer中的特征限制(Characteristic Restrictions)、缺省值(Default Values)、自由特性(Free Characteristics)...
- CentOS6.9 minimal版本安装图形化界面
- iphone6主板注释
- f2fs学习笔记 - 4. f2fs文件系统组件说明
- Linux自学之旅-基础命令(一)
- 51单片机延时程序(以延时30ms为例)
- 使用sessionStorage实现页面间传值与传对象