Hatena::ブログ(Diary)

_development, RSSフィード Twitter

2012-07-02

How to use the new android.net.nsd package for Zeroconf

mDNS/DNS-SD

AndroidはJelly Bean(APIレベル16)でmDNS(Multicast DNS)とDNS-SD(DNS based Service Discovery)をサポートしました。

mDNSとDNS-SDを使えば、DNSサーバーレジストリサーバーのようなコンフィグレーションサーバーを介することなく、ネットワーク上のホストの名前を解決し、サービスを提供しているホストを自動的に発見することができます。


mDNSとDNS-SDネットワークの構成作業を簡略、自動化するためのZeroconf技術の主張な要素です。

これらはAppleによるZeroconf実装に含まれ、その実装はBonjourという名前で知られています。

また、その仕様はインターネットドラフトとして以下のURLで公表されています。

http://files.dns-sd.org/draft-cheshire-dnsext-dns-sd.txt


これら機能はandroid.net.nsdパッケージで提供されています。

その主要なクラスはNsdManagerです。

NsdManagerは、サービスの発見と名前解決、その結果を受け取るためのメソッドを提供しています。

さらに、自身で提供するサービスを他のホストに公開するためのメソッドも提供します。

NsdManagerが扱う結果はNsdServiceInfoクラスのオブジェクトとして扱われます。

NsdServiceInfoは次のプロパティを持っています。

  • serviceType
  • serviceName
  • host
  • port

serviceType

serviceTypeはサービスの種類を表す識別子です。

これは例えば、"_ipp._tcp.local."や、"_myservice._udp.local."のようになります。

識別子"."で区切られ、頭から順に、サービスの種類、プロトコルドメインを表します。

最初の例ではippがサービスの種類、tcpプロトコル、localがドメインです。

ippはInternet Printing Protocolの略で、これら既知のサービスの種類は以下のURLで定義されています。

http://www.dns-sd.org/ServiceTypes.html

二番目の例では、myserviceがサービスの種類です。

これは独自のサービスで、開発者が独自のサービスをネットワーク上に公開する場合はこのような既知の

サービスとして定義されていない識別子を使うことになるでしょう。

例えば、ネットワーク対戦が可能なゲームアプリケーション(仮にHungry Birdsとしましょう)を作る場合、ゲームを実行するそれぞれのホストは

"_hungrybirds._tcp.local."という識別子でサービスを公開し、ネットワーク上で同じ識別子のサービスを探して対戦相手にすることでしょう。

serviceName

続いてserviceNameは、(紛らわしいことに)serviceTypeで識別されるサービスを公開しているホストの名前です。

これは例えば、"Someone's Mac"、"10-0-1-2"といった、ホストコンピュータの名前やIPアドレスなどが使われることでしょう。

host, port

hostとportは、サービスを公開しているホストのIPアドレスと、サービスを公開しているポートです。

サービスの種類から、そのサービスを公開しているホストを発見し、そのhostとportを取得するまでがmDNS/DNS-SDの領域です。

そこから先、実際にそのホスト(が公開しているポート)に接続して、サービスが定めるプロトコルに従って通信するのは開発者の役目です。


サービスの公開と検索

mDNS/DNS-SDを使うには2つの手順があります。

一つはサービスの公開、そしてサービスの検索です。

これらは、両方必要な場合もありますし、いずれか片方だけが必要な場合もあり、それはプログラムの役割に依ります。

プログラムが、既知のサービスを利用するクライアントであるなら、必要なのはサービスの検索です。

逆にサービスを提供するサーバーであるなら、必要なのはサービスの公開です。

そして、P2P形式である場合はサービスの公開と検索の両方が必要になるでしょう。

以下にサービスの公開、サービスの検索の順に、その方法を説明します。

プログラムP2P形式の場合はそれらを組み合わせて使ってください。


サービスの公開

まずは公開されたサービスを簡単に見られるようにツールを手にいれましょう。

DNS-SDのサイト(http://www.dns-sd.org/)で紹介されているBonjour Browserのようなツールを使うのが楽でしょう。

ツールを手にいれたら、ツールを実行するコンピュータネットワークに接続されていることを確認してツールを実行してください。

Macを使っているか、同一ネットワークに他のMacや、Bonjourサービスを実行中のその他のコンピューターがある場合は、いくつかのサービスがリストされることでしょう。

そうでない場合でも気にしないでください。これから作成するプログラムを実行すれば、そのサービスがリストされるはずですから。

では、新しくAndroidプロジェクトを作ってください。

android.net.nsdパッケージはJelly Bean(APIレベル16)以降でしか使えないので、APIレベルは16以降にする必要があります。

また、それ以下では実行できないのでminSdkVersionも16以上にしておくと良いでしょう。

そして後で不幸なことが起きないように(アーメン)今のうちに android.permission.INTERNET をマニフェストに追加しておきましょう。


NsdManagerオブジェクトの取得

まず、ContextのgetSystemService()メソッドを呼び出して、NsdManagerオブジェクトを取得します。

    /** The instance of NsdManager */
    NsdManager nsdManager;

    /**
     * Ensure SystemService objects.
     */
    void ensureSystemServices() {
        nsdManager = (NsdManager) getSystemService(NSD_SERVICE);
        if (nsdManager == null) {
            finish();
        }
    }

サービスの内容を表すNsdServiceInfoオブジェクトの作成

次に、サービスの内容を表すNsdServiceInfoオブジェクトを作成します。

ここでは、サービスの種類としてippを指定していますが、実際のプログラムでは公開するサービスにあった適切な種類を指定してください。

    /** The port of this service */
    static final int SERVICE_PORT = 8888;
    /** The type of this service */
    static final String SERVICE_TYPE = "_ipp._tcp.";
    /** The name of this service */
    static final String SERVICE_NAME = Build.MODEL + ' ' + "the pseudo printer";

    /**
     * Allocate ServiceInfo object for register service.
     * 
     * @return
     */
    static NsdServiceInfo allocateServiceInfo() {
        NsdServiceInfo serviceInfo = new NsdServiceInfo();
        serviceInfo.setPort(SERVICE_PORT);
        serviceInfo.setServiceName(SERVICE_NAME);
        serviceInfo.setServiceType(SERVICE_TYPE);
        return serviceInfo;
    }

registerService()メソッドを呼び出してサービスを公開

最後に、NsdManagerのregisterService()メソッドを呼び出してサービスを公開します。

unregisterService()は、サービスの公開を終了するメソッドです。

    /**
     * Register service to mDNS/DNS-SD.
     */
    void registerService() {
        NsdServiceInfo serviceInfo = allocateServiceInfo();
        int protocolType = NsdManager.PROTOCOL_DNS_SD;
        nsdManager.registerService(serviceInfo, protocolType, registrationListener);
    }

    /**
     * Unregister service from mDNS/DNS-SD.
     */
    void unregisterService() {
        if (regstrationListenerRegistered)
            nsdManager.unregisterService(registrationListener);
    }

RegistrationListenerオブジェクトで結果を受け取る

registrationListenerはRegistrationListenerオブジェクトで、サービス公開の結果を受け取るために使います。

ここでは、特別な処理はしていませんが、もしサービスの公開が失敗した場合はリスナーは登録されず、その状態でunregisterService()を呼び出してしまうとエラーが発生するので、フラグを管理して失敗時はunregisterService()を呼ばないようにします。

    /** The flag which the RegstrationListener was registered or not */
    boolean regstrationListenerRegistered;

    /** The RegstrationListener */
    RegistrationListener registrationListener = new NsdManager.RegistrationListener() {
        @Override
        public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
            Log.w(TAG, format("UnregstrationFailed serviceInfo=%s, errorCode=%d", serviceInfo, errorCode));
        }

        @Override
        public void onServiceUnregistered(NsdServiceInfo serviceInfo) {
            Log.i(TAG, format("ServiceUnregisterd serviceInfo=%s", serviceInfo));
            regstrationListenerRegistered = false;
        }

        @Override
        public void onServiceRegistered(NsdServiceInfo serviceInfo) {
            Log.i(TAG, format("ServiceRegisterd serviceInfo=%s", serviceInfo));
            regstrationListenerRegistered = true;
        }

        @Override
        public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
            Log.w(TAG, format("RegstrationFailed serviceInfo=%s, errorCode=%d", serviceInfo, errorCode));
        }
    };

アプリケーションの作成

ここでは、Activityの活性化するときにサービスを公開し、非活性化するときにサービスの公開を停止するようアプリケーションを作成しました

    // Activity's lifecycle methods.

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ensureSystemServices();
    }

    @Override
    protected void onResume() {
        super.onResume();
        registerService();
    }

    @Override
    protected void onPause() {
        unregisterService();
        super.onPause();
    }

結果の確認

コードを実行して結果を確認してみましょう。

実行の前にAndroid端末を、ツールと同じWi-Fiネットワークに接続するのを忘れずに。

Bonjour Browserのようなツールで、Internet Printing Protocol(_ipp._tcp.local)に"Galaxy Nexus the pseudo printer"が追加されるのが確認できるはずです。

"Galaxy Nexus"の部分はプログラム上でBuild.MODELフィールドの値を使っているので、各自の環境にあわせて読み替えてください。


もうひとつ、このサービスが他のシステムからどのように見えるか確認してみましょう。

Macを使っているなら、システム環境設定から「プリントとスキャン」を開いてください。

次にプリンタの追加を行うために、プリンタのリストの「+」ボタンを押しましょう。

"Galaxy Nexus the pseudo printer"がプリンターとしてリストされていることでしょう。

これは、サービスを公開する時にサービスの種類として"ipp"を使ったからです。

ippは、Macネットワークプリンタを探すときに使うサービスであるため、プログラムippを偽ったサービスを公開したため、Macはこれをプリンターだと認識したのです。


以上がサービスの公開の手順です。


サービスの検索

サービスのリストを検索する

サービスのリストを検索するにはNsdManagerのdiscoverServices()メソッドを呼びます。

discoverServices()は検索するサービスの種類を指定しますが、サービスの種類として_services._dns-sd._udp.を指定するとネットワーク上に公開されているサービスの一覧が取得できます。

検索を停止する時は、NsdManagerのstopServiceDiscovery()メソッドを呼びます。

    /** The type of service for searching */
    static final String SERVICE_TYPE = "_services._dns-sd._udp.";

    /**
     * Start discovery
     */
    private void startDiscovery() {
        nsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener);
    }

    /**
     * Stop discovery
     */
    private void stopDiscovery() {
        if(discoveryStarted)
            nsdManager.stopServiceDiscovery(discoveryListener);
    }

DiscoveryListenerオブジェクトの作成

discoverServices()メソッドの結果はNsdManager.DiscoveryListenerのオブジェクトで受け取ります。

ネットワーク上にサービスが見つかる毎にonServiceFound()コールバックメソッドが呼ばれます。

サービスがネットワーク上から取り除かれた場合はonServiceLost()コールバックメソッドが呼ばれます。

その他のコールバックメソッドは検索の開始・停止の成功・失敗により呼ばれます。

    /**
     * The DiscoveryListener
     */
    NsdManager.DiscoveryListener discoveryListener = new NsdManager.DiscoveryListener() {
        
        @Override
        public void onStopDiscoveryFailed(String serviceType, int errorCode) {
            Log.w(TAG, format("Failed to stop discovery serviceType=%s, errorCode=%d", serviceType, errorCode));
        }
        
        @Override
        public void onStartDiscoveryFailed(String serviceType, int errorCode) {
            Log.w(TAG, format("Failed to start discovery serviceType=%s, errorCode=%d", serviceType, errorCode));
        }
        
        @Override
        public void onServiceLost(NsdServiceInfo serviceInfo) {
            Log.i(TAG, format("Service lost serviceInfo=%s", serviceInfo));
        }
        
        @Override
        public void onServiceFound(NsdServiceInfo serviceInfo) {
            Log.i(TAG, format("Service found serviceInfo=%s", serviceInfo));
        }
        
        @Override
        public void onDiscoveryStopped(String serviceType) {
            discoveryStarted = false;
            Log.i(TAG, format("Discovery stopped serviceType=%s", serviceType));
        }
        
        @Override
        public void onDiscoveryStarted(String serviceType) {
            discoveryStarted = true;
            Log.i(TAG, format("Discovery started serviceType=%s", serviceType));
        }
    };

_services._dns-sd._udp.の検索結果の確認

_services._dns-sd._udp.を指定した検索を実行すると、次のような結果が得られます。

この結果は同一ネットワーク上に存在するMacが公開しているサービスがリストされたもので、結果は環境によって異なります。

Discovery started serviceType=_services._dns-sd._udp.
Service found serviceInfo=name: _sleep-proxy type: _udp.local. host: null port: 0 txtRecord: null
Service found serviceInfo=name: _acp-sync type: _tcp.local. host: null port: 0 txtRecord: null
Service found serviceInfo=name: _airport type: _tcp.local. host: null port: 0 txtRecord: null
Service found serviceInfo=name: _raop type: _tcp.local. host: null port: 0 txtRecord: null
Service found serviceInfo=name: _afpovertcptype: _tcp.local. host: null port: 0 txtRecord: null
Service found serviceInfo=name: _smb type: _tcp.local. host: null port: 0 txtRecord: null
Service found serviceInfo=name: _adisk type: _tcp.local. host: null port: 0 txtRecord: null
Service found serviceInfo=name: _rfb type: _tcp.local. host: null port: 0 txtRecord: null

サービスの詳細を検索する

_services._dns-sd._udp.を使った検索で取得したサービス、またはあらかじめ決められたサービスを指定して詳細を検索します。

まず、_services._dns-sd._udp.を使った場合と同じようにdiscoverService()メソッドを呼び出します。

ここで指定している_raopはMacAirTunesサービスですが、同じように他の既知のサービス、または独自のサービスを指定できます。

    /** The type of service for searching */
    static final String SERVICE_TYPE = "_raop._tcp.";

    /**
     * Start discovery
     */
    private void startDiscovery() {
        nsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener);
    }

見つかったサービスを解決する

サービスが見つかったら、引数として渡されたNsdServiceInfoオブジェクト引数に、NsdManagerのresolveService()メソッドを呼び出します。

もし、予めサービスの種類と名前が判明している場合は、discoverServices()メソッドによる検索を経ずに、判明している情報からNsdServiceInfoオブジェクトを構成してresolveService()メソッドを呼ぶことでもサービスの解決ができます。

    /**
     * The DiscoveryListener
     */
    NsdManager.DiscoveryListener discoveryListener = new NsdManager.DiscoveryListener() {
        
        @Override
        public void onServiceFound(NsdServiceInfo serviceInfo) {
            Log.i(TAG, format("Service found serviceInfo=%s", serviceInfo));
            nsdManager.resolveService(serviceInfo, resolveListener);
        }

        ...(略)
    };

解決したサービスを利用する

サービスの解決が完了したら、そのサービスを利用するために必要な情報が引数として渡されたNsdServiceInfoのプロパティーから得られます。

これは主にhostとportですが、サービスによってはtxtRecordに追加の情報を提供する場合もあります。

(しかし、今回動作させた環境では、サービスはtxtRecordで情報を提供しているにもかかわらず、txtRecordは常にnullでした。)

    /**
     * The ResolverListener
     */    
    NsdManager.ResolveListener resolveListener = new NsdManager.ResolveListener() {
        
        @Override
        public void onServiceResolved(NsdServiceInfo serviceInfo) {
            Log.i(TAG, format("Service resolved serviceInfo=%s", serviceInfo));
        }
        
        @Override
        public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
            Log.w(TAG, format("Failed to resolve serviceInfo=%s, errorCode=%d", serviceInfo, errorCode));
        }
    };

サービスの詳細の確認

ResolveListenerのonServiceResolved()メソッドに渡されるNsdServiceInfoオブジェクトプロパティーから得られる次の情報が使えます。(IPアドレスコンピューター名は一部伏字にしています。)

  • host: 10.0.*.2
  • port: 5000

つまり、このサービスに接続するにはIPアドレス10.0.*(実際は実在するIPアドレスの値).2のポート5000番に接続すれば良いことになります。

これ以降は通常のネットワークプログラミングと同じです。

Discovery started serviceType=_raop._tcp.
Service found serviceInfo=name: ********BC04@Someone's AirMac Express type: _raop._tcp. host: null port: 0 txtRecord: null
Service resolved serviceInfo=name: ********BC04@Someone's\\032AirMac\\032Express type: ._raop._tcp host: /10.0.*.2 port: 5000 txtRecord: null


ソースファイル

エントリで使ったソースファイルはこちらです。

https://github.com/esmasui/underdevelopment/tree/master/HelloDNSSD


参考

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

リンク元