第3回 JXTAのDiscoveryの働き

丸山不二夫
稚内北星学園大学学長
(http://www.wakhok.ac.jp/)
2001/12/22

 本連載では、成長過程にあるJXTAのテクニカルな部分に触れながら、JXTAによって実現可能となるPtoPサービスの可能性にアプローチしていきたい。また、大きなリリースのタイミングでは、その詳細について解説する。(編集局)


 今回は、shareコマンドとsearchコマンドのソースを読みます。JXTA-Shellのコマンド作成に必要な情報を紹介しながら、JXTAがどのように「告知」の公開・探索を行っているのかを見ていきたいと思います。

今回の内容
JXTA-Shellのコマンド・クラスをどこに作るか
JXTA-Shellのイディオム

Shell内部の変数の値を獲得するには
Discoveryインスタンスの獲得
shareコマンドsearchコマンドの処理
pushメソッドとgetLocalAdvertisemententsメソッド

探索メッセージは、どのように送られるか?
Discoveryの働きのまとめ

JXTA-Shellのコマンド・クラスをどこに作るか

 JXTA-Shellのコマンドは、すべてがnet.jxta.impl.shell.ShellAppクラスを継承して作られます。JXTA-Shellコマンドを作成するためには、このクラスのほかに、net.jxta.impl.shell.*以下のクラスが必要になります。

 shareコマンド・クラスのパッケージ名とクラス名を示します。パッケージ名ですが、net.jxta.impl.shell.bin.+shareという形をしています。このshare以前の名前は予約されています。この固定部分にコマンド名(この例の場合にはshare)を付けたのが、Shellコマンド・クラスのパッケージ名になります。さらに、コマンド名と同じ名前のクラスが、このパッケージの中にある必要があります。

package net.jxta.impl.shell.bin.share;

import net.jxta.impl.shell.*;
...........
...........

public class share extends ShellApp {

  private Discovery disco=null;
  ShellEnv env;
...........
...........

}

 もしもcmdXという名前のJXTA-Shellコマンドを作りたいのであれば、./net/jxta/impl/shell/bin/cmdX/cmdX.classというパス名を持つクラスファイルが必要だということです。なにか、cmdXが二重になっていると思われるかもしれませんが、そうではありません。もしも、コマンドcmdX用に補助的なヘルパー・クラスが必要であれば、これらのcmdXに関連するファイルたちを、cmdXディレクトリ以下に次のように配置すればいいのです。

./net/jxta/impl/shell/bin/cmdX/cmdX.class
./net/jxta/impl/shell/bin/cmdX/cmdXHelper1.class
./net/jxta/impl/shell/bin/cmdX/cmdXHelper2.class
.............
./net/jxta/impl/shell/bin/cmdX/cmdXHelperN.class
./net/jxta/impl/shell/bin/cmdX/cmdXImage1.gif
.............
./net/jxta/impl/shell/bin/cmdX/cmdXIcon.ico

JXTA-Shellのイディオム ―― getEnvとgetDiscovery ――

 JXTA-Shellでは、コマンド文字列の実行はShellAppインスタンスのstartAppメソッドの呼び出しとして実行されます。このとき、コマンドに与えられていた引数たちは文字列の配列としてこのstartAppメソッドに渡されます。次に示すのは、shareコマンドのstartAppメソッドのリストです。

 public int startApp (String[] args) {<BR>

  if ((args == null) || (args.length !=1)) { // 引数配列がnull、あるいは、1つ以外の引数を
   return syntaxError();       // 持てば、エラー
  }

  env = getEnv();        // 環境とdiscoveryサービスを獲得する
  disco = group.getDiscovery();

  String name = args[0];       // share に与えられた引数=告知の名前

  ShellObject obj = env.get ("stdgroup");   // 環境変数stdgroupに設定されている値を
  PeerGroup group = (PeerGroup) obj.getObject();  // groupに設定する

  obj = env.get (name);    // 引数の告知は、あらかじめ変数に
  if (obj == null) {        // 入っていなければいけない
   println ("share: cannot access " + name);
   return ShellApp.appMiscError;
  }

  StructuredDocument doc = null;
  Advertisement adv = null;

  try {
   adv = (Advertisement) obj.getObject();  // 引数の告知の名前から、告知を獲得する
   publishAdv(adv);         // 告知の公開
  } catch (Exception e) {
   println ("share: " + name + " is not a proper Document");
   return ShellApp.appMiscError;
  }
  return ShellApp.appNoError;
 }

 このソースの次の処理は、ほとんどすべてのJXTA-Shellコマンドで行われている基本的なイディオムといっていいものです。

env = getEnv();
disco = group.getDiscovery();

Shell内部の変数の値を獲得するには

 env = getEnv();のenvはShellEnvクラスのインスタンスで、JXTA-Shell内部の変数の値を獲得する際に用いられます。envはHashtableと同じようなものなのですが、直接変数を格納しているわけではなく、オブジェクトと変数名のペアから成るShellObjectというオブジェクトのインスタンスを格納しています。これは、変数名を表す文字列がnullのときに、自動的に変数名を生成して値を格納する際に利用される仕掛けなのですが、そのために、変数名を指定してその値を引き出すときには、次の例のように、いったんgetメソッドでShellObjectを獲得してから、getObjectメソッドを実行しなければなりません。

ShellObject obj = env.get ("stdgroup");
PeerGroup group = (PeerGroup) obj.getObject();

 この2段階のシーケンスはよく登場しますので、このパターンを頭に入れておいてください。

Discoveryインスタンスの獲得

 Discoveryインスタンスのdiscoは、share/searchコマンドをはじめ、JXTA-Shellのいくつかのコマンドの中でdisco = group.getDiscovery();で獲得されます。このdiscoは重要な役割を果たします。つまり、discoはShellAppクラスを拡大したコマンド・クラスの中でprivate Discovery disco=null;と定義されているのですが、net.jxta.discovery.Discoveryはインターフェイスなので、正確にいえば、discoはDiscoveryインターフェイスを実装したインスタンスということになります。

 次に、コマンドsearchのソースの一部を示します。ここでもgetEnvメソッドとgetDiscoveryメソッドが使われていることを確認してください。

public class search extends ShellApp {
  private ShellEnv env;
  private Discovery discovery=null;
  .............
  .............

  public int startApp(String[] args) {

    env = getEnv();
    discovery = group.getDiscovery();
    .............
    .............

    try {
      if (rflag || pflag ) {
        return(discover(pid,attr,val));
      } else {
        return(getLocal(attr,val));
      }
    }
    catch(Throwable ex) {
      .............
    }
  }

shareコマンドとsearchコマンドの処理

 先のshareコマンドとsearchコマンド、2つのstartAppメソッドのソースを眺めれば、shareコマンドの処理の中心がpublishAdv(adv);というメソッド呼び出しであり、searchコマンドの処理の中心がdiscover(pid,attr,val)とgetLocal(attr,val)いう2つのメソッド呼び出しであることが分かります。これらのメソッドのソースを次に示します。

private void publishAdv(Advertisement adv) {
    try {
        disco.publish(adv, Discovery.ADV);
    } catch( Exception ignored ) {
  }
}

private int discover(String address,  String attr, String val ) {
    discovery.getRemoteAdvertisements(address,discovery.ADV, attr, val,threshold);
    println("JXTA Advertisement search message sent");
    return ShellApp.appNoError;
}

private int getLocal(String attr, String val) {
    Enumeration res;
    try {
        res = discovery.getLocalAdvertisements(discovery.ADV, attr, val);
    } catch (Exception e) {
        println("nothing stored");
        return ShellApp.appNoError;
    }
    ..............
    ..............
}

 こうして整理すると、shareコマンドは基本的にはDiscoveryインスタンスに対するpublishメソッドの呼び出しであり、searchコマンドは同じくDiscoveryインスタンスに対するgetRemoteAdvertisementsメソッド、あるいはgetLocalAdvertisementsメソッドの呼び出しであることが分かります。

publishメソッドとgetLocalAdvertisementsメソッド

 net.jxta.impl.discovery.DiscoveryServiceクラスのpublishメソッドのソースの一部を示します。publishは、告知のタイプがGROUPかPEERかそのほかの告知かによって、基本的にはそれらのIDを取り込んで一意なファイルの名前を作り、告知のXMLドキュメントを格納しローカルなディレクトリ上に書き出します。

public void publish(Advertisement adv, int type,
    long timeoutForMe, long timeoutForOthers) throws IOException {

    ID advID = null;
    String advName = null;
    // タイプは、GROUPかPEERか、それ以外か?
    switch( type ) {
        case GROUP :
            advID = ((PeerGroupAdv)adv).getGid();
            break;
        case PEER :
            advID = ((PeerGroupAdv)adv).getPid();
            break;
        default:
            advID = ID.nullID;
            break;
    }
    // 告知からXML文書を生成する
    StructuredDocument doc;
    try {
        doc = (StructuredDocument) adv.getDocument(new MimeMediaType("text/xml"));
    } catch (IOException error) {
        throw error;
    } catch (Exception everything) {
        throw new IOException
        ("Advertisement couldn’t be saved because of :"
        + everything.toString());
    }

    // 告知のXML文書を格納するファイル名を決める
    if( advID.equals( ID.nullID ) )
        advName = cm.createTmpName(doc);
    else
        advName = advID.getUniqueValue().toString();

    if (LOG.isDebugEnabled()) {
        LOG.debug("publishing " + advName + " in " + dirname[type]);
    }

    // cmディレクトリ下にセーブする
    cm.save(dirname[type], advName, doc, timeoutForMe, timeoutForOthers);
}

 publishされた告知、また、次に見るgetRemoteAdvertisementsでネットワーク上で発見された告知は、具体的にはJXTAが起動されたディレクトリ下のcmという名前のディレクトリの下に、次のように収められています。画面1を参照してください。ここでは、Advと名付けられたディレクトリ下に、cmで始まる名前を持ついくつかのファイルがあることが分かりますね。このファイルの中にいくつかの告知がXML文書として公開されているわけです。

画面1

 今回、getLocalAdvertisementsメソッドのソースは省略しますが、こうしたcmディレクトリ下のXMLドキュメントから、attributeやvalueといった検索条件に合致した告知たちを選び出す働きをしています。publishメソッドが告知を書き出して、getLocalAdvertisementsメソッドがそれを読み出すわけですから、この2つのメソッドは、相互に補い合う働きをしていることになります。

getRemoteAdvertisementsメソッドのソースを読む

 先の2つのメソッドが、ともにローカルに働いているのに対して、searchコマンドが-rオプションを付けて呼ばれたときに、内部で呼び出されるgetRemoteAdvertisementsメソッドは、ネットワークを越えてほかのPeer上の告知を見つけに行きます。Discoveryインスタンス上で定義されたこれらのメソッドのこうした働きは、本連載のJavaでのJXTAプログラムの紹介ですでに見てきました。

 JXTA-Shellのコマンドが、JXTA-JavaのAPIを利用して書かれていることをこれまで見てきたのですが、publishやgetRemoteAdvertisementsといった、JXTA-JavaのAPIに登場するメソッドのソースも簡単に手に入ります。今回は、これらのソースのメソッドの内部の処理をもう少し詳しく見てみることにします。次に、getRemoteAdvertisementsメソッドのソースを示します。

public int getRemoteAdvertisements(String peer, int type,
String attribute, String value, int threshold) {

    DiscoveryQuery dquery = new DiscoveryQuery(type,
        advToString(myGroup.getAdvertisement()),
        attribute, value, threshold);

    if ((attribute == null) || (value == null)) {
        dquery.setAttr("null");
        dquery.setValue("null");
    } else {
        dquery.setAttr(attribute);
        dquery.setValue(value);
    }
    // Private copy for thread safety
    int myQueryID = qid++;
    LOG.debug("sending query: " + attribute + " = " + value);
    ResolverQuery query = new ResolverQuery(advertisement.getName(),
        "JXTACRED", localPeerId, dquery.toString(), myQueryID);
    resolver.sendQuery(peer, query);
    return myQueryID;
}

 初めにDiscoveryQueryというクラスのインスタンスdqueryが生成されています。このインスタンスに、setAttrやsetValueを使い、引数で与えられているattributeやvalueの値を設定しているようです。続いてResolverQueryクラスのインスタンスを生成しています。そのコンストラクタの引数をよく見ると、dquery.toString()という形で、先に作ったdqueryインスタンスを文字列にしてqueryを作るのに使っていますね。このqueryがsendQueryメソッドの引数に与えられます。Resolverインスタンスに対するsendQueryメソッドの呼び出しが、getRemoteAdvertisementsメソッドの中心部分を構成しています。このnet.jxta.impl.resolver.ResolverServiceのsendQueryメソッドは、JXTAのメッセージ交換メカニズムの特徴をよく表しています。

探索メッセージは、どのように送られるか?

 ここでQueryと呼ばれているものは、まず、「これこれの告知を検索せよ」という探索命令だと考えてください。細かなメカニズムは後で見ることとして、最初に確認したいのは、各ピアは、こうしたQueryを受け取るとその問い合わせに対する「回答」を作り出して、それを質問者に送り返そうとするということです。探索ですから、できるだけ多くのPeerに対してこうした問い合わせを行う必要があります。問題は、Peer to Peerの通信を特色とするJXTAが、どのようにして多数のピアに対する問い合わせを実行するかということです。

 net.jxta.impl.resolver.ResolverServiceのsendQueryのソースを次に示します。このメソッドは、第1引数がnullのときには、まさに、たくさんのピアに探索メッセージを送りつける働きをします(逆に、第1引数が与えられれば、そのピアに対してだけ、メッセージを送ろうとします)。

public void sendQuery(String rdvPeer,
                      ResolverQueryMsg query)
{
    ResolverResponse doc=null;
    LOG.debug("sending query");

    Message propagateMsg = endpoint.newMessage();

    if (rdvPeer == null){
        try {
            propagateMsg.push(outQueName,(InputStream)((Document)
               (query.getDocument(new MimeMediaType("text/xml")))).getStream());
            rendezvous.propagateInGroup(propagateMsg,
                                        advertisement.getName(),
                                        outQueName,
                                        7,
                                        null);
        } catch (Exception e) {
             LOG.info( "Error during propagate" ,e);
            throw new RuntimeException ("Error during propagate :"
                                        +e.toString());
        }
    } else {

        //unicast instead
        try {
            respond (rdvPeer,
                     advertisement.getName(),
                     outQueName,
                     outQueName,
                     ((Document)
     (query.getDocument(new MimeMediaType("text/xml")))).getStream());
        } catch (Exception e) {
            LOG.info( "Error while unicasting query :", e );
            throw new RuntimeException ("Error while unicasting query :"
                                        +e.toString());
        }
    }
}

 問い合わせメッセージを、たくさんのピアに伝播(Propagate)する働きを担っているのは、次のpropagateInGroupです。後に、sendToNetwork、sendToEachRendezVous、sendToEachClientという3つのメソッドが並んでいますね。この3つのメソッドの並びが、JXTAに特徴的なPropagateの働きをよく表しています。

public void propagateInGroup(Message msg, String serviceName,
        String serviceParam, int defaultTTL,
        String prunePeer)
         throws IOException {

    Message dupMsg = msg.dup();
    if (updatePropHeader(dupMsg, defaultTTL)) {
        sendToNetwork(dupMsg, serviceName, serviceParam, prunePeer);
        sendToEachRendezVous(dupMsg, serviceName, serviceParam, prunePeer);
        sendToEachClient(dupMsg, serviceName, serviceParam, prunePeer);
    }
}

 sendToNetworkメソッドは、一番近い所に存在するピア同士がメッセージを交換するときに用いられます。同じネットワークに属するピア同士が、マルチキャストを通じて、メッセージを送るときに利用されるメソッドです。図1で、ピアAから、同一ネットワーク(正確にいえば、マルチキャスト・メッセージが到達可能なサブネット)内のピアB、C、Dに対するメッセージの送り出しです。

図1

 sendToEachRendezVousは、あるピアから、そのピアが利用しているすべてのRendezVousに対してメッセージを送り出します。図2で、E、F、G、HはRendezVousを表しています。DからE、F、Gへの送り出し、CからHへの送り出しは、sendToEachRendezVousメソッドの働きを表しています。

図2

 sendToEachClientが意味を持つのは、送り手側のピアがRendezVousである場合だけです。そうしたとき、このメソッドは、そのピアをRendezVousとしているすべてのクライアント・ピアに対してメッセージを送ります。図3で、RendezVousであるE、F、G、Hから発せられるメッセージが、このsendToEachClientメソッドの働きを示しています。

図3 

 このようにして、この3つのメソッドを内に含む、propagateInGroupメソッドの働きによって、Aから出発した探索メッセージは、ネットワークの中に大きく広がることができます。このメソッドの第4引数であるdefaultTTLは、こうした拡大を無限に続けるのではなく、ここで指定された整数値のホップでpropagateを終了せよという意味を持っています。defaultでは、このTTL(Time To Live)の値は7に設定されています。

Discoveryの働きのまとめ

 Discoveryの働きの探索は次回にも続きます。ここまでの過程を、ひとまずまとめておきましょう。

 DiscoveryインスタンスでのgetRemoteAdvertisementsメソッドの呼び出しは、探索queryメッセージを引数に与えた、Resolverインスタンス上でのsendQueryメソッドの呼び出しを引き起こします。このことは、DiscoveryServiceがより基本的なサービスであるResolverServiceに「問題解決」の仕事を依頼していると考えることができます。ResolverServiceが解決すべき問題が、この場合は、「探索依頼」だったということです。

 Resolverインスタンス上でのsendQueryメソッドの呼び出しは、今度は、Rendezvousインスタンス上でのpropagateInGroupメソッドの呼び出しを引き起こします。このメソッドによって、最初のgetRemoteAdvertisementsメソッドを発したピアからの探索queryは、ネットワーク上の数多くのピアに広がっていきます。

 ところで、この最後のメッセージのPropagateの説明には少しおかしいところがあります。先に、propagateInGroup内の3つのメソッドの働きを3つの図で説明したのですが、それぞれのPropagateの中心には別のピアが存在しています。ピアAでpropagateInGroupメソッドが呼ばれたなら、ピアB、C、Dにはメッセージが届きますが、それ以上にはメッセージは広がらないはずです。

 先の説明のようにメッセージが広がってゆくというのなら、メッセージを受け取ったピアが、バケツリレーのようにメッセージを、次のピアに送り出さなくてはなりません。JXTAには、実際そうしたメカニズムが組み込まれています。

 次回も今回に引き続き、Discoveryの働きを、さらに見ていきたいと思います。


連載記事一覧




Java Agile フォーラム 新着記事
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Java Agile 記事ランキング

本日 月間