2007年12月13日

takushimaThree Ringsフレームワークを使う(第4回)

こんにちは。

Three Ringsフレームワークを使うシリーズももう4回目になりました。これまで第1回「フレームワーク概要」第2回「ビルド環境の構築」第3回「マルチプレイのサンプル紹介」と進めてきました。最終回の今回は、第3回のサンプルのソースコードリーディングを行うという形式でお送りしたいと思います。

さてソースコードリーディングといってもサンプルを作ったのは私ですので、私自身が理解するために記録するわけではありません。読者の方がソースコードを読まれる際の副読テキストとしてお使いください。

ではまず、読む目的を決めましょう。サーバからクライアントへのデータの流れに着目することにして、「セッションの確立と、円の状態が同期される方法を理解する」ことを目的にします。先のサンプルには逆の流れ(クライアントからサーバ)も含まれていますが、ここではふれないことにします。そちらはご自身でソースコードを追ってみてください。

TestServer.java

public class TestServer extends PresentsServer implements ClientObserver, DirectProvider
{

サーバが起動しないことには、クライアントは接続もなにもできません。サーバの実装を見てみましょう。このようにTestServerクラスは PresentsServerをベースクラスとして、2つのインタフェースを実装しています。それぞれ、セッション監視 (ClientObserver)、クライアントへの位置変更RPCの提供(DirectProvider)です。詳細は後にしてエントリポイントに進みます。

    public static void main (String[] args)
    {
        TestServer server = new TestServer();
        try {
            server.init();
            server.run();
        } catch (Exception e) {
            log.warning("Unable initialize server");
            log.log(Level.WARNING, e.getMessage(), e);
        }
    }

初期化(init)と接続受付スレッドの起動(run)を行っています。ただこれは定型文のようなもので、コピペするところです。initを見てみます。

    public void init () throws Exception
    {
        super.init();

super.init ()によりPresentsServerの初期化が行われます。PresentsServerは最低限必要であろうマネージャ(接続管理、クライアント管理、DObject管理、PRC管理)をサブクラスに提供してくれます。PresentsServerでマネージャはこのように定義されています。

    /** The manager of network connections. */
    public static ConnectionManager conmgr;

    /** The manager of clients. */
    public static ClientManager clmgr;

    /** The distributed object manager. */
    public static PresentsDObjectMgr omgr;

    /** The invocation manager. */
    public static InvocationManager invmgr;

これらのフィールドはよく使うので覚えておきましょう。また独自のマネージャを作る場合も、同じように定義してinitで初期化するやり方で組み込めばよさそうです。ではsuper.init()後に戻ります。

_distObj = omgr.registerObject(new DistributedObject());

DObject マネージャ(PresentsDObjectMgr)にDistributedObjectのインスタンスを登録しています。 DistributedObjectはDObjectをベースクラスにもつ、データオブジェクトです。サンプル用に作成しました。 DistributedObjectにはクライアントに対応する円を保持するハッシュテーブル(DSet)が定義されています。

DObject は前回のヒント集で分散オブジェクトと呼んでいたものです。ここで作成したDistributedObjectはネットワークをはさんでいてもインスタンスが共有されているかのように扱えます。またThree Ringsの分散オブジェクトはデフォルトではサーバ側でのみ書き換え可になるので、共有されているといってもクライアント側が勝手に値を書き換えることはできません。主にチート防止のためです。

clmgr.addClientObserver(this);

セッション監視のためのリスナー登録です。

clmgr.setClientFactory(new ClientFactory() {...}

ここは2段になった匿名クラスを使っているのでちょっと分かりにくいですね。しかも両方ファクトリクラスという。これは独自に用意したBootstrapDataを使うための作業です。BootstrapDataはクライアントの接続時にサーバから渡される初期情報になります。

init内で見るべきところはこんなものでしょうか。runはサブクラスでオーバーライドする必要はないので飛ばします。ここまででサーバはクライアントの接続を待ち受けることができるようになります。ではクライアントが接続してきた後に進みますよ。

TestServerはClientObserverを実装していますので、セッションに関連したイベントが受け取れます。

    public void clientSessionDidStart (PresentsClient client)
    {
        Circle circle = new Circle();
        circle.id = client.getClientObject().getOid();
        circle.x = Constants.VIEW_WIDTH / 2;
        circle.y = Constants.VIEW_HEIGHT / 2;
        int r  = (int)(Math.random() * 255);
        int g  = (int)(Math.random() * 255);
        int b  = (int)(Math.random() * 255);
        circle.rgb = new Color(r, g, b).getRGB();

        _distObj.addToCircles(circle);
    }

これはクライアントの接続後に呼び出されるメソッドです。クライアントと1対1で対応したCircleインスタンスを作成し、 DistributedObjectのハッシュテーブルに追加しています。addToCirclesによってCircleインスタンスはシリアライズされ、DistributedObjectを共有しているクライアントたちに転送されます。こうして分散オブジェクトの状態は同期されることになります。

DistributedObject.java

クライアント側に移る前にDistributedObjectを見てみましょう。実はサンプルのソースコードにはいくつか自動生成されたものが含まれていますが、DistributedObjectもそうしたもののひとつです。build.xmlのgendobjターゲットを実行するとファイル名が Object.javaで終わるソースコードに修正が加えられます。対象のクラスではDObjectを継承しておいてください。

        // AUTO-GENERATED: METHODS START
        ...
        // AUTO-GENERATED: METHODS END

で囲まれた区間にデータ更新用のメソッド(ex:addToCircles)が追加されていることが確認できたでしょうか?まぁ難しく考えなくてもコードジェネレータのおかげで新しい分散オブジェクトを作るときは、ただフィールドを定義すればよいってことです。

TestClient.java

それではクライアント側に移りましょう。サーバ側の準備は整いました。クライアントはサーバに接続して、DistributedObjectの更新を受け取り、描画すればよいことになります。

クライアント側では大まかに通信と描画でクラスを分けています。TestClientが通信をつかさどり、FieldPanelがシーンの描画を行っています。MVCをご存知の方はDObjectを Model、FieldPanelをView、TestClientをControllerだと思っていただくと整理しやすいと思います。

public class TestClient extends JFrame implements RunQueue, SessionObserver, Subscriber, KeyListener, MouseListener
{

TestClientはいろいろなインタフェースを実装していますが、ポイントは接続監視(SessionObserver)とDObjectの購読監視(Subscriber)でしょうか。

クライアントの事実上のエントリポイントはコンストラクタです。connectの呼び出しとウィンドウの作成を行っています。

    private void connect ()
    {
        UsernamePasswordCreds creds = new UsernamePasswordCreds(new Name("test" + System.currentTimeMillis()), "test");

        _client = new Client(creds, this);
        _client.setServer("localhost", Client.DEFAULT_SERVER_PORTS);
        _client.addClientObserver(this);

        _client.logon();
    }

connect はPresentsServerに対応した通信用のオブジェクト(Client)を生成しています。Clientには接続先のIPアドレスや、認証情報(UsernamePasswordCreds)、接続状況を知るためのリスナー(SessionObserver)を渡しています。ログオンに成功すると次のメソッドが呼び出されます。

    public void clientDidLogon (Client client)
    {
        SimpleBootstrapData bstrap = (SimpleBootstrapData)client.getBootstrapData();
        client.getDObjectManager().subscribeToObject(bstrap.distOid, this);
        ...
    }

BootstrapData からDistributedObjectのIDを取り出し購読を要求(subscribeToObject)しています。この段階では DistributedObjectのリファレンスは取得できていないため、IDでオブジェクトを指定しています。そもそも subscribeToObjectがリファレンスを得るための行為になります。また購読すると以後、同期情報が届くようになります。

    public void objectAvailable (DistributedObject object)
    {
        ...
        _panel.setDistObj(object);
        ...
    }

要求が成功するとobjectAvailableが呼び出され、ようやくリファレンスを取得できます。TestClientがDistributedObjectに関知するのはここまでで、以後はFieldPanelに処理が移っていきます。

FieldPanel.java

このクラスはTestClientから渡されたDistributedObjectのフィールド(ハッシュテーブル)を監視して、状態に応じた表示を行っています。

public class FieldPanel extends JPanel implements SetListener
{

DObjectへのフィールド更新イベントのリスナーインタフェース(SetListener)を実装しています。

    public void setDistObj (DistributedObject distObj)
    {
        ...
        _distObj.addListener(this);
    }

まずTestClientから受け取ったDistributedObjectに自身(リスナー)を登録しておきます。これで3つのイベントが受け取れるようになりました。以下のメソッドがそうです。

    public void entryAdded (EntryAddedEvent event) {...}
    public void entryRemoved (EntryRemovedEvent event) {...}
    public void entryUpdated (EntryUpdatedEvent event) {...}

引数のイベントオブジェクトを使えばDistributedObjectのどのフィールドが更新されたかを知ることができます。しかし今回は円のハッシュテーブルに更新があったことだけ分かればよく、細かい内容に興味はありませんから、entry~ではただ再描画を要求するだけになっています。

さあようやく描画するところまで着きました。paintComponentにあります。

    public void paintComponent (Graphics gc)
    {
        ...
            for (Circle circle : _distObj.circles) {
                ...
                gc.drawOval(circle.x - Constants.CIRCLE_RADIUS,
                            circle.y - Constants.CIRCLE_RADIUS,
                            Constants.CIRCLE_RADIUS * 2,
                            Constants.CIRCLE_RADIUS * 2);
            }
        ...
    }

ただハッシュテーブルをたどり円を描いておしまいです。

長々と読んできましたが「セッションの確立と、円の状態が同期される方法を理解する」ことができましたか?全て理解していなくてもDistributedObjectを改造することはできるのではないでしょうか?ぜひ挑戦してみてください。

それでは

Leave a Comment

Trackbacked

trackback url for this entry: http://www.pyramid-inc.net/lab/archives/68/trackback