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 que outros dispositivos fornecem em uma rede local. Os dispositivos com suporte 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 (DNS-SD), que permite que seu app solicite serviços especificando um tipo de serviço e o nome de uma instância de dispositivo que fornece o tipo de serviço desejado. O DNS-SD é compatível com o Android e outras plataformas móveis.

Adicionar a NSD ao app permite que os usuários identifiquem outros dispositivos na rede local que têm suporte aos serviços solicitados pelo app. Isso é útil para uma variedade de aplicativos ponto a ponto, como compartilhamento de arquivos ou jogos multiplayer. 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 as informações de nome e conexão dele para a rede local e procurar informações de outros aplicativos que fazem o mesmo. Por fim, 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 app pela rede local, avance 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 apresenta as informações que outros dispositivos na rede usam quando estão decidindo 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, ou seja, o nome visível para outros dispositivos na rede. O nome fica visível para qualquer dispositivo na rede que use a NSD para procurar serviços locais. Lembre-se de que o nome precisa ser exclusivo para qualquer serviço na rede, e o Android gerencia automaticamente a resolução de conflitos. Se dois dispositivos na rede tiverem o app NsdChat instalado, um deles vai mudar o nome do serviço automaticamente para algo como "NsdChat (1)".

O segundo parâmetro define o tipo de serviço e especifica qual protocolo e camada de transporte o aplicativo usa. A sintaxe é "_<protocol>._<transportlayer>". No snippet de código, o serviço usa o protocolo HTTP executado pelo TCP. Um aplicativo que ofereça um serviço de impressora (por exemplo, uma impressora de 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 da lista da IANA de nomes de serviço e números de porta (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 (em inglês).

Ao configurar a porta do seu serviço, evite fixá-la no código, porque isso entre em conflito com outros aplicativos. Por exemplo, supondo que seu aplicativo sempre use 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 aplicativo seja conhecida por outros apps durante a compilação. Em vez disso, os aplicativos podem receber essas informações da transmissão do serviço logo antes de se conectar a ele.

Para inicializar um soquete em qualquer porta disponível, basta defini-lo como 0 caso esteja trabalhando com soquetes.

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 sucesso ou a falha do registro e cancelamento do registro do 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 precisa 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 de webcams com redes fósseis, até as batalhas brutais e ferozes dos jogadores de jogo da velha nas proximidades. A chave para permitir que seu aplicativo veja esse vibrante ecossistema de funcionalidades é a descoberta de serviços. Seu aplicativo precisa ouvir transmissões de serviços na rede para ver quais serviços estão disponíveis e filtrar qualquer coisa com que não possa trabalhar.

A descoberta de serviços, assim como o registro de serviço, tem duas etapas: configuraçã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 ao aplicativo quando a descoberta é iniciada, quando ela falha e quando os serviços são encontrados e perdidos (ou seja, "perdido" significa "não está mais disponível"). O snippet faz várias verificações quando um serviço é encontrado.

  1. O nome do serviço encontrado é comparado ao nome do serviço local para determinar se o dispositivo capturou a própria transmissão (o que é válido).
  2. O tipo de serviço é verificado para confirmar que é um tipo a que seu aplicativo pode se conectar.
  3. O nome do serviço é verificado 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 querer se conectar apenas a instâncias em execução em outros dispositivos. No entanto, se o aplicativo quiser se conectar a uma impressora de 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 seu 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 seu 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 esse 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 NSD conforme apropriado durante o ciclo de vida do aplicativo. Cancelar o registro do seu aplicativo quando ele for fechado ajuda a evitar 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 mãe é pausada e reativada quando a atividade é retomada. Modifique os métodos do 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 apropriado.

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