#RMI Java Remote Method Invocation
二つの異なるJVM同士でプロセス間通信を行う。サーバー=受信側は任意の文字列キーをBindして、接続を待つ。クライアント=送信側はそのキーでサーバーに接続する。
そして共通インターフェイスにあるメソッドをクライアント側からコールすると、サーバー側のスコープで実行される。
クライアントからサーバーはメソッドの引数、その逆はメソッドの戻り値で値をやり取りする。ここで指しているクライアント側からのコールはクライアントの好きなタイミングで出来るが、サーバーからは出来ない。http1でサーバー側からのプッシュは基本できないのと似てる。
セキュリティなにそれなローカル専用。CentOS6とwin7環境で確認。環境は偶然どっちも以下のとおり。ビルド番号まで同じだった。
>java -version
java version "1.8.0_45"
Java(TM) SE Runtime Environment (build 1.8.0_45-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.45-b02, mixed mode)
送信と受信二つのJVMインスタンスの他にrmiregistryというプロセスも必要。
rmiregistry -J-Djava.rmi.server.useCodebaseOnly=false
というコマンドを立ち上げる。&を付けてバックグラウンドプロセス化するのがベター。起動したら放置で多分OK。
パラメーターは無くても起動出来るけど、代わりに後述のインターフェイスとなるクラス名を書かないといけないので管理が面倒
さらに後述するけどセキュリティポリシーも多分必須。
##シンプルにサーバー・クライアント形式で接続
まずは共通となるインターフェイスを作る。サーバー側とクライアント側でやり取りするメソッドを定義する。
送信側と受信側と接続するステップにおいてはパッケージ名とインターフェイス名が一致する必要があって、メソッドについてはコールする時に一致判定が行われる。ので余計なメソッドがある分には問題ない。クラスにthrows RemoteException;
とあるのは必須。無いと実行時にエラーが出る。
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface RmiInterface extends Remote {
void sendData(String i) throws RemoteException;
}
java.rmi.ServerException: RemoteException occurred in server thread; nested exception is:
java.rmi.UnmarshalException: unrecognized method hash: method not supported by remote object
at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:354)
サーバー 受信側のコードは以下のとおり。クライアント 送信側からの接続はいくらでも受け付ける。接続時、切断時のメソッドは無いっぽい。接続時はインターフェイスにonConnect()
とか定義すればいいけど切断は気がつかない?
コード見れば分かるけど、接続されたクライアントの情報を取るメソッドは無いからサーバーからクライアントへの直接の情報送信は出来ない。
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
public class Server implements RmiInterface {
public static void main(String[] args) throws Exception {
Server obj = new Server();
RmiInterface stub = (RmiInterface) UnicastRemoteObject.exportObject(obj, 0);
LocateRegistry.getRegistry().bind("RmiTest", stub);//ここでRmiTestの名前で接続を待つ
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
public void run() {
//unbindせずにプロセスが死ぬと、再度bindする時に怒られてrmiregistryプロセスの再起動が必要になる
try {
LocateRegistry.getRegistry().unbind("RmiTest");
} catch (RemoteException | NotBoundException e) {
e.printStackTrace();
}
}
}));
Thread.sleep(999 * 1000);//終了されるとマズいから手抜き
}
@Override
public void sendData(String i) {//クライアントのインスタンスからコールされる
System.err.println(i);
}
}
クライアント 送信側 いくらでも起動すればよい。
import java.rmi.Naming;
public class Client {
public static void main(String[] args) {
try {
System.setSecurityManager(new SecurityManager());//セキュリティポリシーをセット
RmiInterface i = (RmiInterface) Naming.lookup("rmi://localhost/RmiTest");//localhost部分は固定。RmiTestをサーバー側と一致させる
int count = 1;
while (true) {
Thread.sleep(1 * 1000);
i.sendData("ok-" + count);//ここでok-1 とかの引数がサーバー側のメソッドで実行される
count++;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
セキュリティポリシーファイル ファイルとしてはどうしても必要っぽい。「セキュリティ無し。全部許可」くらいパラメーターで指定出来たらいいのに。
grant {
permission java.security.AllPermission;
};
実行するコマンドライン
前述のRmiInterface.java
、Server.java
、Client.java
を1つのjarファイルに固めた前提とする
サーバー・クライアント共通に必要なコマンドは以下のとおり。コマンドの最後に&を付けてバックグランドにするのが普通かな。
rmiregistry -J-Djava.rmi.server.useCodebaseOnly=false
サーバー 受信側に必要なコマンドは以下のとおり。パラメーターは全て必要。codebaseはフルパスで指定する必要があるっぽい。もちろん改行消して1行にするべし。
rmiregistryのuseCodebaseOnlyを指定しないと、rmiregistryにいちいちcodebaseを指定しないといけない。めんどい。
java -Djava.rmi.server.useCodebaseOnly=false
-Djava.rmi.server.codebase=file:C:/test/hoge.jar
-Djava.security.policy=java.policy
-cp hoge.jar Server
クライアント 送信側に必要なコマンドは以下のとおり。いくらでも起動して良い。同一のjarに固めてるから起動するクラスの指定が違うだけ。
サーバー側もクライアント側も、必要なパラメーターは同じ。
java -Djava.rmi.server.useCodebaseOnly=false
-Djava.rmi.server.codebase=file:C:/test/hoge.jar
-Djava.security.policy=java.policy
-cp hoge.jar Client
これでシンプルなサーバー・クライアント形式の通信が出来る。
bindするキーを動的に指定出来るし、コマンドライン的にはサーバー側クライアント側の区別は無いので、適当に乱数を挟むとクライアント側でもサーバーを起動して双方向の通信も可能になる。
自分の環境で、どうすればちゃんとunbind出来るのかは確認するべし。