Обнаружение сетевых сервисов

Обнаружение сетевых сервисов (NSD) позволяет приложению получать доступ к сервисам, которые предоставляют другие устройства в локальной сети. К устройствам, поддерживающим NSD, относятся принтеры, веб-камеры, HTTPS-серверы и другие мобильные устройства.

NSD реализует механизм обнаружения сервисов на основе DNS (DNS-SD), который позволяет приложению запрашивать сервисы, указывая тип сервиса и имя экземпляра устройства, предоставляющего нужный тип сервиса. DNS-SD поддерживается как на Android, так и на других мобильных платформах.

Добавив NSD в приложение, вы позволите пользователям находить в локальной сети другие устройства, которые поддерживают сервисы, запрашиваемые вашим приложением. Это полезно для различных одноранговых приложений, таких как обмен файлами или многопользовательские игры. API NSD в Android упрощают реализацию таких функций.

В этом уроке показано, как создать приложение, которое может транслировать свое имя и информацию о подключении в локальную сеть и сканировать информацию из других приложений, делающих то же самое. Наконец, в этом уроке показано, как подключиться к тому же приложению, запущенному на другом устройстве.

Зарегистрируйте свой сервис в сети

Примечание. Этот шаг необязателен. Если вам не нужно транслировать сервисы приложения по локальной сети, перейдите к следующему разделу – Поиск сервисов в сети.

Чтобы зарегистрировать сервис в локальной сети, сначала создайте объект 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).

Второй параметр задает тип сервиса, указывает, какой протокол и транспортный уровень использует приложение. Синтаксис: "_<протокол>._<транспортный уровень>". Во фрагменте кода сервис использует протокол HTTP, работающий через TCP. Приложение, предлагающее сервис принтера (например, сетевой принтер), установит тип сервиса "_ipp._tcp".

Примечание. Администрация адресного пространства интернета (IANA) управляет централизованным авторитетным списком типов сервисов, используемых протоколами обнаружения сервисов, такими как NSD и Bonjour. Вы можете скачать список названий сервисов и номеров портов. Если вы хотите использовать новый тип сервиса, зарезервируйте его, заполнив регистрационную форму 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);
}

Поиск сервисов в сети

Сеть кишит жизнью: от чудовищных сетевых принтеров до послушных сетевых веб-камер и жестоких, огненных битв игроков в крестики-нолики. Чтобы ваше приложение могло использовать эту экосистему, необходимо обнаружение сервисов. Приложение должно прослушивать широковещательные сообщения в сети, чтобы определить, какие сервисы доступны, и отфильтровать те, с которыми оно не может работать.

Обнаружение сервисов, как и их регистрация, состоит из двух этапов: настройки прослушивателя обнаружения с соответствующими обратными вызовами и выполнения одного асинхронного вызова API к discoverServices().

Сначала создайте экземпляр анонимного класса, который реализует 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);
        }
    };
}

API NSD использует методы в этом интерфейсе, чтобы сообщать приложению о начале и сбое обнаружения, а также о найденных и потерянных сервисах (потерянных – значит больше не доступных). Обратите внимание, что этот фрагмент выполняет несколько проверок, когда сервис найден.

  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 и вставьте код, чтобы запускать и останавливать трансляцию и обнаружение сервиса.

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