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

ネットワーク サービス ディスカバリ(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 アプリがインストールされている場合、どちらか一方がサービス名を自動的に「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);
    }