Использовать обнаружение сетевых служб

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

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

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

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

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

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

Чтобы зарегистрировать службу в локальной сети, сначала создайте объект NsdServiceInfo . Этот объект предоставляет информацию, которую другие устройства в сети используют, когда решают, подключаться ли к вашей службе.

Котлин

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

Ява

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 . Если вы намерены использовать новый тип сервиса, вам следует зарезервировать его, заполнив форму регистрации портов и услуг IANA .

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

Если вы работаете с сокетами, вот как вы можете инициализировать сокет для любого доступного порта, просто установив для него значение 0.

Котлин

fun initializeServerSocket() {
    // Initialize a server socket on the next available port.
    serverSocket = ServerSocket(0).also { socket ->
        // Store the chosen port.
        mLocalPort = socket.localPort
        ...
    }
}

Ява

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 для оповещения вашего приложения об успешной или неудачной регистрации и отмене регистрации службы.

Котлин

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

Ява

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() .

Котлин

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

Ява

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 . В следующем фрагменте показан простой пример:

Котлин

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

Ява

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

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

  1. Имя найденной службы сравнивается с именем локальной службы, чтобы определить, получило ли устройство только что собственную трансляцию (что действительно).
  2. Тип службы проверяется, чтобы убедиться, что это тип службы, к которой может подключиться ваше приложение.
  3. Имя службы проверяется для проверки подключения к правильному приложению.

Проверка имени службы не всегда необходима и актуальна только в том случае, если вы хотите подключиться к определенному приложению. Например, приложение может захотеть подключаться только к своим экземплярам, ​​работающим на других устройствах. Однако если приложение хочет подключиться к сетевому принтеру, достаточно убедиться, что тип службы — «_ipp._tcp».

После настройки прослушивателя вызовите discoverServices() , передав тип службы, которую должно искать ваше приложение, используемый протокол обнаружения и только что созданный прослушиватель.

Котлин

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

Ява

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

Подключайтесь к сервисам в сети

Когда ваше приложение находит в сети службу для подключения, оно должно сначала определить информацию о соединении для этой службы, используя resolveService() . Реализуйте NsdManager.ResolveListener для передачи в этот метод и используйте его для получения NsdServiceInfo содержащего информацию о соединении.

Котлин

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

Ява

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 по мере необходимости в течение жизненного цикла приложения. Отмена регистрации приложения при его закрытии помогает другим приложениям не думать, что оно все еще активно, и пытаться подключиться к нему. Кроме того, обнаружение службы — это дорогостоящая операция, и ее следует останавливать, когда родительское действие приостанавливается, и повторно включать, когда оно возобновляется. Переопределите методы жизненного цикла вашего основного действия и вставьте код для запуска и остановки трансляции и обнаружения службы, если это необходимо.

Котлин

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

Ява

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