JmDNSによるBonjourのサービス解決

android.net.nsdパッケージが現状バグで使えなさげだが、Bonjourを扱うには元々実績のあるJavaのライブラリィが提供されており、有り難いことにandroidからでも使えるのだ。

JmDNS

JmDNSはmDNS(Multicast DNS)とDNS-SD(DNS based Service Discovery)をサポートし、Bonjourとの完全な互換を歌うオープンソースのライブラリィであり、Apache License, Version 2.0ライセンス下で使用することができる。

Javaを使ったサービス探索〜解決のためのコード
static JmDNS jmdns;

jmdns = JmDNS.create();
jmdns.addServiceListener("_ipp._tcp.local.", new ServiceListener(){
    @Override
    public void serviceAdded(ServiceEvent event) {
        System.out.println("Service added   : " + event.getName() + "." + event.getType());
        jmdns.requestServiceInfo(event.getType(), event.getName());
    }
    @Override
    public void serviceRemoved(ServiceEvent event) {
        System.out.println("Service removed : " + event.getName() + "." + event.getType());
    }
    @Override
    public void serviceResolved(ServiceEvent event) {
        System.out.println("Service resolved: " + event.getInfo());
    }
});

JmDNSクラスのファクトリを呼びインスタンスを取得した後に、対象となるサービスタイプに対してリスナを登録する。android.net.nsd同様に簡単だ。ポイントは、serviceAddedメソッド中でrequestServiceInfoメソッドを呼び出していること。これをやらないとserviceResolvedが飛んでこないのである。 ※

なお、JmDNSのサービスと個々のソケット通信はIPマルチキャストがベースになっているのだが、そもそもandroidではデフォルトでマルチキャストが無効にされているために、このシンプルなコードを使うことはできない。

よってまずはandroid上でJmDNSを使うためにマルチキャストを有効にしなくてはならない。

androidでIPマルチキャストのパケットを受け取るために必要なコード
// マルチキャストアドレス宛てパケットを受け取る
WifiManager wifiManager = (WifiManager) context.getSystemService(android.content.Context.WIFI_SERVICE);
WifiManager.MulticastLock multicastLock = wifiManager.createMulticastLock("for JmDNS"); // デバッグのためのタグ
multicastLock.setReferenceCounted(true);
multicastLock.acquire();

なお、JmDNSはメインスレッドでネットワーク操作を行うため、android3.0以降のバージョンで動かすとNetworkOnMainThreadException例外が発生してしまう。

 00:00.267: W/dalvikvm(1553): threadid=1: thread exiting with uncaught exception (group=0xa6f5e288)
 00:00.267: E/AndroidRuntime(1553): FATAL EXCEPTION: main
 00:00.267: E/AndroidRuntime(1553): android.os.NetworkOnMainThreadException
 00:00.267: E/AndroidRuntime(1553): 	at android.os.StrictMode$AndroidBlockGuardPolicy.onNetwork(StrictMode.java:1117)
 00:00.267: E/AndroidRuntime(1553): 	at java.net.InetAddress.lookupHostByName(InetAddress.java:385)
 00:00.267: E/AndroidRuntime(1553): 	at java.net.InetAddress.getLocalHost(InetAddress.java:365)
 00:00.267: E/AndroidRuntime(1553): 	at javax.jmdns.impl.HostInfo.newHostInfo(HostInfo.java:76)
 00:00.267: E/AndroidRuntime(1553): 	at javax.jmdns.impl.JmDNSImpl.<init>(JmDNSImpl.java:408)
 00:00.267: E/AndroidRuntime(1553): 	at javax.jmdns.JmDNS.create(JmDNS.java:60)

この例外は以下のアドホックなコードをJmDNSクラスの生成より前に追加することで回避することが可能になるが

if (android.os.Build.VERSION.SDK_INT > 9) {
    StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
    StrictMode.setThreadPolicy(policy);
}

バックグラウンドスレッドやAsyncTask内ででJmDNSを生成すれば良いだけなので、そうすべきだろう。(今回は割愛する)

実行結果

前回も紹介したが、Xcode付属のプリンタシミュレータで公開されている仮想のプリンタはBonjourサービスとしてシステムに登録されているため、これをテストに使おう。

macにはdns-sdコマンドがインストールされており、mDNSホストつまりBonjourサービス名を解決することができるので、dns-sdコマンドでippプリンタのサービスをダンプしてみたが、Bonjourサービスとしてプリンタが登録されていることが解る。

androidではemulatorを使い、最初に説明したIPマルチキャスト、JmDNSクラスの生成とそのリスナのコールバックメソッドの内容をTextViewにappendするコードを組んだテストアプリケーションを実行した。

このようにdns-sdコマンドでダンプされたのと同じ、プリンタシミュレータで登録されたサービスの追加、解決が成功しているのが解るだろう。

※ ネット上で対象になっている version 3.4.1には、PRT(ipp)のサービスサブタイプを解決できないというバグがある。
JmDNS / Bugs / #112 JmDNS 3.4.1 does not answer to PTR requests on sub-types
このバグは最新版である3.4.2では修正されているので、サンプルソースコード等に同梱されているソースコードやjarではなく、リポジトリ上の最新ソースコードを使わなくてはならない。
http://jmdns.svn.sourceforge.net/viewvc/jmdns/trunk/