Utiliser la détection de services réseau

La détection de services réseau permet à votre application d'accéder aux services fournis par d'autres appareils sur un réseau local. Les appareils compatibles avec NSD incluent les imprimantes, les webcams, les serveurs HTTPS et d'autres appareils mobiles.

NSD met en œuvre le mécanisme de détection de services basé sur DNS (DNS-SD), qui permet à votre application de demander des services en spécifiant un type de service et le nom d'une instance d'appareil qui fournit le type de service souhaité. DNS-SD est compatible à la fois sur Android et sur d'autres plates-formes mobiles.

L'ajout de NSD à votre application permet à vos utilisateurs d'identifier d'autres appareils du réseau local qui prennent en charge les services demandés par votre application. Cela est utile pour diverses applications peer-to-peer, telles que le partage de fichiers ou les jeux multijoueurs. Les API NSD d'Android simplifient les efforts requis pour implémenter ces fonctionnalités.

Cette leçon explique comment créer une application capable de diffuser son nom et ses informations de connexion sur le réseau local, et de rechercher des informations provenant d'autres applications qui en font de même. Enfin, cette leçon vous montre comment vous connecter à la même application exécutée sur un autre appareil.

Enregistrer votre service sur le réseau

Remarque : Cette étape est facultative. Si vous ne souhaitez pas diffuser les services de votre application sur le réseau local, vous pouvez passer à la section suivante, Découvrir les services sur le réseau.

Pour enregistrer votre service sur le réseau local, commencez par créer un objet NsdServiceInfo. Cet objet fournit les informations utilisées par les autres appareils du réseau lorsqu'ils décident de se connecter à votre service.

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

Cet extrait de code définit le nom du service sur "NsdChat". Le nom du service est le nom de l'instance: il s'agit du nom visible par les autres appareils du réseau. Ce nom est visible par tous les appareils du réseau qui utilisent NSD pour rechercher des services locaux. N'oubliez pas que le nom doit être unique pour chaque service du réseau et qu'Android gère automatiquement la résolution des conflits. Si l'application NsdChat est installée sur deux appareils du réseau, l'un d'eux remplace automatiquement le nom du service par "NsdChat (1)".

Le deuxième paramètre définit le type de service, ainsi que le protocole et la couche de transport utilisés par l'application. La syntaxe est "_<protocol>._<transportlayer>". Dans l'extrait de code, le service utilise le protocole HTTP qui s'exécute via TCP. Une application offrant un service d'impression (par exemple, une imprimante réseau) définirait le type de service sur "_ipp._tcp".

Remarque : L'IANA (International Assigned Numbers Authority) gère une liste centralisée et faisant autorité des types de services utilisés par les protocoles de détection de services tels que NSD et Bonjour. Vous pouvez la télécharger à partir de la liste IANA des noms de service et des numéros de port. Si vous avez l'intention d'utiliser un nouveau type de service, vous devez le réserver en remplissant le formulaire d'enregistrement de services et de ports IANA.

Lorsque vous définissez le port de votre service, évitez de le coder en dur, car cela crée un conflit avec d'autres applications. Par exemple, si votre application utilise toujours le port 1337, cela peut créer un conflit avec d'autres applications installées qui utilisent le même port. Utilisez plutôt le prochain port disponible de l'appareil. Étant donné que ces informations sont fournies à d'autres applications par une diffusion de service, il n'est pas nécessaire que le port utilisé par votre application soit connu des autres applications au moment de la compilation. À la place, les applications peuvent obtenir ces informations à partir de la diffusion de votre service, juste avant de s'y connecter.

Si vous travaillez avec des sockets, voici comment initialiser un socket sur n'importe quel port disponible en le définissant simplement sur 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();
    ...
}

Maintenant que vous avez défini l'objet NsdServiceInfo, vous devez implémenter l'interface RegistrationListener. Cette interface contient des rappels utilisés par Android pour alerter votre application du succès ou de l'échec de l'enregistrement ou de l'annulation de l'inscription au service.

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

Vous disposez maintenant de tous les éléments nécessaires pour enregistrer votre service. Appelez la méthode registerService().

Notez que cette méthode est asynchrone. Par conséquent, tout code qui doit être exécuté après l'enregistrement du service doit être utilisé dans la méthode 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);
}

Découvrir les services sur le réseau

Le réseau regorge de vie, des imprimantes réseau les plus monstres aux webcams douloureuses du réseau, en passant par les combats brutaux et violents des joueurs de morpion à proximité. La détection de services est la clé pour permettre à votre application de profiter de cet écosystème dynamique de fonctionnalités. Votre application doit écouter les diffusions de service sur le réseau pour voir quels services sont disponibles et filtrer tous les éléments avec lesquels l'application ne peut pas fonctionner.

La détection de services, comme l'enregistrement de services, comporte deux étapes : la configuration d'un écouteur de découverte avec les rappels pertinents et l'exécution d'un seul appel d'API asynchrone vers discoverServices().

Tout d'abord, instanciez une classe anonyme qui implémente NsdManager.DiscoveryListener. L'extrait de code suivant présente un exemple 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);
        }
    };
}

L'API NSD utilise les méthodes de cette interface pour informer votre application lorsque la découverte est lancée, lorsqu'elle échoue, et lorsque des services sont trouvés et perdus (le terme "perdu" signifie "n'est plus disponible"). Notez que cet extrait effectue plusieurs vérifications lorsqu'un service est trouvé.

  1. Le nom du service trouvé est comparé au nom du service local pour déterminer si l'appareil vient de recevoir sa propre diffusion (ce qui est valide).
  2. Le type de service est vérifié pour vérifier qu'il s'agit bien d'un type de service auquel votre application peut se connecter.
  3. Le nom du service est vérifié pour vérifier la connexion à la bonne application.

Il n'est pas toujours nécessaire de vérifier le nom du service. Il n'est pertinent que si vous souhaitez vous connecter à une application spécifique. Par exemple, l'application peut ne vouloir se connecter qu'à des instances d'elle-même exécutées sur d'autres appareils. Toutefois, si l'application souhaite se connecter à une imprimante réseau, il suffit de vérifier que le type de service est "_ipp._tcp".

Après avoir configuré l'écouteur, appelez discoverServices() en indiquant le type de service que votre application doit rechercher, le protocole de découverte à utiliser et l'écouteur que vous venez de créer.

Kotlin

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

Java

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

Se connecter aux services du réseau

Lorsque votre application trouve un service sur le réseau auquel se connecter, elle doit d'abord déterminer les informations de connexion pour ce service, à l'aide de la méthode resolveService(). Implémentez un NsdManager.ResolveListener à transmettre à cette méthode et utilisez-le pour obtenir un NsdServiceInfo contenant les informations de connexion.

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

Une fois le service résolu, votre application reçoit des informations détaillées sur le service, y compris une adresse IP et un numéro de port. Vous disposez de tout ce dont vous avez besoin pour créer votre propre connexion réseau au service.

Annuler l'enregistrement de votre service à la fermeture de l'application

Il est important d'activer et de désactiver la fonctionnalité NSD selon les besoins pendant le cycle de vie de l'application. L'annulation de l'enregistrement de votre application lorsqu'elle est fermée permet d'éviter que d'autres applications pensent qu'elle est toujours active et tentent de s'y connecter. En outre, la détection de services est une opération coûteuse. Elle doit être arrêtée lorsque l'activité parente est suspendue, puis réactivée lorsque l'activité est réactivée. Remplacez les méthodes de cycle de vie de votre activité principale et insérez du code pour démarrer et arrêter la diffusion et la découverte du service, le cas échéant.

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