NSdManager ResolveListener 错误代码 3:失败已处于活动状态

2023-11-26

我在 Android 应用程序中使用 NsdManager 来发现由我开发的另一台设备发布的 NSD 服务。我只在Android App上进行服务发现(这一侧不需要服务注册)。网络上同时发布了同一类型服务的多个实例。

我开始使用Google提供的示例代码(https://developer.android.com/training/connect-devices-wireless/nsd)但由于同时重用同一个解析器对象进行多个服务解析,我遇到了致命错误。 然后我发现有几个人建议每次创建一个新的解析器对象(例如侦听器已在使用(服务发现)).

我这样做了,致命错误被解决失败错误代码 3 取代,这意味着解决过程处于活动状态。比以前好多了,但是只解决了第一个服务,其余的都因为这次失败而被忽略了。

然后我发现有人建议对错误代码 3 进行特殊处理,递归地重新发送解析请求,直到它最终得到解决(NSNetworkManager.ResolveListener 消息 Android).

我在 Kotlin 中实现了这个解决方案,它确实有效,但我并不是很满意,因为:

  1. 我相信我正在创建很多额外的解析器对象 我不确定它们后来是否被垃圾收集。
  2. 我在循环中重试几次,可能会导致额外的和 给设备和网络带来不必要的负担。不确定我是否 应在再次调用服务解析之前添加短暂的睡眠。
  3. 如果出现网络问题,程序可能会尝试数千次 多次解决相同的服务而不是直接放弃 解决并等待服务再次被发现。

RxBonjour2 的人们提出了一个更复杂、更强大的解决方案,但它太复杂了,我无法遵循它:https://github.com/mannodermaus/RxBonjour/blob/2.x/rxbonjour-drivers/rxbonjour-driver-nsdmanager/src/main/kotlin/de/mannodermaus/rxbonjour/drivers/nsdmanager/NsdManagerDiscoveryEngine.kt

我对 Google 的官方示例没有正确处理这些问题感到沮丧。 nsd_chat 示例使用单个解析器对象,当网络上以相同类型发布多个相同类型的服务时,该示例会失败。

您能提出更好的解决方案吗?或者我下面的代码有什么改进吗?

import android.app.Application
import android.content.Context
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import androidx.lifecycle.AndroidViewModel
import timber.log.Timber


class ViewModel(application: Application) : AndroidViewModel(application) {

    // Get application context
    private val myAppContext: Context = getApplication<Application>().applicationContext

    // Declare DNS-SD related variables for service discovery
    var nsdManager: NsdManager? = null
    private var discoveryListener: NsdManager.DiscoveryListener? = null

    // Constructor for the View Model that is run when the view model is created
    init {

        // Initialize DNS-SD service discovery
        nsdManager = myAppContext.getSystemService(Context.NSD_SERVICE) as NsdManager?

        initializeDiscoveryListener()

        // Start looking for available services in the network
        nsdManager?.discoverServices(NSD_SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener)

    }

    // Instantiate DNS-SD discovery listener
    // used to discover available Sonata audio servers on the same network
    private fun initializeDiscoveryListener() {

        // Instantiate a new DiscoveryListener
        discoveryListener = object : NsdManager.DiscoveryListener {

            override fun onDiscoveryStarted(regType: String) {
                // Called as soon as service discovery begins.
                Timber.d("Service discovery started: $regType")
            }

            override fun onServiceFound(service: NsdServiceInfo) {
                // A service was found! Do something with it
                Timber.d("Service discovery success: $service")
                when {
                    service.serviceType != NSD_SERVICE_TYPE ->
                        // Service type is not the one we are looking for
                        Timber.d("Unknown Service Type: ${service.serviceType}")
                    service.serviceName.contains(NSD_SERVICE_NAME) ->
                        // Both service type and service name are the ones we want
                        // Resolve the service to get all the details
                        startResolveService(service)
                    else ->
                        // Service type is ours but not the service name
                        // Log message but do nothing else
                        Timber.d("Unknown Service Name: ${service.serviceName}")
                }
            }

            override fun onServiceLost(service: NsdServiceInfo) {
                onNsdServiceLost(service)
            }

            override fun onDiscoveryStopped(serviceType: String) {
                Timber.i("Discovery stopped: $serviceType")
            }

            override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
                Timber.e("Start Discovery failed: Error code: $errorCode")
                nsdManager?.stopServiceDiscovery(this)
            }

            override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {
                Timber.e("Stop Discovery failed: Error code: $errorCode")
                nsdManager?.stopServiceDiscovery(this)
            }
        }
    }

    fun startResolveService(service: NsdServiceInfo) {

        val newResolveListener =  object : NsdManager.ResolveListener {

            override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
                // Called when the resolve fails. Use the error code to determine action.
                when (errorCode) {
                    NsdManager.FAILURE_ALREADY_ACTIVE -> {
                        // Resolver was busy
                        Timber.d("Resolve failed: $serviceInfo - Already active")
                        // Just try again...
                        startResolveService(serviceInfo)
                    }
                    else ->
                        Timber.e("Resolve failed: $serviceInfo - Error code: $errorCode")
                }
            }

            override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
                onNsdServiceResolved(serviceInfo)
            }
        }

        nsdManager?.resolveService(service, newResolveListener)
    }

    companion object {

        // We'll only search for NDS services of this type
        const val NSD_SERVICE_TYPE: String = "_servicetype._tcp."
        // and whose names start like this
        const val NSD_SERVICE_NAME: String = "ServiceName-"
    }

    override fun onCleared() {
        try {
            nsdManager?.stopServiceDiscovery(discoveryListener)
        } catch (ignored: Exception) {
            // "Service discovery not active on discoveryListener",
            // thrown if starting the service discovery was unsuccessful earlier
        }
        Timber.d("onCleared called")
        super.onCleared()
    }

    fun onNsdServiceResolved(serviceInfo: NsdServiceInfo) {
        // Logic to handle a new service
        Timber.d("Resolve Succeeded: $serviceInfo")
    }

    fun onNsdServiceLost(service: NsdServiceInfo) {
        // Logic to handle when the network service is no longer available
        Timber.d("Service lost: $service")
    }

}

我通过以下方式解决了这个问题:

  1. 创建线程安全队列来存储待解决的服务
  2. 使用线程安全列表来存储已解析服务的列表
  3. 使用原子布尔标志来查看 ResolveListener 何时繁忙

为了使解决方案更加通用,我构建了一个 NdsHelper 抽象类。它有 2 个必须重写的函数:onNsdServiceResolved(NsdServiceInfo) 和 onNsdServiceLost(NsdServiceInfo)。

我使用 Timber 来记录消息,但您可以用标准 Log 函数替换它们。

这是 NsdHelper 类(Kotlin 代码):

import android.content.Context
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import timber.log.Timber
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.collections.ArrayList

abstract class NsdHelper(val context: Context) {

    // Declare DNS-SD related variables for service discovery
    val nsdManager: NsdManager? = context.getSystemService(Context.NSD_SERVICE) as NsdManager?
    private var discoveryListener: NsdManager.DiscoveryListener? = null
    private var resolveListener: NsdManager.ResolveListener? = null
    private var resolveListenerBusy = AtomicBoolean(false)
    private var pendingNsdServices = ConcurrentLinkedQueue<NsdServiceInfo>()
    var resolvedNsdServices: MutableList<NsdServiceInfo> = Collections.synchronizedList(ArrayList<NsdServiceInfo>())

    companion object {

        // Type of services to look for
        const val NSD_SERVICE_TYPE: String = "_myservicetype._tcp."
        // Services' Names must start with this
        const val NSD_SERVICE_NAME: String = "MyServiceName-"
    }

    // Initialize Listeners
    fun initializeNsd() {
        // Initialize only resolve listener
        initializeResolveListener()
    }

    // Instantiate DNS-SD discovery listener
    // used to discover available Sonata audio servers on the same network
    private fun initializeDiscoveryListener() {

        // Instantiate a new DiscoveryListener
        discoveryListener = object : NsdManager.DiscoveryListener {

            override fun onDiscoveryStarted(regType: String) {
                // Called as soon as service discovery begins.
                Timber.d("Service discovery started: $regType")
            }

            override fun onServiceFound(service: NsdServiceInfo) {
                // A service was found! Do something with it
                Timber.d("Service discovery success: $service")

                if ( service.serviceType == NSD_SERVICE_TYPE &&
                        service.serviceName.startsWith(NSD_SERVICE_NAME) ) {
                    // Both service type and service name are the ones we want
                    // If the resolver is free, resolve the service to get all the details
                    if (resolveListenerBusy.compareAndSet(false, true)) {
                        nsdManager?.resolveService(service, resolveListener)
                    }
                    else {
                        // Resolver was busy. Add the service to the list of pending services
                        pendingNsdServices.add(service)
                    }
                }
                else {
                    // Not our service. Log message but do nothing else
                    Timber.d("Not our Service - Name: ${service.serviceName}, Type: ${service.serviceType}")
                }
            }

            override fun onServiceLost(service: NsdServiceInfo) {
                Timber.d("Service lost: $service")

                // If the lost service was in the queue of pending services, remove it
                var iterator = pendingNsdServices.iterator()
                while (iterator.hasNext()) {
                    if (iterator.next().serviceName == service.serviceName)
                        iterator.remove()
                }

                // If the lost service was in the list of resolved services, remove it
                synchronized(resolvedNsdServices) {
                    iterator = resolvedNsdServices.iterator()
                    while (iterator.hasNext()) {
                        if (iterator.next().serviceName == service.serviceName)
                            iterator.remove()
                    }
                }

                // Do the rest of the processing for the lost service
                onNsdServiceLost(service)
            }

            override fun onDiscoveryStopped(serviceType: String) {
                Timber.i("Discovery stopped: $serviceType")
            }

            override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
                Timber.e("Start Discovery failed: Error code: $errorCode")
                stopDiscovery()
            }

            override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {
                Timber.e("Stop Discovery failed: Error code: $errorCode")
                nsdManager?.stopServiceDiscovery(this)
            }
        }
    }

    // Instantiate DNS-SD resolve listener to get extra information about the service
    private fun initializeResolveListener() {
        resolveListener =  object : NsdManager.ResolveListener {

            override fun onServiceResolved(service: NsdServiceInfo) {
                Timber.d("Resolve Succeeded: $service")

                // Register the newly resolved service into our list of resolved services
                resolvedNsdServices.add(service)

                // Process the newly resolved service
                onNsdServiceResolved(service)

                // Process the next service waiting to be resolved
                resolveNextInQueue()
            }

            override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
                // Called when the resolve fails. Use the error code to debug.
                Timber.e("Resolve failed: $serviceInfo - Error code: $errorCode")

                // Process the next service waiting to be resolved
                resolveNextInQueue()
            }
        }
    }

    // Start discovering services on the network
    fun discoverServices() {
        // Cancel any existing discovery request
        stopDiscovery()

        initializeDiscoveryListener()

        // Start looking for available audio channels in the network
        nsdManager?.discoverServices(NSD_SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener)
    }

    // Stop DNS-SD service discovery
    fun stopDiscovery() {
        if (discoveryListener != null) {
            try {
                nsdManager?.stopServiceDiscovery(discoveryListener)
            } finally {
            }
            discoveryListener = null
        }
    }

    // Resolve next NSD service pending resolution
    private fun resolveNextInQueue() {
        // Get the next NSD service waiting to be resolved from the queue
        val nextNsdService = pendingNsdServices.poll()
        if (nextNsdService != null) {
            // There was one. Send to be resolved.
            nsdManager?.resolveService(nextNsdService, resolveListener)
        }
        else {
            // There was no pending service. Release the flag
            resolveListenerBusy.set(false)
        }
    }

    // Function to be overriden with custom logic for new service resolved
    abstract fun onNsdServiceResolved(service: NsdServiceInfo)

    // Function to be overriden with custom logic for service lost
    abstract fun onNsdServiceLost(service: NsdServiceInfo)
}

这是如何从 ViewModel(或者从活动或片段,如果您更改调用不同帮助器方法的位置)使用它:

import android.app.Application
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.net.nsd.NsdServiceInfo
import androidx.lifecycle.AndroidViewModel
import timber.log.Timber
import java.util.*


class MyViewModel(application: Application) : AndroidViewModel(application) {

    // Get application context
    private val myAppContext: Context = getApplication<Application>().applicationContext

    // Declare NsdHelper object for service discovery
    private val nsdHelper: NsdHelper? = object : NsdHelper(myAppContext) {

        override fun onNsdServiceResolved(service: NsdServiceInfo) {
            // A new network service is available

            // Put your custom logic here!!!

        }

        override fun onNsdServiceLost(service: NsdServiceInfo) {
            // A network service is no longer available

            // Put your custom logic here!!!

        }
    }

    // Block that is run when the view model is created
    init {

        // Initialize DNS-SD service discovery
        nsdHelper?.initializeNsd()

        // Start looking for available audio channels in the network
        nsdHelper?.discoverServices()

    }

    // Called when the view model is destroyed
    override fun onCleared() {
        nsdHelper?.stopDiscovery()
        Timber.d("onCleared called")
        super.onCleared()
    }

}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

NSdManager ResolveListener 错误代码 3:失败已处于活动状态 的相关文章

  • Android应用程序开发中的EditText警告

    在 xml 文件中声明 EditText 时 我收到了如下警告 没有标签视图通过 android labelFor id id start 属性指向此文本字段 编辑文本代码是
  • 如何拦截全局资源加载?

    背景 我需要使用翻译 SDK Lokalise 文档here https docs lokalise com en articles 3487109 lokalise android sdk 2 0 beta 旨在从其服务器加载字符串资源
  • 在 Android 中获取联系方式需要花费大量时间?

    您好 目前正在做一个与联系人相关的项目 我正在从联系人中获取详细信息 电子邮件 电话号码和联系人姓名 效果很好 但问题是获取联系方式需要很长时间 超过 1000 个联系人 包括从社交网站同步的联系人 这样我就放了一个Asynchronous
  • 删除 Android 中切换按钮的填充

    我正在 android 中创建一个简单的切换按钮并将背景设置为可绘制对象
  • onBackPressed 仅关闭 ProgressDialog

    我意识到我的异步任务有一个小问题 我意识到 当我按 Android 设备上的后退按钮来关闭进度对话框和异步任务时 只有我的进度对话框被关闭 而我的异步任务仍在执行 我真的不知道为什么会发生这种情况 所以我只是希望有人能让我回到正确的轨道并帮
  • 标记上存在语法错误,需要 AnnotationName - 查询错误

    我收到了令牌语法错误 AnnotationName 预期出现在以下行 query findInBackground new FindCallback
  • AdMob 广告未显示

    因此 我使用 Play Services SDK 实施了 AdMob 广告 我已经 按照书本 做了所有事情 但广告不会显示 如果我将 AdView 背景设置为白色 它会显示空白 但不显示广告 我正在使用 Fragments 但我将 AdVi
  • 未找到 Google 地图 api v2 类

    我正在使用谷歌地图 api v2 一切正常 今天早上我更新了 sdk 现在地图无法工作 尝试了很多事情 例如再次导入 lib 项目 但似乎没有任何效果 请帮忙 这是 logcat 输出 05 16 08 53 34 327 E dalvik
  • Android Surface 与 Canvas 的关系

    Surface 和 Canvas 之间到底是什么关系 请解释 表面是一个缓冲区 画布保存绘图 视图未附加到画布 也不是表面 窗户被绑在 Surface 和 ViewRoot 询问 随后使用的画布表面 通过要绘制的视图 详细答案你可以阅读这篇
  • com.google.android:android:jar 的 dependency.dependency.version' 丢失

    我正在尝试使用 Eclipse 运行一个简单的虚拟 Android 项目 并且我正在尝试使用 Maven amd 我已按照已接受答案的教程进行操作this https stackoverflow com questions 6735562
  • 删除SD卡上的文件夹

    I tried File delete 但它不起作用 如何删除SD卡上的目录 我正在开发 Android 2 1 在删除目录本身之前 您必须将所有目录清空 请参阅here http www rgagnon com javadetails j
  • 这样理解Activity类似于iOS中的ViewController就可以了吗?

    这样理解Activity类似于iOS中的ViewController就可以了吗 我很困惑接受 Android 中的术语概念 如 活动 服务 等 是的 我想说 Activity 和 ViewController 非常相似 只有一个很大的区别
  • 如何使用 Retrofit 2 和 RxJava 处理分页

    我知道如何处理 Retrofit 响应 但在使用 rx java 处理来自 REST API 的分页时遇到问题 背景 我使用的其余 api 为我提供了以下响应 并在标题中提供了下一页的链接 HTTP 200 OK Allow GET HEA
  • 无法从视图转换为按钮

    我在这里遇到非常令人沮丧的问题 我有这个代码 Button b findViewById android R id button1 我收到了这个错误 类型不匹配 无法将表单视图转换为按钮 但是按钮1is一个按钮 在我的 XML 布局文档中
  • 使用 Delphi 10.2.1 Tokyo 的模态 Android 对话框

    我有以下用于在 Android 上显示模式消息的 Delphi 代码 该代码在 10 1 Berlin 上运行良好 但在 Delphi 10 2 1 Tokyo 上停止运行 此过程现在会挂起 Android 应用程序 procedure c
  • Android 中 Activity 的服务回调

    我有我的 GPSTracker 的摘要 它返回用户的位置 其作品 public class GPSTracker extends Service implements LocationListener public GPSTracker C
  • Android HTTP PUT 请求

    谁能给我一个HTTP PUT请求 Android 的示例代码 假设您想使用 HttpURLConnection 要执行 HTTP PUT 请使用以下命令 URL url new URL http www example com resour
  • Android appwidget 远程视图未更新

    当我从某些活动更新小部件时 列表远程视图不会更新 我的意思是刷新自身 它会出现直到应用程序小部件的更新 日志显示 但不会进入列表视图的适配器以用新数据填充它 public void onUpdate Context context AppW
  • Android smoothScrollTo 不调用 onScrollStateChanged

    我在用smoothScrollBy 滚动到 a 中的特定位置ListView 我希望在以下情况时得到通知ListView完成滚动以将其与当前集成onScrollStateChanged 当用户用手指滚动时触发的事件 目前我正在使用Timer
  • 我收到“循环依赖”Android Dagger Hilt 错误

    我从头开始检查了所有内容 但找不到错误 我找不到错误 可能是什么 我收到以下编译错误 HomeViewModel java 6 error ComponentProcessor MiscError dagger internal codeg

随机推荐

  • 保留数据迭代器的向量

    我有一个功能 void get good items const std vector
  • Spring SecurityContext 在错误页面上返回 null 身份验证

    我正在尝试为 403 访问被拒绝 和 500 内部服务器错误 等错误编写自定义错误页面 它们将从 Velocity 模板呈现 并使用用户的区域设置翻译所有消息 身份验证和区域设置解析在应用程序中运行良好 我在 web xml 中将位置设置为
  • Python 链式属性访问中的无传播[重复]

    这个问题在这里已经有答案了 有没有空传播算子 空感知成员访问 运算符 在Python中 所以我可以写类似的东西 var object children grandchildren property 如 C VB NET 和 TypeScri
  • 为什么 ClassInitialize 修饰方法会使我的所有测试失败?

    我从MSDN了解到 ClassInitialize是标记一个方法 该方法将在所有测试运行之前为所有测试执行一次设置代码 当我在下面的简化装置中包含这样的方法时 所有测试都会失败 我一注释掉 他们又通过了 TestClass public c
  • 获取 SortedList 中 2 个键之间的所有键的最快方法是什么?

    给定一个人口稠密的SortedList
  • 如何在Java中确定给定日期的前一天日期?

    我假设 Java 有一些内置的方法可以做到这一点 给定一个日期 如何确定该日期之前一天的日期 例如 假设给我的是 3 1 2009 上一个日期是 2009 年 2 月 28 日 如果给我的是 3 1 2008 那么之前的日期就是 2 29
  • 在 Swift 的条件语句中将可选值分配给新变量的原因

    我正在浏览 swift 文档 在可选部分中 它讨论了使用问号 表示可能为零的变量 这可以在 if 语句中使用来检查 nil 但在文档中 他们将可选值分配给条件中的新变量 是否有一个原因 例如 它出现在the docs与此类似 Declare
  • 使用循环对数据框进行子集化

    我有一个如下所示的数据框 index ID date Amount 2 1001 2010 06 08 0 21 1001 2010 10 08 10 6 1002 2010 08 16 30 5 1002 2010 11 25 20 9
  • pg.rb 分段错误 [Mojave 升级]

    pg rb 中的分段错误 56 版本 导轨 5 2 0 红宝石 2 4 4 PG宝石 0 20 0 升级到 Mac OS mojave 后 我的本地主机服务器遇到了问题 服务器本身启动正常 但是当尝试通过网络浏览器访问它时 它崩溃了 并且出
  • 行动代表。如何获取调用该方法的实例

    我有一个操作 我想知道如何访问调用该方法的实例 Exemple this FindInstance gt this InstanceOfAClass Method this FindInstance gt this InstanceOfAC
  • RS232串口通信 C# win7 .net Framework 3.5 sp1

    你好 我是 C 串口新手 我写了一个c 程序 运行在winXP和win7上 以在机器发送数据时保留从串口接收到的数据 using System IO using System IO Ports using System Threading
  • 嵌入式Python 2.7.2 从用户定义的目录导入模块

    我将 Python 嵌入到具有定义的 API 的 C C 应用程序中 应用程序需要实例化脚本中定义的类 其结构大致如下 class userscript1 def init self do something here def method
  • 将元组列表转换为字典

    我有一个像这样的元组列表 a 1 a 2 a 3 b 1 b 2 c 1 我想通过第一项迭代此键控 因此 例如 我可以打印如下内容 a 1 2 3 b 1 2 c 1 如果不保留一个项目来跟踪第一个项目是否与我围绕元组循环相同 我该如何去做
  • Gmail 未显示正确的字体

    我正在尝试更改电子邮件的字体以打开 sans 但是 我在 Gmail 呈现正确字体时遇到问题 我设法找到解决 Outlook 问题的方法 这就是我所拥有的 body font face font family Open Sans font
  • vim 中的 Zsh 别名如 !gst?

    有没有办法在 vim 中运行我的 zsh 别名 并将输出发送到新的分割 我正在使用 oh my zsh git 别名 例如gst 而我无法做到 gst在 vim 里面 Thanks Try 设置 shell zsh l 并将别名设置为 zs
  • 将 RTSP 存储到文件位置

    我能够通过 C Winform 应用程序在 Windows 7 64 位机器上流式传输 rtsp 这是我使用的库 VLCD点网这是播放 RTSP 流的代码示例 LocationMedia media new LocationMedia rt
  • ActiveMQ 消费者挂起

    我有一个使用 SSL 传输的 activeMQ 代理 我有大约 10 个消费者正在使用该代理 我正在使用骆驼来配置我的路线 每隔一段时间 它就会挂起并且不会消耗新消息 即使我重新启动消费者 即使队列中有待处理的消息 我开始尝试通过一次一个地
  • Visual Studio 2015。文件未添加到 TFS

    我正在使用 Visual Studio 2015 update 3 以及托管在 Visualstudio com 上的 TFS 当我将 C 类文件添加到 Visual Studio 中的一个项目时 它不会自动添加到源代码管理中 对于同一解决
  • 是什么导致我的 java.net.SocketException: 连接重置? [复制]

    这个问题在这里已经有答案了 我们经常看到但间歇性的java net SocketException Connection reset我们的日志中出现错误 我们不确定在哪里Connection reset错误实际上是从哪里来的 以及如何去调试
  • NSdManager ResolveListener 错误代码 3:失败已处于活动状态

    我在 Android 应用程序中使用 NsdManager 来发现由我开发的另一台设备发布的 NSD 服务 我只在Android App上进行服务发现 这一侧不需要服务注册 网络上同时发布了同一类型服务的多个实例 我开始使用Google提供