Usar a descoberta de serviços de rede

A descoberta de serviços de rede (NSD, na sigla em inglês) permite que seu app acesse serviços fornecidos por outros dispositivos em uma rede local. Os dispositivos compatíveis com a NSD incluem impressoras, webcams, servidores HTTPS e outros dispositivos móveis.

A NSD implementa o mecanismo de descoberta de serviços baseada em DNS, que permite que seu app solicite serviços especificando um tipo de serviço e o nome de uma instância de dispositivo que oferece o tipo de serviço desejado. O DNS-SD é compatível com o Android e outras plataformas móveis.

Adicionar a NSD ao seu app permite que os usuários identifiquem outros dispositivos na rede local que são compatíveis com os serviços solicitados pelo app. Isso é útil para vários de aplicativos ponto a ponto, como compartilhamento de arquivos ou jogos multijogador. As APIs NSD do Android simplificam o esforço necessário para a implementação desses recursos.

Esta lição mostra como criar um aplicativo que pode transmitir o nome e as informações de conexão dele para a rede local e procurar informações de outros aplicativos que fazem o mesmo. Além disso, a lição mostra como se conectar ao mesmo aplicativo em execução em outro dispositivo.

Registrar seu serviço na rede

Observação: esta etapa é opcional. Se você não se importa em transmitir os serviços do seu app pela rede local, pule para a próxima seção, Descobrir serviços na rede.

Para registrar seu serviço na rede local, crie um objeto NsdServiceInfo. Esse objeto traz as informações que outros dispositivos na rede usam ao decidir se conectar ao seu serviço.

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

Esse snippet de código define o nome do serviço como "NsdChat". O nome do serviço é o nome da instância, que fica visível para outros dispositivos na rede. O nome fica visível para qualquer dispositivo na rede que esteja usando a NSD para procurar serviços locais. Lembre-se de que cada serviço na rede precisa ter um nome exclusivo e que o Android gerencia automaticamente a resolução de conflitos. Se dois dispositivos na rede tiverem o aplicativo NsdChat instalado, um deles mudará o nome do serviço automaticamente para algo como “NsdChat(1)”.

O segundo parâmetro define o tipo de serviço e especifica o protocolo e a camada de transporte que o aplicativo usa. A sintaxe é "_<protocol>._<transportlayer>". No snippet de código, o serviço usa o protocolo HTTP executado via TCP. Um aplicativo que ofereça um serviço de impressora (por exemplo, uma impressora em rede) definirá o tipo de serviço como "_ipp._tcp".

Observação: a Autoridade internacional para atribuição de números (IANA, na sigla em inglês) gerencia uma lista centralizada e oficial de tipos de serviço usados por protocolos de descoberta de serviços, como NSD e Bonjour. É possível fazer o download dessas informações pela lista da IANA de nomes de serviços e números de porta (link em inglês). Se você pretende usar um novo tipo de serviço, faça a reserva com o preenchimento do Formulário de registro de portas e serviço da IANA (link em inglês).

Ao configurar a porta do seu serviço, evite codificá-la, porque isso causará conflitos com outros aplicativos. Por exemplo, se seu aplicativo sempre usar a porta 1337, ele poderá entrar em conflito com outros aplicativos instalados que usam a mesma porta. Em vez disso, use a próxima porta disponível do dispositivo. Como essas informações são fornecidas a outros apps por uma transmissão de serviço, não é necessário que a porta usada pelo seu aplicativo seja conhecida pelos outros apps no momento da compilação. Em vez disso, os aplicativos podem acessar essas informações a partir da transmissão do serviço imediatamente antes de se conectarem a ele.

Se estiver trabalhando com soquetes, veja como inicializar um soquete para qualquer porta disponível simplesmente definindo-o como 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();
        ...
    }
    

Agora que você definiu o objeto NsdServiceInfo, é preciso implementar a interface RegistrationListener. Essa interface contém callbacks usados pelo Android para alertar seu aplicativo sobre o êxito ou a falha do registro e cancelamento de registro no serviço.

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

Agora você tem tudo o que precisa para registrar seu serviço. Chame o método registerService().

Observe que esse método é assíncrono. Portanto, qualquer código que precise ser executado depois que o serviço for registrado precisará estar no 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);
    }
    

Descobrir serviços na rede

A rede está repleta de vida, das impressoras ferozes e webcams comportadas às batalhas brutais e intensas dos jogadores de jogo da velha. O segredo para permitir que seu aplicativo veja esse ecossistema vibrante de funcionalidades é a descoberta de serviços. Seu app precisa detectar as transmissões de serviço na rede para ver quais serviços estão disponíveis e filtrar qualquer item com que o aplicativo não possa trabalhar.

A descoberta de serviços, assim como o registro de serviço, tem duas etapas: criação de um listener de descoberta com os callbacks relevantes e uma única chamada de API assíncrona para discoverServices().

Primeiro, instancie uma classe anônima que implemente NsdManager.DiscoveryListener. O snippet a seguir mostra um exemplo simples:

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

A API NSD usa os métodos nessa interface para informar seu aplicativo quando a descoberta é iniciada, quando ela falha e quando os serviços são encontrados e perdidos (ou seja, não estão mais disponíveis). Observe que o snippet faz várias verificações quando um serviço é encontrado.

  1. O nome do serviço encontrado é comparado com o nome do serviço local para determinar se o dispositivo detectou uma transmissão própria (o que é válido).
  2. O tipo de serviço é analisado para verificar se é um tipo ao qual seu aplicativo pode se conectar.
  3. O nome do serviço é analisado para verificar a conexão com o aplicativo correto.

Verificar o nome do serviço nem sempre é necessário e só é relevante se você quiser se conectar a um aplicativo específico. Por exemplo, o aplicativo pode apenas querer se conectar a instâncias em execução em outros dispositivos. No entanto, se o aplicativo quiser se conectar a uma impressora em rede, será suficiente ver que o tipo de serviço é "_ipp._tcp".

Depois de configurar o listener, chame discoverServices(), transmitindo o tipo de serviço que o aplicativo precisa procurar, o protocolo de descoberta a ser usado e o listener que você acabou de criar.

Kotlin

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

Java

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

Conectar-se a serviços na rede

Quando o aplicativo encontra um serviço na rede para se conectar, ele precisa primeiro determinar as informações de conexão para esse serviço, usando o método resolveService(). Implemente um NsdManager.ResolveListener para transmitir o método e use-o para receber um NsdServiceInfo contendo as informações de conexão.

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

Depois que o serviço for resolvido, seu aplicativo receberá informações de serviço detalhadas, incluindo um endereço IP e um número de porta. Isso é tudo que você precisa para criar sua própria conexão de rede com o serviço.

Cancelar o registro do seu serviço ao fechar o aplicativo

É importante ativar e desativar a funcionalidade de NSD conforme for adequado durante o ciclo de vida do aplicativo. Cancelar o registro do seu aplicativo ao fechá-lo ajuda a impedir que outros aplicativos pensem que ele ainda está ativo e tentem se conectar a ele. Além disso, a descoberta de serviços é uma operação cara e precisa ser interrompida quando a atividade principal é pausada, e reativada quando a atividade é retomada. Modifique os métodos de ciclo de vida da sua atividade principal e insira o código para iniciar e interromper a transmissão e a descoberta de serviços conforme for adequado.

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