ネットワーク サービス ディスカバリを使用する

アプリでネットワーク サービス ディスカバリ(NSD)を使用すると、他のデバイスがローカル ネットワーク上で提供しているサービスにアクセスできます。NSD をサポートするデバイスには、プリンタ、ウェブカメラ、HTTPS サーバー、その他のモバイル デバイスがあります。

NSD は DNS ベースのサービス ディスカバリ(DNS-SD)メカニズムを実装しています。このメカニズムは、サービスのタイプと、目的のタイプのサービスを提供するデバイス インスタンスの名前を指定することで、アプリがサービスをリクエストすることを可能にします。DNS-SD は、Android でも他のモバイル プラットフォームでもサポートされています。

NSD をアプリに追加すると、ローカル ネットワーク上で、アプリがリクエストするサービスをサポートする他のデバイスをユーザーが識別できるようになります。これは、ファイル共有やマルチプレーヤー型ゲームなど、さまざまなピアツーピア アプリで役立ちます。Android の NSD API は、こうした機能を実装する手間を軽減します。

このレッスンでは、アプリの名前と接続情報をローカル ネットワークにブロードキャストし、他のアプリから情報をスキャンするアプリの作成方法を説明します。最後に、別のデバイスで実行されている同じアプリに接続する方法を示します。

サービスをネットワークに登録する

注: この手順は省略可能です。アプリのサービスをローカル ネットワークにブロードキャストしない場合は、このセクションを飛ばして次のネットワーク上のサービスを検出するに進んでください。

サービスをローカル ネットワークに登録するには、はじめに 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 は競合の解決を自動的に処理します。ネットワーク上の 2 台のデバイスの両方に NsdChat アプリがインストールされている場合、サービス名の 1 つは「NsdChat(1)」のような名前に自動的に変更されます。

2 番目のパラメータはサービスタイプを設定し、アプリが使用するプロトコルとトランスポート レイヤを指定します。構文は「_<protocol>._<transportlayer>」です。コード スニペットでは、サービスは TCP で動作する HTTP プロトコルを使用しています。たとえば、プリンタ サービスを提供するアプリ(ネットワーク プリンタなど)では、サービスタイプを「_ipp._tcp」に設定します。

注: International Assigned Numbers Authority(IANA)は、NSD や Bonjour などのサービス ディスカバリ プロトコルで使用されるサービスタイプの信頼できる一元的なリストを管理しています。 このリストは、IANA のサービス名とポート番号のリストからダウンロードできます。新しいサービスタイプを使用したい場合は、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);
    }
    

ネットワーク上のサービスを検出する

扱いづらいネットワーク プリンタからなじみやすいネットワーク ウェブカメラまで、ネットワークは生き生きしたモノであふれており、生き残りを賭けて至るところでモノどうしの激しい戦いが繰り広げられています。この活気に満ちたエコシステムの機能をアプリが発見するための鍵は、サービス ディスカバリです。ネットワーク上のサービス ブロードキャストをリスニングして、利用可能なサービスを見極め、アプリが利用できないものを除外する必要があります。

サービス ディスカバリには、サービス登録と同様に 2 つのステップがあります。それは、関連するコールバックを検出するリスナーの設定と、discoverServices() に対する単一の非同期 API 呼び出しです。

はじめに、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);
            }
        };
    }
    

NSD API は、このインターフェースのメソッドを使用して、検出が開始されたとき、失敗したとき、そしてサービスが見つかったときと失われたときに、アプリに通知します(「失われた」とは「もう利用できない」という意味です)。このスニペットは、サービスが見つかったときにいくつかのチェックを行います。

  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 機能を有効化および無効化することが重要です。アプリの終了時にアプリを登録解除すると、終了したアプリをまだアクティブであると他のアプリが見なして接続しようとするのを防ぐことができます。また、サービス ディスカバリは高コストなオペレーションであるため、親アクティビティが一時停止されると停止し、再開されると再び有効化することが必要です。メイン アクティビティのライフサイクル メソッドをオーバーライドし、必要に応じてサービス ブロードキャストとサービス ディスカバリを開始および停止するコードを挿入してください。

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