Обнаружение сетевых сервисов (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 использует методы в этом интерфейсе, чтобы сообщать приложению о начале и сбое обнаружения, а также о найденных и потерянных сервисах (потерянных – значит больше не доступных). Обратите внимание, что этот фрагмент выполняет несколько проверок, когда сервис найден.
- Название найденного сервиса сравнивается с названием локального сервиса, чтобы определить, не обнаружило ли устройство собственную трансляцию (что допустимо).
- Тип сервиса проверяется, чтобы убедиться, что это тип сервиса, к которому может подключиться ваше приложение.
- Название сервиса проверяется, чтобы убедиться в подключении к правильному приложению.
Проверять название сервиса не всегда необходимо. Это нужно делать, только если вы хотите подключиться к определенному приложению. Например, приложение может подключаться только к своим экземплярам, запущенным на других устройствах. Однако если приложение хочет подключиться к сетевому принтеру, достаточно увидеть, что тип сервиса – "_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); }