使用网络服务发现

网络服务发现 (NSD) 使您的应用能够访问其他设备在本地网络上提供的服务。支持 NSD 的设备包括打印机、网络摄像头、HTTPS 服务器和其他移动设备。

NSD 实现基于 DNS 的服务发现 (DNS-SD) 机制,该机制允许您的应用通过指定服务类型和提供所需服务类型的设备实例的名称来请求服务。Android 和其他移动平台都支持 DNS-SD。

在您的应用中添加 NSD 允许您的用户识别本地网络上支持您的应用请求的服务的其他设备。这对于各种点对点应用(例如文件共享或多人游戏)非常有用。Android 的 NSD API 简化了您实现此类功能所需的努力。

本课程将向您展示如何构建一个应用程序,该应用程序可以将其名称和连接信息广播到本地网络,并扫描来自执行相同操作的其他应用程序的信息。最后,本课程将向您展示如何连接到在另一台设备上运行的同一应用程序。

在网络上注册您的服务

注意:此步骤是可选的。如果您不关心在本地网络上广播您的应用服务,您可以跳到下一部分,“在网络上发现服务”。

要在本地网络上注册您的服务,首先创建一个 NsdServiceInfo 对象。此对象提供其他网络设备在决定是否连接到您的服务时使用的信息。

Kotlin

fun registerService(port: Int) {
    // Create the NsdServiceInfo object, and populate it.
    val serviceInfo = NsdServiceInfo().apply {
        // The name is subject to change based on conflicts
        // with other services advertised on the same network.
        serviceName = "NsdChat"
        serviceType = "_nsdchat._tcp"
        setPort(port)
        ...
    }
}

Java

public void registerService(int port) {
    // Create the NsdServiceInfo object, and populate it.
    NsdServiceInfo serviceInfo = new NsdServiceInfo();

    // The name is subject to change based on conflicts
    // with other services advertised on the same network.
    serviceInfo.setServiceName("NsdChat");
    serviceInfo.setServiceType("_nsdchat._tcp");
    serviceInfo.setPort(port);
    ...
}

此代码片段将服务名称设置为“NsdChat”。服务名称是实例名称:它是网络上其他设备可见的名称。该名称对网络上任何使用 NSD 查找本地服务的设备可见。请记住,该名称对于网络上的任何服务都必须是唯一的,并且 Android 会自动处理冲突解决。如果网络上的两台设备都安装了 NsdChat 应用程序,则其中一台设备会自动将服务名称更改为类似“NsdChat (1)”的内容。

第二个参数设置服务类型,指定应用程序使用的协议和传输层。语法为“_<protocol>._<transportlayer>” 。在代码片段中,服务使用运行在 TCP 上的 HTTP 协议。提供打印机服务的应用程序(例如,网络打印机)会将其服务类型设置为“_ipp._tcp”。

注意: 国际分配号码管理局 (IANA) 管理着一个集中式的、权威的服务类型列表,这些服务类型由服务发现协议(如 NSD 和 Bonjour)使用。您可以从IANA 服务名称和端口号列表下载该列表。如果您打算使用新的服务类型,则应通过填写IANA 端口和服务注册表单来预留它。

设置服务端口时,避免硬编码,因为这会与其他应用程序冲突。例如,假设您的应用程序始终使用端口 1337,则它可能与使用相同端口的其他已安装应用程序发生冲突。相反,请使用设备的下一个可用端口。由于此信息通过服务广播提供给其他应用程序,因此您的应用程序使用的端口无需在编译时为其他应用程序所知。相反,应用程序可以在连接到您的服务之前,从您的服务广播中获取此信息。

如果您使用套接字,以下是如何通过将其设置为 0 来初始化任何可用端口的套接字。

Kotlin

fun initializeServerSocket() {
    // Initialize a server socket on the next available port.
    serverSocket = ServerSocket(0).also { socket ->
        // Store the chosen port.
        mLocalPort = socket.localPort
        ...
    }
}

Java

public void initializeServerSocket() {
    // Initialize a server socket on the next available port.
    serverSocket = new ServerSocket(0);

    // Store the chosen port.
    localPort = serverSocket.getLocalPort();
    ...
}

现在您已经定义了NsdServiceInfo对象,您需要实现RegistrationListener接口。此接口包含 Android 用于提醒您的应用程序服务注册和注销成功或失败的回调。

Kotlin

private val registrationListener = object : NsdManager.RegistrationListener {

    override fun onServiceRegistered(NsdServiceInfo: NsdServiceInfo) {
        // Save the service name. Android may have changed it in order to
        // resolve a conflict, so update the name you initially requested
        // with the name Android actually used.
        mServiceName = NsdServiceInfo.serviceName
    }

    override fun onRegistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
        // Registration failed! Put debugging code here to determine why.
    }

    override fun onServiceUnregistered(arg0: NsdServiceInfo) {
        // Service has been unregistered. This only happens when you call
        // NsdManager.unregisterService() and pass in this listener.
    }

    override fun onUnregistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
        // Unregistration failed. Put debugging code here to determine why.
    }
}

Java

public void initializeRegistrationListener() {
    registrationListener = new NsdManager.RegistrationListener() {

        @Override
        public void onServiceRegistered(NsdServiceInfo NsdServiceInfo) {
            // Save the service name. Android may have changed it in order to
            // resolve a conflict, so update the name you initially requested
            // with the name Android actually used.
            serviceName = NsdServiceInfo.getServiceName();
        }

        @Override
        public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
            // Registration failed! Put debugging code here to determine why.
        }

        @Override
        public void onServiceUnregistered(NsdServiceInfo arg0) {
            // Service has been unregistered. This only happens when you call
            // NsdManager.unregisterService() and pass in this listener.
        }

        @Override
        public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
            // Unregistration failed. Put debugging code here to determine why.
        }
    };
}

现在您拥有注册服务的所有要素。调用方法registerService()

请注意,此方法是异步的,因此服务注册后需要运行的任何代码都必须放在onServiceRegistered()方法中。

Kotlin

fun registerService(port: Int) {
    // Create the NsdServiceInfo object, and populate it.
    val serviceInfo = NsdServiceInfo().apply {
        // The name is subject to change based on conflicts
        // with other services advertised on the same network.
        serviceName = "NsdChat"
        serviceType = "_nsdchat._tcp"
        setPort(port)
    }

    nsdManager = (getSystemService(Context.NSD_SERVICE) as NsdManager).apply {
        registerService(serviceInfo, NsdManager.PROTOCOL_DNS_SD, registrationListener)
    }
}

Java

public void registerService(int port) {
    NsdServiceInfo serviceInfo = new NsdServiceInfo();
    serviceInfo.setServiceName("NsdChat");
    serviceInfo.setServiceType("_http._tcp.");
    serviceInfo.setPort(port);

    nsdManager = Context.getSystemService(Context.NSD_SERVICE);

    nsdManager.registerService(
            serviceInfo, NsdManager.PROTOCOL_DNS_SD, registrationListener);
}

发现网络上的服务

网络充满生机,从庞大的网络打印机到温顺的网络摄像头,再到附近井字游戏玩家残酷而激烈的战斗。让您的应用程序看到这个充满活力的功能生态系统的关键是服务发现。您的应用程序需要侦听网络上的服务广播以查看哪些服务可用,并过滤掉应用程序无法使用的任何内容。

服务发现与服务注册类似,也分为两个步骤:设置具有相关回调的发现侦听器,并对discoverServices()进行一次异步 API 调用。

首先,实例化一个实现NsdManager.DiscoveryListener的匿名类。以下代码片段显示了一个简单的示例

Kotlin

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

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

    override fun onServiceFound(service: NsdServiceInfo) {
        // A service was found! Do something with it.
        Log.d(TAG, "Service discovery success$service")
        when {
            service.serviceType != SERVICE_TYPE -> // Service type is the string containing the protocol and
                // transport layer for this service.
                Log.d(TAG, "Unknown Service Type: ${service.serviceType}")
            service.serviceName == mServiceName -> // The name of the service tells the user what they'd be
                // connecting to. It could be "Bob's Chat App".
                Log.d(TAG, "Same machine: $mServiceName")
            service.serviceName.contains("NsdChat") -> nsdManager.resolveService(service, resolveListener)
        }
    }

    override fun onServiceLost(service: NsdServiceInfo) {
        // When the network service is no longer available.
        // Internal bookkeeping code goes here.
        Log.e(TAG, "service lost: $service")
    }

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

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

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

Java

public void initializeDiscoveryListener() {

    // Instantiate a new DiscoveryListener
    discoveryListener = new NsdManager.DiscoveryListener() {

        // Called as soon as service discovery begins.
        @Override
        public void onDiscoveryStarted(String regType) {
            Log.d(TAG, "Service discovery started");
        }

        @Override
        public void onServiceFound(NsdServiceInfo service) {
            // A service was found! Do something with it.
            Log.d(TAG, "Service discovery success" + service);
            if (!service.getServiceType().equals(SERVICE_TYPE)) {
                // Service type is the string containing the protocol and
                // transport layer for this service.
                Log.d(TAG, "Unknown Service Type: " + service.getServiceType());
            } else if (service.getServiceName().equals(serviceName)) {
                // The name of the service tells the user what they'd be
                // connecting to. It could be "Bob's Chat App".
                Log.d(TAG, "Same machine: " + serviceName);
            } else if (service.getServiceName().contains("NsdChat")){
                nsdManager.resolveService(service, resolveListener);
            }
        }

        @Override
        public void onServiceLost(NsdServiceInfo service) {
            // When the network service is no longer available.
            // Internal bookkeeping code goes here.
            Log.e(TAG, "service lost: " + service);
        }

        @Override
        public void onDiscoveryStopped(String serviceType) {
            Log.i(TAG, "Discovery stopped: " + serviceType);
        }

        @Override
        public void onStartDiscoveryFailed(String serviceType, int errorCode) {
            Log.e(TAG, "Discovery failed: Error code:" + errorCode);
            nsdManager.stopServiceDiscovery(this);
        }

        @Override
        public void onStopDiscoveryFailed(String serviceType, int errorCode) {
            Log.e(TAG, "Discovery failed: Error code:" + errorCode);
            nsdManager.stopServiceDiscovery(this);
        }
    };
}

NSD API 使用此接口中的方法来通知您的应用程序何时启动发现、何时失败以及何时发现和丢失服务(丢失表示“不再可用”)。请注意,此代码片段在找到服务时执行了一些检查。

  1. 将找到的服务的服务名称与本地服务的服务名称进行比较,以确定设备是否刚刚拾取了自己的广播(这是有效的)。
  2. 检查服务类型,以验证它是否是您的应用程序可以连接的服务类型。
  3. 检查服务名称,以验证是否连接到正确的应用程序。

检查服务名称并不总是必要的,并且仅在您想要连接到特定应用程序时才相关。例如,应用程序可能只想连接到在其他设备上运行的自身实例。但是,如果应用程序想要连接到网络打印机,则足以看到服务类型为“_ipp._tcp”。

设置侦听器后,调用discoverServices(),传入您的应用程序应查找的服务类型、要使用的发现协议以及您刚刚创建的侦听器。

Kotlin

nsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener)

Java

nsdManager.discoverServices(
        SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener);

连接到网络上的服务

当您的应用程序找到要连接的网络上的服务时,它必须首先使用resolveService()方法确定该服务连接信息。实现一个NsdManager.ResolveListener传递到此方法中,并使用它获取包含连接信息的NsdServiceInfo

Kotlin

private val resolveListener = object : NsdManager.ResolveListener {

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

    override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
        Log.e(TAG, "Resolve Succeeded. $serviceInfo")

        if (serviceInfo.serviceName == mServiceName) {
            Log.d(TAG, "Same IP.")
            return
        }
        mService = serviceInfo
        val port: Int = serviceInfo.port
        val host: InetAddress = serviceInfo.host
    }
}

Java

public void initializeResolveListener() {
    resolveListener = new NsdManager.ResolveListener() {

        @Override
        public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
            // Called when the resolve fails. Use the error code to debug.
            Log.e(TAG, "Resolve failed: " + errorCode);
        }

        @Override
        public void onServiceResolved(NsdServiceInfo serviceInfo) {
            Log.e(TAG, "Resolve Succeeded. " + serviceInfo);

            if (serviceInfo.getServiceName().equals(serviceName)) {
                Log.d(TAG, "Same IP.");
                return;
            }
            mService = serviceInfo;
            int port = mService.getPort();
            InetAddress host = mService.getHost();
        }
    };
}

解析服务后,您的应用程序会收到详细的服务信息,包括 IP 地址和端口号。这是创建您自己的网络连接以连接到服务所需的一切。

在应用程序关闭时注销您的服务

在应用程序的生命周期中适当地启用和禁用 NSD 功能非常重要。在应用程序关闭时注销您的应用程序有助于防止其他应用程序认为它仍然处于活动状态并尝试连接到它。此外,服务发现是一项代价高昂的操作,应在父 Activity 暂停时停止,并在 Activity 恢复时重新启用。覆盖主 Activity 的生命周期方法,并在其中插入代码以根据需要启动和停止服务广播和发现。

Kotlin

    // In your application's Activity

    override fun onPause() {
        nsdHelper?.tearDown()
        super.onPause()
    }

    override fun onResume() {
        super.onResume()
        nsdHelper?.apply {
            registerService(connection.localPort)
            discoverServices()
        }
    }

    override fun onDestroy() {
        nsdHelper?.tearDown()
        connection.tearDown()
        super.onDestroy()
    }

    // NsdHelper's tearDown method
    fun tearDown() {
        nsdManager.apply {
            unregisterService(registrationListener)
            stopServiceDiscovery(discoveryListener)
        }
    }

Java

    // In your application's Activity

    @Override
    protected void onPause() {
        if (nsdHelper != null) {
            nsdHelper.tearDown();
        }
        super.onPause();
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (nsdHelper != null) {
            nsdHelper.registerService(connection.getLocalPort());
            nsdHelper.discoverServices();
        }
    }

    @Override
    protected void onDestroy() {
        nsdHelper.tearDown();
        connection.tearDown();
        super.onDestroy();
    }

    // NsdHelper's tearDown method
    public void tearDown() {
        nsdManager.unregisterService(registrationListener);
        nsdManager.stopServiceDiscovery(discoveryListener);
    }