Użyj wykrywania usług sieciowych

Wykrywanie usług sieciowych (NSD) zapewnia aplikacji dostęp do usług udostępnianych przez inne urządzenia w sieci lokalnej. Urządzenia obsługujące NSD to m.in. drukarki, kamery internetowe, serwery HTTPS i inne urządzenia mobilne.

NSD implementuje oparty na DNS mechanizm wykrywania usług (DNS-SD), który umożliwia aplikacji wysyłanie żądań usług przez określenie typu usługi i nazwy instancji urządzenia, która zapewnia odpowiedni typ usługi. DNS-SD jest obsługiwany zarówno na Androidzie, jak i na innych platformach mobilnych.

Dodanie NSD do aplikacji umożliwia użytkownikom identyfikowanie innych urządzeń w sieci lokalnej, które obsługują usługi, których żąda aplikacja. Przydaje się to w przypadku różnych aplikacji peer-to-peer, takich jak udostępnianie plików czy granie w gry wieloosobowe. Interfejsy NSD API na Androida upraszczają wdrożenie takich funkcji.

Z tej lekcji dowiesz się, jak stworzyć aplikację, która może rozgłaszać swoją nazwę i informacje o połączeniu do sieci lokalnej oraz skanować w poszukiwaniu informacji z innych aplikacji, które się tym zajmują. Z tej lekcji dowiesz się, jak połączyć się z tą samą aplikacją działającą na innym urządzeniu.

Rejestrowanie usługi w sieci

Uwaga: ten krok jest opcjonalny. Jeśli nie chcesz udostępniać usług swojej aplikacji przez sieć lokalną, możesz przejść do następnej sekcji – Odkrywanie usług w sieci.

Aby zarejestrować usługę w sieci lokalnej, najpierw utwórz obiekt NsdServiceInfo. Udostępnia on informacje, których inne urządzenia w sieci używają, gdy podejmują decyzję, czy połączyć się z Twoją usługą.

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

Ten fragment kodu ustawia nazwę usługi na „NsdChat”. Nazwa usługi to nazwa instancji, która jest widoczna dla innych urządzeń w sieci. Nazwa jest widoczna dla każdego urządzenia w sieci, które używa NSD do wyszukiwania usług lokalnych. Pamiętaj, że nazwa musi być unikalna dla każdej usługi w sieci, a Android automatycznie rozwiązuje konflikty. Jeśli 2 urządzenia w sieci mają zainstalowaną aplikację NsdChat, jedno z nich automatycznie zmieni nazwę usługi na np. „NsdChat(1)”.

Drugi parametr określa typ usługi, który określa protokół i warstwę transportu używaną przez aplikację. Składnia to „_<protokół>._<transportlayer>”. We fragmencie kodu usługa używa protokołu HTTP działającego przez TCP. Aplikacja oferująca usługę związaną z drukarką (np. drukarkę sieciową) ustawi typ usługi na „_ipp._tcp”.

Uwaga: organizacja IANA (International Assigned Numbers) zarządza scentralizowaną, autorytatywną listą typów usług używanych przez protokoły wykrywania usług, takie jak NSD czy Bonjour. Listę tę możesz pobrać z listy nazw usług i numerów portów IANA. Jeśli chcesz używać nowego typu usługi, zarezerwuj ją, wypełniając formularz rejestracji portów i usług IANA.

Podczas ustawiania portu usługi unikaj kodowania go na stałe, ponieważ koliduje to z innymi aplikacjami. Jeśli na przykład Twoja aplikacja zawsze używa portu 1337, może dojść do konfliktu z innymi zainstalowanymi aplikacjami, które korzystają z tego samego portu. Zamiast tego użyj najbliższego dostępnego portu urządzenia. Ponieważ te informacje są dostarczane do innych aplikacji za pomocą komunikatu usługi, port, którego używa Twoja aplikacja, nie musi być widoczny dla innych aplikacji w czasie kompilowania. Zamiast tego aplikacje mogą pobrać te informacje z transmisji usługi tuż przed nawiązaniem połączenia z usługą.

Jeśli pracujesz z gniazdami, w ten sposób możesz zainicjować gniazdo do dowolnego dostępnego portu, ustawiając go na 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();
    ...
}

Po zdefiniowaniu obiektu NsdServiceInfo musisz wdrożyć interfejs RegistrationListener. Ten interfejs zawiera wywołania zwrotne używane przez Androida do informowania aplikacji o powodzeniu lub niepowodzeniu rejestracji i wyrejestrowania usługi.

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

Teraz masz wszystko, czego potrzebujesz, aby zarejestrować swoją usługę. Wywołaj metodę registerService().

Ta metoda jest asynchroniczna, więc każdy kod, który ma być uruchamiany po zarejestrowaniu usługi, musi zostać uwzględniony w metodzie 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);
}

Odkrywanie usług w sieci

Sieć tętni życiem, od najgroźniejszych drukarek sieciowych po posłuszne kamery internetowe, a także brutalne bitwy z pobliskimi graczami w kółko i krzyżyki. Kluczem do tego, aby aplikacja mogła dostrzec ten tętniący życiem ekosystem funkcji, jest odkrycie usług. Aplikacja musi nasłuchiwać transmisji usługi w sieci, aby sprawdzić, jakie usługi są dostępne, i odfiltrować wszystkie elementy, z którymi aplikacja nie może działać.

Wykrywanie usług, podobnie jak rejestracja usługi, składa się z 2 etapów: skonfigurowania odbiornika wykrywania z odpowiednimi wywołaniami zwrotnymi i pojedynczego asynchronicznego wywołania interfejsu API discoverServices().

Najpierw utwórz instancję anonimowej klasy z implementacją NsdManager.DiscoveryListener. Ten fragment kodu zawiera prosty przykład:

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

Interfejs NSD API korzysta z metod dostępnych w tym interfejsie, aby informować aplikację o rozpoczęciu wykrywania, porażce oraz o znalezieniu i utraceniu usług (utracone oznacza, że „nie będą już dostępne”). Zwróć uwagę, że ten fragment kodu wykonuje kilka kontroli po znalezieniu usługi.

  1. Nazwa usługi znalezionej usługi jest porównywana z nazwą usługi lokalnej w celu określenia, czy urządzenie właśnie odebrało własną transmisję (co jest prawidłowe).
  2. Typ usługi jest sprawdzany w celu potwierdzenia, że jest to typ usługi, z którą aplikacja może się połączyć.
  3. Nazwa usługi jest sprawdzana w celu weryfikacji połączenia z właściwą aplikacją.

Sprawdzanie nazwy usługi nie zawsze jest konieczne i ma znaczenie tylko wtedy, gdy chcesz połączyć się z konkretną aplikacją. Aplikacja może np. chcieć łączyć się tylko z instancjami, które działają na innych urządzeniach. Jeśli jednak aplikacja chce połączyć się z drukarką sieciową, wystarczy, że zobaczysz, że typ usługi to „_ipp._tcp”.

Po skonfigurowaniu detektora wywołaj discoverServices(), przekazując typ usługi, której ma szukać aplikacja, protokół wykrywania, którego ma używać, oraz utworzony właśnie detektor.

Kotlin

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

Java

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

Nawiązywanie połączeń z usługami w sieci

Gdy aplikacja znajduje w sieci usługę, z którą chce się połączyć, musi najpierw określić informacje o połączeniu z tą usługą przy użyciu metody resolveService(). Zaimplementuj obiekt NsdManager.ResolveListener, który przekazuje dane do tej metody, i używaj go do uzyskania obiektu NsdServiceInfo z informacjami o połączeniu.

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

Po rozwiązaniu usługi aplikacja otrzyma szczegółowe informacje o usłudze, w tym adres IP i numer portu. To wszystko, czego potrzebujesz do utworzenia własnego połączenia sieciowego z usługą.

Wyrejestruj usługę po zamknięciu aplikacji

Ważne jest, aby w trakcie cyklu życia aplikacji włączać i wyłączać funkcje NSD. Wyrejestrowanie aplikacji po jej zamknięciu sprawia, że inne aplikacje nie mogą uważać, że jest ona nadal aktywna i próbują się z nią połączyć. Poza tym wykrywanie usług jest kosztowne i należy je zatrzymywać po wstrzymaniu aktywności nadrzędnej i ponownie włączać po jej wznowieniu. Zastąp metody cyklu życia głównej aktywności i wstaw kod, aby odpowiednio rozpocząć i zatrzymać transmisję oraz wykrywanie usług.

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