lipermimodその2 コード概要
Java-RMIの代替RMIライブラリlipermimodの紹介の続きである。lipermimodについての全投稿は/tag/lipermimodにある。
ここでは具体的にコードの記述方法を見ていく。
現在のところ暗号化は無し
その前にだが、これを書いている現在(2018/5/24時点)では暗号化は動作していない。このためインターネットを通すべきではない。自分はLAN内で使用するので、そんなものが必要無いので実装が遅れている。
サーバ側の作成
これは完全に一つのサーバと複数のクライアントとの通信インフラであり、クライアントどうしの通信はできないことに注意。そして、サーバ側は単一のポートでクライアントからの接続を待ち受ける。
クライアントが接続した後では、そのクライアントからのメソッドコールをサーバ側で受け値を返すことができ、クライアントからコールバックインターフェースを受け取った場合は、それを使ってクライアント側をコールすることができる。
最も単純な場合のサーバの作成は以下である。
Server server = new Server();
server.bind(4455); // 任意のポート番号
しかし、このままではクライアントを作成しても何のサービスもできないため、あらかじめサーバ側でサービス用オブジェクトを作成し、それをグローバルとして登録する必要がある。
Server server = new Server();
server.registerGlobal("serverGlobal", new ServerGlobalImpl());
server.bind(4455);
クライアント側は、”serverGlobal”という名前を指定することにより、ServerGlobalImplのインターフェースを取得し、このメソッドを呼び出すことになる。
ServerGlobalの書き方
もちろん名前は何でもよい。これはあくまでも例であるが、サーバ側で最初に提供するサービス、ServerGlobalの記述方法としては以下である。
まず、IRemoteインターフェースを拡張するServerGlobalインターフェースを定義する。
public interface ServerGlobal extends IRemote {
public String hello(String value);
}
注意点としては、
- このインターフェースは必ずIRemoteを拡張していなくてはいけない。直接的にでも間接的にでもよい。
次に、このインターフェースを実装するクラスを作成するのだが、クライアント側に渡されるのは当然ながらインターフェースのみである。当たり前だが、オブジェクトそれ自身が渡されてしまったら、クライアントは自己完結してしまい、サーバ側を呼び出すことにはならない。
private static class ServerGlobalImpl implements ServerGlobal {
public String hello(String value) {
System.out.println("server side:" + value);
return value + " world";
}
}
このオブジェクトを最初に「グローバル」として公開しておく。
server.registerGlobal("serverGlobal", new ServerGlobalImpl());
クライアントの作成
クライアント側では、まず最初にポートを指定してサーバ側に接続し、サーバがグローバルとして公開しているオブジェクトのインターフェースを得る。
あとは、そのメソッドを呼び出せばよい。
Client client = new Client();
client.connect("localhost", 4455);
ServerGlobal serverGlobal = client.getGlobal("serverGlobal");
System.out.println("client side:" + serverGlobal.hello("hello"));
何が起こっているのか?
もちろん、サーバ・クライアントは別のプログラムであり、共通部分はlipermimodライブラリとServerGlobalというインターフェースだ。サーバ側作成したその実装である。ServerGlobalImplはクライアントコードでは必要無い。つまり、ServerGlobalを実装したオブジェクトがクライアントに渡されるわけではない。
つまり、クライアント側にはServerGlobalインターフェースを実装したクラスは存在していないのだ。
しかし、なぜそれがクライアント側に渡され、そのメソッドが呼び出せるかといえば、java.lang.reflect.Proxyのおかげである。簡単に言えば、インターフェースを指定するとそのオブジェクトを動的に作成する機能だ。そして、作成時に「そのメソッドを呼び出すと相手側に通信する」という処理を組み込んでいるのである。
※この具体的な処理は、com.cm55.lipermimod.proxy.ProxyObjectsで行っている。
このため、クライアント側ではインターフェースの名前を受け取るだけなのだが、その実際のオブジェクトをその場で生成する。そして、そのメソッドを呼び出すとサーバ側への通信を行うというわけである。
インターフェースの定義について
両者でやりとりされるインタフェースについては、ごく普通のJavaのインターフェースからは少々制限がある。
IRemoteを拡張していること
このインターフェースでは、IRemoteを直接的にあるいは間接的に拡張していることが必要である。
一つのオブジェクトに複数のインターフェースが可能
上にあげた例では、単一のインターフェースしか使っていないが、実際には複数のインターフェースが可能である。例えば、
public interface Foo extends IRemote {
}
public interface Bar extends IRemote {
}
public static class FooBar implements Foo, Bar {
}
この場合、FooBarオブジェクトが転送される場合に、Fooとして受け取ってもBarとして受け取ってもよい。
IRemoteを拡張しないインターフェースは無視される
例えば以下のような場合、
public interface Foo extends IRemote {
}
public interface Bar extends IRemote {
}
public interface Sample {
}
public static class FooBar implements Foo, Bar, Sample {
}
SampleはIRemoteを拡張していないので、クライアント側でFooBarをSampleとして受け取ることはできない。このインターフェースが伝達されることは無い。
全インターフェースメソッド名はユニークであること
例えば、次のようなケースは許されない。
public interface Foo extends IRemote {
void call(int value);
}
public interface Bar extends IRemote {
void call(String value);
}
public static class FooBar implements Foo, Bar {
public void call(int value) {}
public void call(String value) {}
}
二つのインターフェースは引数の異なる同じ名前のメソッドを定義している。もちろん、通常のJavaプログラムでは許されるのだが、lipermimodではエラーになる。
この理由は、メソッド呼び出しの際の通信文において、メソッド引数の型まで記述するのが無駄と思われるからだ。つまり、メソッド呼び出し(実際には通信)においては、メソッド名だけが伝達されるため、メソッド名だけで実際のメソッドが特定されなければならず、引数による違いは認識されない。
メソッドの引数と返り値はSerializableであること
メソッド呼び出し及びその返り値においては、オブジェクトがシリアライズされて伝達される。このため、すべての引数と返り値はシリアライズ可能である必要がある。
Javaのシリアライゼーションにおいては、intやString等は、はなからシリアライズ可能であるので考慮の必要が無いのだが、自作のクラスについては、少なくともSerializableを実装している必要がある。
public class MyObject implements Serializbale {
}
IRemoteはSerializableではないこと
上記とは逆に、IRemoteの派生インターフェースはSerializableであってはならない。そもそも、このインターフェースを実装したオブジェクト自体が相手側に送られるわけではないため、その概念はSerializableとは矛盾している。したがって、次のようなインターフェースを実装するオブジェクトを転送しようとするとエラーになる。
public interface Foo implements IRemote, Serializable {
}
もちろん以下もエラーになる
public interface Foo implements IRemote {
}
public class FooImpl implements Foo, Serializable {
}
サーバからクライアントの呼び出し
インターフェースとメソッド引数については、上述したものがすべてである。この規則のもとで自由にシステムを構成することができる。
サーバからクライアントを呼び出してもらうことも可能である。例えば、こういうシナリオを考えてみる。
- クライアントがサーバに対し、長時間かかる処理を依頼し、同時に処理が終了したらその旨通知してもらうように依頼する。
- サーバはこれを引き受け、処理を行い、終了したらクライアントを呼び出す。
これを行うには、例えば次のようなインターフェースを書く。
public interface ServerGlobal {
/** サーバに重い処理を要求する。終了したらCallbackを呼び出してもらう */
public void heavyWork(Callback callback);
}
public interface Callback {
/** 処理終了がクライアント側に通知される */
public void finished();
}
サーバ側の実装は以下のようなものになる。
public class ServerGlobalImpl implements ServerGlobal {
public void heavyWork(Callback callback) {
// 重い処理をする
...
// クライアントに通知する
callback.finished();
}
}
クライアントとしては以下だ。
// サーバグローバルを取得する
ServerGlobal serverGlobal = ....
// 重い処理を依頼する
severGlobal.heavyWork(new Callback() {
public void finished() {
System.out.println("終了した");
}
});
つまり、最初はあらかじめサーバ側にグローバルとして登録されたServerGlobalのオブジェクトをクライアントが受け取るだけなのだが、サーバに対してクライアント側で作成したオブジェクトを受け渡すこともでき、それをサーバ側が呼び出すことができる。
この通信はクライアントから始まるのだが、通信自体は双方向で何の制限も無く、サーバとクライアントは対等な関係である。ただし、クライアントはただ一つのサーバとの通信を行うのに対し、サーバは一度に複数のクライアントに対応している点が異なる。