正文
话说,翻别人的代码,其实还是容易学到很多东西的。比如说,现在这个砖,早上客户说“手机上的设备发现功能没有了”,震惊我半天,毕竟搬砖很多东西都是黑箱操作的,我只需要了解黑箱的输入输出即可,不用关心黑箱的具体实现,我明明记得这玩意我复制过去了啊,断点也进去了啊,为啥没有啊?
我们先说这个功能,局域网内部的设备发现。不知道mDNS的我第一感觉是这么实现的:
- 首先判断wifi连接状态。
- 然后获取到当前设备的wifi状态下的ip地址。
- 通常设备是发布到固定ip网段内部的,所以再判断一下ip的网段。
- 最后基于最后的网段去ping当前网段内的非自己的ip,ping得通就是可用的。
什么?你说慢?开协程嘛,开线程嘛,ping的时候就ping一次就好嘛。
然而当我打开对应的代码进行调试的时候,emmmm? 并不是通过我的想法实现的。他是通过jmDNS去实现的,那么就来到了知识点盲区环节,那么什么是Multicast DNS呢?
什么是Multicast DNS
这个直接百度百科就可以看到:
mDNS协议发布为 RFC 6762使用IP多播用户数据报协议 (UDP)数据包,由Apple Bonjour和开源Avahi软件包实现。 Android包含mDNS实现。 mDNS也已在Windows 10中实现,最初仅限于发现网络打印机[3] ,后来也能够解析主机名。
mDNS可以与DNS服务发现 (DNS-SD)结合使用, DNS服务发现是RFC 6763中单独指定的配套零配置技术。
mdns 即多播dns(Multicast DNS),mDNS主要实现了在没有传统DNS服务器的情况下使局域网内的主机实现相互发现和通信,使用的端口为5353,遵从dns协议,使用现有的DNS信息结构、名语法和资源记录类型。并且没有指定新的操作代码或响应代码。在局域网中,设备和设备之前相互通信需要知道对方的ip地址的,大多数情况,设备的ip不是静态ip地址,而是通过dhcp协议动态分配的ip 地址,如何设备发现呢,就是要mdns大显身手,例如:现在物联网设备和app之间的通信,要么app通过广播,要么通过组播,发一些特定信息,感兴趣设备应答,实现局域网设备的发现,当然mdns 比这强大。
我更喜欢这种解释:
每一个进入局域网的主机开启了mNDS服务的话,都会向局域网内的所有主机发送消息,我是谁,我的IP地址是多少,然后其他主机就会响应,并告诉你,他是谁,他的IP地址是多少
基于这种逻辑,我们可以获取到需要的IP地址,这么就直接过滤掉了不需要的设备,同时也将自己也加入的需要的设备群里面,其他设备也很方便的与我们通信。
那么JAVA如何去实现呢
jmNDS javadoc org直接将maven 都提供了。通过上面的mDMS的逻辑,去发现设备大致分为2步:
- 将自己添加为设备并广而告之
- 收到其他设备的消息
那么直接开整。首先是导入maven:
implementation "org.jmdns:jmdns:3.5.8"
添加权限
<uses-permission
android:name="android.permission.NEARBY_WIFI_DEVICES"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
当前wifi下的IP地址然后转化为jmdns需要的InetAddress
对象。
创建jmDNS对象:
val address = getInetAddress()
val hostname = address?.hostName
var jmdns= JmDNS.create(address, hostname)
将自己发布成服务
这玩意贼重要,主要是单纯的局域网内部可能没有设备信息。那就只能两台手机,自己发布自己测试了。
val serviceType = "_http._tcp.local."
val serverName = "JmDNS Test"
val port = 1234
val serviceInfo = ServiceInfo.create(serviceType, serverName, port,"描述信息")
//注册服务
jmDNS?.registerService(serviceInfo)
创建jmDNS的监听对象:ServiceListener
并设置操作:
// 添加服务监听。
jmDNS?.addServiceTypeListener(object : ServiceTypeListener{
override fun serviceTypeAdded(event: ServiceEvent) {
LogUtils.e(event.info)
}
override fun subTypeForServiceTypeAdded(event: ServiceEvent) {
LogUtils.e(event.info)
}
})
// 这个是通过type
jmDNS?.addServiceListener("_http._tcp.local.", object : ServiceListener {
override fun serviceAdded(event: ServiceEvent) {
// 请求服务信息。
jmDNS?.requestServiceInfo(event.type, event.name, 1000)
LogUtils.e(event.info)
}
override fun serviceRemoved(event: ServiceEvent) {
// 服务移除
LogUtils.e(event.info)
}
override fun serviceResolved(event: ServiceEvent) {
// 在这里是检索到了服务,但是为啥是这个命名。然后再这里就获取信息就自己处理逻辑,比较服务很多。
LogUtils.e(event.info)
}
})
这里面有一个懵逼的点,那就是 _http._tcp.local.
这个应该可以有很多类型,但是有哪些类型不知道,需要后续检索一下。
为什么需要获取到wifiManger中的MulticastLock然后调用acquire()
先上MulticastLock文档地址 我们可以从文档中直接翻到这句话:
允许应用程序接收 Wifi 多播数据包。通常,Wifi 堆栈会过滤掉未明确寻址到该设备的数据包。获取多播锁将导致堆栈接收寻址到多播地址的数据包。处理这些额外的数据包可能会导致明显的电池消耗,在不需要时应将其禁用。
通过上面的描述我们知道wifi 会过滤掉数据包,所以说,这和我们mDNS的接收其他设备的数据相违背,所以说,我们需要关闭其过滤功能。 获取到 MulticastLock 对象后,是执行了3个函数的。
setReferenceCounted(false)
:控制这是引用计数还是非引用计数多播锁。acquire()
:锁定 Wifi 多播,直至release()
被呼叫。release()
:解锁 Wifi 多播,恢复非专门发送至该设备的数据包过滤器并节省电量。
Android系统支持的网络服务发现
还是先上官网文档Use network service discovery。权限相关的还是和上面的类似。基于wifiManger的特性。所以我们还是手动屏蔽一下,至于NSDManager
是否会自己屏蔽,还没仔细看代码,感觉不用屏蔽。
nsdManger= application.getSystemService<NsdManager>()
获取到当前局域网的ip 并且注册服务
获取ip 信息并且进行转换。
wifiManager?.connectionInfo?.let {
address = InetAddress.getByName(
String.format(
Locale.ENGLISH,
"%d.%d.%d.%d",
it.ipAddress and 0xff,
it.ipAddress shr 8 and 0xff,
it.ipAddress shr 16 and 0xff,
it.ipAddress shr 24 and 0xff
)
)
}
})
注册服务
jobRegistr= lifecycleScope.launch {
var nsdServiceInfo= NsdServiceInfo()
nsdServiceInfo.serviceName="my_nsd_s"
nsdServiceInfo.port=500
nsdServiceInfo.host=address
nsdServiceInfo.serviceType="_http._tcp"
nsdManger?.registerService(nsdServiceInfo,NsdManager.PROTOCOL_DNS_SD,registrationListener)
}
服务的监听:
val registrationListener= object :NsdManager.RegistrationListener{
override fun onRegistrationFailed(p0: NsdServiceInfo?, p1: Int) {
LogUtils.e(p0)
}
override fun onUnregistrationFailed(p0: NsdServiceInfo?, p1: Int) {
LogUtils.e(p0)
}
override fun onServiceRegistered(p0: NsdServiceInfo?) {
LogUtils.e(p0)
}
override fun onServiceUnregistered(p0: NsdServiceInfo?) {
LogUtils.e(p0)
}
}
这么写便于取消注册。当执行到onServiceRegistered
便表示注册成功了,如果没address我写Demo的时候是无法注册成功的。当type=http. tcp.local.会报错,但是为http. tcp 就可以检索到局域网中的mac。
扫描服务
jobDiscovery= lifecycleScope.launch {
nsdManger?.let {
discoveryListener=MyDiscoveryListener(it)
it.discoverServices("_http._tcp",NsdManager.PROTOCOL_DNS_SD,discoveryListener)
}
}
因为discoveryListener写成匿名内部类会泄漏,所以:
class MyDiscoveryListener(val nsdManger:NsdManager):NsdManager.DiscoveryListener{
override fun onStartDiscoveryFailed(p0: String?, p1: Int) {
LogUtils.e(p0)
}
override fun onStopDiscoveryFailed(p0: String?, p1: Int) {
LogUtils.e(p0)
}
override fun onDiscoveryStarted(p0: String?) {
LogUtils.e(p0)
}
override fun onDiscoveryStopped(p0: String?) {
LogUtils.e(p0)
}
override fun onServiceFound(p0: NsdServiceInfo) {
LogUtils.e(p0)
nsdManger.resolveService(p0,object :NsdManager.ResolveListener{
override fun onResolveFailed(p0: NsdServiceInfo?, p1: Int) {
LogUtils.e(p0)
}
override fun onServiceResolved(p0: NsdServiceInfo?) {
LogUtils.e(p0)
// 处理扫描到的设备。
}
})
}
override fun onServiceLost(p0: NsdServiceInfo) {
LogUtils.e(p0)
}
}
onServiceFound
执行的时候,说明扫描到设备了,然后就需要调用:nsdManger.resolveService
去解析参数。在onServiceResolved
中处理相关的逻辑。
添加和扫描到时候,type的值都比较重要。
取消释放资源
private fun cancel() {
lock?.release()
wifiManager=null
nsdManger?.apply {
stopServiceDiscovery(discoveryListener)
unregisterService(registrationListener)
}
jobRegistr?.cancel()
jobDiscovery?.cancel()
jobRegistr=null
jobDiscovery=null
nsdManger=null
}
感觉job 不用暂停。
总结
果然纸上得来终觉浅,绝知此事要躬行
当初写笔记的时候,决定划划水啊,简单的整的很快就决定简单嘛。
当接触到自己未知的领域的时候,就发现到处都难。而且还有很多没有整清楚的地方,这只是一个简单的发现服务而已。比如说type 的定义就是一个默认的板块。然后包下面提供了很多其他功能,也没有看。
当然了,写这个也不是没有收获,比如说,内存泄露相关的以后就可以避免了
内存泄漏
对String扩展toast竟然提示内存泄漏?
只要想法足够骚,就能写出足够震惊的泄露的代码。代码如下。
fun String.showToast(context: Context){
Toast.makeText(context,this,Toast.LENGTH_LONG).show()
}
emmm?还没有看具体的字节码指令,暂时不知道为啥会泄漏。反正不要这么写就行了呗。
系统服务使用activity的context也会泄漏?
这么写提示就会泄漏
wifiManager = getSystemService<WifiManager>()
下面的写法就不会。参考地址 具体为啥,俺不知道,俺也不敢问。
wifiManager = application.getSystemService<WifiManager>()
包含线程调度的代码,回调写成匿名内部类会泄漏
典型代表就是:
var discoveryListener=object:NsdManager.DiscoveryListener{}
不泄露的写法就是,将NsdManager.DiscoveryListener
不写成匿名内部类。这个建议参考:匿名内部类/Lambda Java和Kotlin谁会导致内存泄漏?