Cómo usar la detección del servicio de red

El descubrimiento de servicios de red (NSD) le otorga a tu app acceso a los servicios que otros dispositivos proporcionan en una red local. Los dispositivos compatibles con NSD incluyen impresoras, cámaras web, servidores HTTPS y otros dispositivos móviles.

NSD implementa el mecanismo de descubrimiento de servicios basado en DNS (DNS-SD), que le permite a tu app solicitar servicios especificando un tipo de servicio y el nombre de una instancia de dispositivo que proporcione el tipo de servicio deseado. DNS-SD es compatible con Android y con otras plataformas móviles.

Si agregas NSD a tu app, los usuarios podrán identificar otros dispositivos en la red local que admiten los servicios que tu app solicita. Esto es útil para una variedad de aplicaciones entre pares, como el uso compartido de archivos o los juegos multijugador. Las APIs de NSD de Android simplifican el esfuerzo necesario para implementar esas funciones.

En esta lección, se muestra cómo compilar una aplicación que pueda transmitir su nombre e información de conexión a la red local y buscar información de otras aplicaciones que hagan lo mismo. Por último, en esta lección, se muestra cómo conectarte a la misma app que se ejecuta en otro dispositivo.

Cómo registrar tu servicio en la red

Nota: Este paso es opcional. Si no te interesa transmitir los servicios de tu app en la red local, puedes pasar a la siguiente sección, Cómo descubrir servicios en la red.

Para registrar tu servicio en la red local, primero crea un objeto NsdServiceInfo, que proporciona la información que usan otros dispositivos de la red cuando deciden conectarse a tu servicio.

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);
    ...
}

Este fragmento de código establece el nombre del servicio como "NsdChat". El nombre del servicio es el nombre de la instancia y es el nombre visible para otros dispositivos de la red. Cualquier dispositivo de la red que use NSD para buscar servicios locales podrá ver el nombre. Ten en cuenta que el nombre debe ser único para cualquier servicio de la red, y que Android maneja automáticamente la resolución de conflictos. Si dos dispositivos en la red tienen instalada la aplicación NsdChat, uno de ellos cambia automáticamente el nombre del servicio a uno similar a "NsdChat (1)".

El segundo parámetro establece el tipo de servicio y especifica qué protocolo y capa de transporte usa la aplicación. La sintaxis es "_<protocol>._<transportlayer>". En el fragmento de código, el servicio usa el protocolo HTTP que se ejecuta en TCP. Una aplicación que ofrece un servicio de impresora (por ejemplo, una impresora de red) establecería el tipo de servicio en "_ipp._tcp".

Nota: La Autoridad Internacional de Números Asignados (IANA) administra una lista centralizada y autorizada de los tipos de servicios que utilizan los protocolos de descubrimiento de servicios, como NSD y Bonjour. Puedes descargar la lista de nombres de servicio y números de puertos de IANA. Si deseas usar un nuevo tipo de servicio, debes completar el formulario de registro de puertos y servicios de IANA para reservarlo.

Cuando configures el puerto para tu servicio, evita codificarlo, ya que esto genera un conflicto con otras aplicaciones. Por ejemplo, si suponemos que tu aplicación siempre usa el puerto 1337, se genera un conflicto potencial con otras aplicaciones instaladas que usan el mismo puerto. En su lugar, usa el siguiente puerto disponible del dispositivo. Debido a que esta información se proporciona a otras apps a través de una transmisión de servicio, no es necesario que otras aplicaciones conozcan el puerto que usa tu aplicación en el tiempo de compilación. En cambio, las aplicaciones pueden obtener esta información de la transmisión del servicio, justo antes de conectarse al servicio.

Si trabajas con sockets, puedes inicializar un socket en cualquier puerto disponible con solo establecerlo en 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();
    ...
}

Ahora que definiste el objeto NsdServiceInfo, debes implementar la interfaz RegistrationListener. Esta interfaz contiene devoluciones de llamada que Android usa para alertar a tu aplicación sobre el éxito o la falla del registro o la cancelación del servicio.

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.
        }
    };
}

Ahora tienes todas las piezas para registrar tu servicio. Llama al método registerService().

Ten en cuenta que este método es asíncrono, por lo que cualquier código que deba ejecutarse después de que se registre el servicio debe ir en el método 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);
}

Cómo descubrir servicios en la red

La red está rebosante de vida, desde las impresoras bestiales, las dóciles cámaras web de la red y las brutales y feroces batallas de los jugadores de tres en línea cercanos. La clave para permitir que tu aplicación vea este vibrante ecosistema de funcionalidades es el descubrimiento de servicios. Tu aplicación necesita escuchar transmisiones de servicios en la red para ver qué servicios están disponibles y filtrar cualquier elemento con el que la aplicación no pueda funcionar.

El descubrimiento de servicios, como el registro de servicios, tiene dos pasos: configurar un objeto de escucha de descubrimiento con las devoluciones de llamada relevantes y realizar una única llamada a la API asíncrona a discoverServices().

Primero, crea una instancia de una clase anónima que implemente NsdManager.DiscoveryListener. En el siguiente fragmento, se muestra un ejemplo simple:

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);
        }
    };
}

La API de NSD usa los métodos de esta interfaz para informar a tu aplicación cuándo se inicia el descubrimiento, cuándo falla y cuándo se encuentran y se pierden servicios (la pérdida significa que "ya no está disponible"). Ten en cuenta que este fragmento realiza varias verificaciones cuando se encuentra un servicio.

  1. El nombre del servicio encontrado se compara con el nombre del servicio local para determinar si el dispositivo acaba de recibir su propia transmisión (que es válida).
  2. Se verifica el tipo de servicio para verificar que sea un tipo de servicio al que se puede conectar tu aplicación.
  3. Se verifica el nombre del servicio para comprobar la conexión a la aplicación correcta.

No siempre es necesario verificar el nombre del servicio y solo es relevante si deseas conectarte a una aplicación específica. Por ejemplo, es posible que la aplicación solo quiera conectarse a instancias de sí misma que se ejecuten en otros dispositivos. Sin embargo, si la aplicación desea conectarse a una impresora de red, es suficiente con verificar que el tipo de servicio sea "_ipp._tcp".

Después de configurar el objeto de escucha, llama a discoverServices() y pasa el tipo de servicio que tu aplicación debe buscar, el protocolo de descubrimiento que se usará y el objeto de escucha que acabas de crear.

Kotlin

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

Java

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

Cómo conectarse a servicios de la red

Cuando tu aplicación encuentra en la red un servicio al cual conectarse, primero debe determinar la información de conexión de ese servicio mediante el método resolveService(). Implementa un NsdManager.ResolveListener para pasar a este método y úsalo para obtener un NsdServiceInfo que contenga la información de conexión.

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();
        }
    };
}

Una vez que se resuelve el servicio, tu aplicación recibe información detallada del servicio, incluida una dirección IP y un número de puerto. Esto es todo lo que necesitas para crear tu propia conexión de red al servicio.

Cómo anular el registro de tu servicio cuando se cierra la app

Es importante habilitar e inhabilitar la funcionalidad de NSD según corresponda durante el ciclo de vida de la aplicación. Cancelar el registro de la aplicación cuando se cierra ayuda a evitar que otras aplicaciones piensen que aún está activa y que intenten conectarse a ella. Además, el descubrimiento de servicios es una operación costosa y debe detenerse cuando la actividad superior está en pausa y volver a habilitarse cuando se reanuda la actividad. Anula los métodos de ciclo de vida de tu actividad principal y, luego, inserta código para iniciar y detener la transmisión y el descubrimiento del servicio según corresponda.

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);
    }