|
FAQ |
Naming.lookup
を呼び出すと、予期しないホスト名やポート番号に対する例外が発行されます。なぜですか。
CLASSPATH
に _Stub ファイルをインストールする必要がありますか。ダウンロードできるのではないかと思いますが。
java.lang.ClassMismatchError
が返されます。なぜでしょうか。
ArrayStoreException
が発行されます。どうなっているのですか。
ClassNotFoundException
が発行されます。どうなっているのですか。
java.net.UnknownHostException
を発行して失敗します。なぜですか。
UnknownHostException
が発行されます。
Naming.bind
と Naming.lookup
に非常に時間がかかるのはなぜですか。
java.net.SocketException:Address already in use
] が発行されるのですが、なぜですか。
System.exit
を使うのは賢明でしょうか。
rmic
コマンドを実行させるにはどうすれば良いでしょうか。
select()
呼び出しでブロック化ではなくポーリングしているようなのですが、レジストリはポーリングで実装されているのですか。
ObjectOutputStream
に書き込むために、クラスが Serializable
を実装しなければならないのはなぜですか。
ObjectOutputStream
から ObjectInputStream
を作成するには、どうすれば良いでしょうか。
writeObject
を使ってネット上を送信し、readObject
を使って受信します。次にオブジェクトのフィールドの値を変更してから同様に送信すると、readObject
から返されるオブジェクトは最初のものと同じで、フィールドの新しい値が反映されていないようです。これは正しい動作なのですか。
Serializable
を実装せず、そのサブクラス B が Serializable
を実装している場合、クラス B を直列化したとき、クラス A のフィールドは直列化されますか。
Naming.lookup
を呼び出すと、予期しないホスト名やポート番号に対する例外が発行されます。なぜですか。
サーバのホスト名や IP アドレスが誤っている (またはクライアントが解釈できないホスト名をサーバが持っている) 場合でも、サーバはその誤ったホスト名を使ってすべてのオブジェクトをエクスポートします。ただし、それらのオブジェクトを受け取ろうとするたびに例外が発生します。
レジストリの位置を示すために Naming.lookup
で指定したホスト名は、サーバへのリモート参照にすでに組み込まれているホスト名には効果がありません。
通常、不可解なホスト名はサーバの修飾されていないホスト名、つまりクライアントのネームサービスに知らされていない非公開な名前です。Windows の場合、その名前はサーバの [ネットワーク] -> [ユーザー情報] -> [コンピュータ名] で設定されています。
対策としては、サーバの起動時にシステムプロパティ java.rmi.server.hostname
を設定します。このプロパティの値は、外部からアクセス可能なサーバのホスト名 (または IP アドレス) でなければならず、これを Naming.lookup
のホスト部分に指定したときの動作は成功します。
詳細については、コールバックと完全修飾ドメイン名に関する質問を参照してください。
CLASSPATH
に _Stub ファイルをインストールする必要がありますか。ダウンロードできるのではないかと思いますが。
java.rmi.server.codebase
プロパティを注釈として付けている場合は、スタブクラスをダウンロードできます。リモートオブジェクトをエクスポートするサーバ上の java.rmi.server.codebase
プロパティを設定する必要があります。リモートクライアントがこのプロパティを設定できるようになると、指定されたコードベースだけからリモートオブジェクトを入手するように制限されます。すべてのクライアント VM が、オブジェクトの場所を示すコードベースを指定しているわけではありません。
リモートオブジェクトが RMI によって整列化されるとき (リモート呼び出しの引数として、または戻り値として)、スタブクラスのコードベースが RMI によって取得され、直列化されたスタブの注釈付けに使用されます。スタブが整列化解除されるときは、CLASSPATH
またはアプレットコードベースなど、オブジェクトを受け取るためのコンテクストクラスローダにすでにそのクラスが存在しない限り、RMIClassLoader
を使ってスタブのクラスファイルをロードするためにコードベースが使用されます。
_Stub クラスが RMIClassLoader
によってロードされた場合は、RMI はすでに、注釈付けに使用するのはどのコードベースかを知っています。_Stub クラスが CLASSPATH
からロードされた場合は、明確なコードベースは存在しないので、RMI は java.rmi.server.codebase
システムプロパティを調べてコードベースを検索します。このシステムプロパティが設定されていない場合は、スタブは null コードベースで整列化されます。つまり、クライアントがその CLASSPATH
内に _Stub クラスファイルに一致するコピーを持っていない限り、このコードベースは使用できません。
コードベースプロパティを指定することは忘れがちです。この間違いを検出する方法の 1 つに、rmiregistry
を個別に起動し、アプリケーションクラスにアクセスしないという方法があります。こうすると、コードベースが省略された場合は、必ず Naming.rebind
が失敗します。
javaw
コマンドは、stdout と stderr に出力を行うので、デバッグ目的では java
コマンドを別のウィンドウで実行してエラーの出力を表示できるようにする方が良いでしょう。これを行うには、次のようにコマンドを実行します。
start java EchoImpl
開発時には、javaw
コマンドの使用はお勧めしません。 サーバの活動を観察するには、-Djava.rmi.server.logCalls=true
を指定してサーバを起動します。
java.lang.ClassMismatchError
が返されます。なぜでしょうか。
java.rmi.registry.RegistryImpl
を含む) を再起動してみてください。これで問題がなくなります。
ArrayStoreException
が発行されます。どうなっているのですか。
FooRemote[] f = new FooRemote[10]; for (int i = 0; i < f.length; i++) { f[i] = new FooRemoteImpl(); }
このようにすれば、RMI が配列の各セルにスタブを入れても、リモート呼び出しでの例外は発生しません。
分散オブジェクトはローカルオブジェクトとは動作が異なります。ロックと障害を処理することなく、ローカルの実装を再利用するだけでは、予測不可能な結果が生じる可能性があります。
ClassNotFoundException
が発行されます。どうなっているのですか。
java.rmi.server.codebase
プロパティを設定する必要があります。CLASSPATH を簡単にチェックするには、完全に指定されたクラス名 (com.bar.pro.Useful など) を指定して javap
コマンドを使う方法があります。このコマンドは、現在の CLASSPATH を使ってクラスへのインタフェースを検索し、出力します。
java -Djava.rmi.server.logCalls=true YourServerimplここで
YourServerImpl
はサーバ名です。サーバがハングしたら、Solaris では ctrl-¥ 、Windows では ctrl-break キーを押すことでモニタダンプとスレッドダンプが得られます。
RMI クライアントがリモート RMI サーバにコンタクトするためには、クライアントはまず最初にサーバへの参照を取得する必要があります。Naming.lookup
メソッド呼び出しは、クライアントがリモートサーバへの参照を最初に取得する最も一般的なメカニズムです。リモート参照は他の方法でも取得することができます。たとえば、すべてのリモートメソッド呼び出しではリモート参照が返されます。Naming.lookup
は、良く知られたスタブを使って rmiregistry
へのリモート呼び出しを行います。rmiregistry
は lookup
メソッドに要求されたオブジェクトへのリモート参照を返します。
すべてのリモート参照にはサーバのホスト名とポート番号が含まれるため、クライアントはそれを使って、特定のリモートオブジェクトのサーバである Virtual Machine を特定することができます。RMI クライアントがリモート参照を取得したあとは、クライアントは参照内のホスト名とポートを使ってリモートサーバへのソケット接続を開くことができます。
RMI では、「クライアント」と「サーバ」という用語は、同一の Java プログラムを指す場合があることに留意してください。RMI サーバとして機能する Java プログラムには、エクスポートされたリモートオブジェクトが含まれます。RMI クライアントは、別の Java Virtual Machine (JVM) 内のリモートオブジェクト上の 1 つ以上のメソッドを呼び出すプログラムです。1 つの JVM がこの両方の機能を実行する場合は、RMI クライアントと RMI サーバの両方として扱われます。
java.net.UnknownHostException
を発行して失敗します。なぜですか。
UnknownHostException
を発行します。
実際に機能するリモート参照を作成するには、RMI サーバはすべての RMI クライアントが解釈可能な完全修飾のホスト名または IP アドレスを提供できなければなりません (完全修飾ホスト名の例: foo.bar.com)。RMI プログラムがリモートコールバックオペレーションを提供する場合、そのプログラムは RMI オブジェクトとして機能するため、RMI クライアントに渡すリモート参照でサーバホスト名として使用する解釈可能なホスト名を決定できる必要があります。リモートオブジェクトとして機能するアプレットを呼び出す JVM は、アプレットが有効なサーバホスト名を提供できなかった場合には UnknownHostException
を発行することがあります。
RMI アプリケーションが UnknownHostException
を発行したときは、結果のスタックトレースを調べて、クライアントがリモートサーバとのコンタクトに使っているホスト名が間違っていないか、また完全修飾されていないかどうかを確認してください。必要なら、サーバの java.rmi.server.hostname
プロパティにサーバマシンの正しい IP アドレスまたはホスト名を設定します。RMI はこのプロパティの値を使ってサーバへのリモート参照を作成します。
UnknownHostException
が発行されます。
java.rmi.server.hostname
プロパティを、RMI サーバマシンの正しい IP アドレスに設定します。このプロパティを次のように設定することにより、ネームサービスから取得した完全修飾ホスト名をサーバが使用するよう指定することもできます。
java.rmi.server.useLocalHostname=true
java.net.InetAddress.getLocalHost()
に依存して完全修飾ドメイン名を返していました。InetAddress
オブジェクトが、コードの static ブロック内のローカルホスト名を初期化し、ローカル IP アドレスを逆ルックアップしてローカルホスト名を取得しました。しかし、ネットワークに接続されていないマシンでこの動作を実行すると、InetAddress
がホスト名を探しても見つからないため、ハングアップが起こりました。
InetAddress
を修正して、ネームサービスに問い合わせをしないネイティブのシステムコールから返される「修飾されていない可能性がある」ホスト名だけを取得するようにしました。java.rmi.server.hostname
プロパティを使うと、ユーザは InetAddress
から提供される正しくないホスト名を無視できるので、RMI にはこの変更を補うための修正は行いませんでした。RMI はネームサービスに問い合わせをせず、修飾されていないホスト名をデフォルトで使用できました。
RMI はあるリモートオブジェクトのサーバのマシンを識別するために、IP アドレスまたは完全修飾のドメイン名を使います。サーバのホスト名は、次の動作によって取得した値に初期化されます。
java.rmi.server.hostname
プロパティが設定されている場合は、RMI はそのプロパティの値をサーバのホスト名として使用し、他の方法で完全修飾ドメイン名を探すことはありません。このプロパティは、RMI サーバ名を探す他のすべての方法に優先します。
java.rmi.server.useLocalHostname
プロパティが true
(デフォルトでは、このプロパティの値は false) に設定されている場合、RMI は次の手順で RMI サーバのホスト名を取得します。
InetAddress.getLocalHost().getHostName()
メソッドから返された値に「.」文字が含まれている場合、RMI はこの値をサーバの完全修飾ドメイン名とみなし、サーバのホスト名として使用します。
InetAddress.getLocalHost().getHostAddress()
から取得したサーバの IP アドレスを使用します。
sun.rmi.transport.tcp.localHostnameTimeOut
=timeOutMillis
java -Dsun.rmi.transport.tcp.localHostnameTimeOut=2000 MyServerApp
java.rmi.server.useLocalHostname
プロパティを true
に設定することを推奨します。一般的に、ホスト名は IP アドレスよりも永続性があります。起動可能なリモートオブジェクトは、一時リモートオブジェクトよりも長時間存在する傾向があります (たとえば、リブート後も存在する)。RMI クライアントは、明示的な IP アドレスよりも修飾されたホスト名を使う方が、より長期間にわたってリモートオブジェクトを検出できます。
Naming.bind
と Naming.lookup
に非常に時間がかかるのはなぜですか。
java.net.InetAddress
を使って TCP/IP ホスト名をルックアップします (InetAddress
クラスは、セキュリティ上の理由から、ホストからアドレスへのマッピングとアドレスからホスト名へのマッピングの両方を行う)。Windows では、ルックアップ機能は Windows のネイティブのソケットライブラリによって行われるので、遅れは RMI ではなく Windows のライブラリの中で起こります。ホストが DNS を使用するよう設定されていて、DNS サーバが通信に関係するホストについての情報を持たず、DNS ルックアップタイムアウトが起こっている場合は問題です。関連するすべてのホストのホスト名またはアドレスをローカルファイル ¥winnt¥system32¥drivers¥etc¥hosts
または ¥windows¥hosts
に書き込んでください。ホストファイルの書式は一般的に、次のようなものです。
IPAddress Machine Nameたとえば:
208.2.84.61 homerこの処置により、最初のルックアップに要する時間を大幅に短縮できます。
192.168.1.1
) を設定します。すると、これを DOS シェルから検出して ping を実行することができます (ping mymachine)。これで、RMI のサンプルプログラムを実行できます。
java.net.SocketException:Address already in use
] が発行されるのですが、なぜですか。
RegistryImpl
が使用するポート (デフォルトは 1099) がすでに使用されていることを意味します。マシン上にすでに実行中の別のレジストリがあると思われるので、それを停止させてください。
RMI が目的のサーバへの通常の (SOCKS) 接続の作成に失敗し、HTTP プロキシサーバが設定されていると通知した場合は、そのプロキシサーバを通じて RMI 要求を 1 つずつトンネリングしようとします。
HTTP トンネリングには 2 つの形式があり、順番に試みられます。1 つ目の形式は、http-to-port で、2 つ目は http-to-cgi です。
http-to-port トンネリングでは、RMI は目的のサーバの正確なホスト名とポート番号を指す http:
URL への HTTP POST 要求を試みます。HTTP 要求には 1 つの RMI 要求が含まれます。HTTP プロキシはこの URL を受け付けると、待機中の RMI サーバにこの POST 要求を転送します。RMI サーバは要求を認識してラップを解除します。呼び出しの結果は HTTP 応答にラップされ、同じプロキシ経由で返送されます。
HTTP プロキシは、異常なポート番号に対する要求を拒否することがよくあります。この場合、RMI は http-to-cgi トンネリングを実行しようとします。RMI 要求は前回同様 HTTP POST 要求内にカプセル化されますが、要求 URL の形式は http://hostname:80/cgi-bin/java-rmi.cgi?port=n
(hostname と n は目的のサーバのホスト名とポート番号) になります。サーバホストのポート 80 で待機している HTTP サーバが必要で、これは
java-rmi.cgi
スクリプト (JDK で提供) を実行します。次に、このスクリプトはポート n で待機中の RMI サーバに要求を転送します。RMI は HTTP トンネリングされた要求を、HTTP サーバや CGI スクリプトなどの外部からの助けなしにラップ解除できます。そのため、クライアントの HTTP プロキシがサーバのポートに直接接続できる場合は、java-rmi.cgi
スクリプトはまったく必要ありません。
HTTP トンネリングの使用をトリガするためには、標準システムプロパティ http.proxyHost
をローカル HTTP プロキシのホスト名に設定する必要があります。報告によると、Navigator の一部のバージョンではこのプロパティが設定されません。
HTTP トンネリングの最大の短所は、内向きの呼び出しや多重接続を許可しない点です。第 2 の短所は、http-to-cgi 方式ではサーバ側に深刻なセキュリティホールができてしまうという点です。その理由は、この方式では、修正しない限り、どんなポートへの内向きの要求もすべてリダイレクトされてしまうからです。
socksProxyHost
は、SOCKS サーバのホスト名に設定されている必要があります。SOCKS サーバのポート番号が 1080 でない場合は、socksProxyPort
プロパティで設定しなければなりません。
この方法が、最も一般的に使用できる方法と考えられます。現在のところ、ServerSockets
は SOCKS を使用しないので、内向きの呼び出しには別のメカニズムを使う必要があります。
この方法の短所は、ファイアウォールの通過を RMI サーバ側から提供されたコードを使って行わなければならない点です。RMI サーバ側では正しい通過方法が分かっているとは限らず、またファイアウォールを通過するための十分な特権を自動的に保持するとは限りません。
exportObject
メソッドには正確なポート番号を指定するための特別な引数があります。JDK 1.1 では、サーバは RMISocketFactory
をサブクラス化して、createServerSocket(0)
への要求を横取りし、特定のポート番号に結びつける要求と置き換えます。
この方法の短所は、ローカルファイアウォールの責任者であるネットワーク管理者の助けが必要なことです。エクスポートされたオブジェクトが別の場所で実行される (コードがそのサイトにダウンロードされたため) 場合、ローカルファイアウォールを管理するネットワーク管理者には、実行しようとしているユーザが誰かわかりません。
ここでの考え方は、「ファイアウォールの外側からそのオブジェクトのリモートメソッドを呼び出そうとする者は誰でも、かわりに(おそらく別のマシンの) 別のポートにコンタクトする」という方法でオブジェクトをエクスポートするということです。この別のポートでは、実際のサーバへの二次的な接続を作成して両方向にバイトを送り出すプログラムが稼動しています。
この方法で難しいのは、ブリッジに接続することをクライアントに納得させることです。ダウンロード可能なソケットファクトリ (JDK 1.2) を使うと効率的にこれを行うことができます。ソケットファクトリを使わない場合は、java.rmi.server.hostname
プロパティを設定することにより、ブリッジホストに名前を付けてポート番号を同じにできます。
外部からプロキシに呼び出しがあると、プロキシは直ちにその呼び出しを内部サーバ上の元のオブジェクトに転送します。プロキシの使い方は外部から見えますが、通信時に元の参照またはプロキシ参照のどちらを渡すかを決定する内部サーバからは見えません。
言うまでもなく、これには大量の設定とローカルネットワーク管理者間の協力が必要です。
それでも、多重接続を「だまし」て、ある特定のファイアウォール規則 (一方向の接続を許可するが、反対方向の接続を許可しないファイアウォール) を回避することはできます。
その方法とは RMI が TCP ポートで待機するのを防ぐことで、これは多重化メカニズムをトリガすることによって行います。これを行う唯一の方法は、checkListen
内で SecurityException
を発行するセキュリティマネージャをインストールすることです。
これを行うことが可能なら、そのかわりにソケットファクトリをインストールすることも可能でしょう。
最悪の場合は、クライアント側のファイアウォールが直接の TCP 接続を一切許可せず、そのファイアウォール内のクライアントが「Web サーフィン」するための HTTP プロキシサーバだけを持っているケースです。この場合、サーバホストは、HTTP 要求に埋め込まれた RMI 要求を含むポート 80 での接続を受け取ります。HTTP サーバで java-rmi.cgi
プログラムを使用するか、 RMI サーバをポート 80 で直接実行できます。どちらの方法でも、サーバはクライアントがエクスポートしたコールバックオブジェクトを使用することはできません。
それより良いケースに、クライアントがサーバへの直接接続を作成できるがサーバからの接続は受け取れないという場合があります。この場合も、コールバックオブジェクトは普通には使用できません。これは特殊なケースで、ある環境下でアプレットが JDK1.0.2 との互換性の目的で「多重化接続モード」に入ります。これにより、コールバックは可能になりますが、スケーラブルではないか、サポートされません。
クライアントファイアウォールの管理者から協力を得られない場合、最も保守的な方法は、次のとおりです。
java-rmi.cgi
スクリプトを使ってポート 80 に置く
DeleGate
など) をポート 80 に置く。これは、接続を受け付けるとすぐに実際のサーバポートに接続し、バイトデータの受け渡しを行います。これは getClientHost()
が誤った情報を返す原因になるので、別のホスト上にない限り、このメソッドを使ってレジストリを利用可能にしないでください。
java.rmi.server.Unreferenced
インタフェースを (その他の必要なインタフェースに加えて) 実装する必要があります。RMI はすべての接続が切り離されたとき、unreferenced
メソッドを呼び出して通知することができます。unreferenced
メソッドの実装により、リモートオブジェクトが通知を受け取る方法が決められます。ただし、レジストリに参照がある場合は、Unreferenced.unreferenced
メソッドは呼び出されません。
OutOfMemoryError
を発行して) 失敗します。
Java API ではコレクションの時期を指定しませんが、JDK 1.1 実装でリモートオブジェクトのコレクションが無期限に遅れるようにみえるのには特別な理由があります。RMI ランタイムは内部的に、エクスポートされたリモートオブジェクトへの「弱参照」をテーブルに保持しています (オブジェクトへのローカルとリモートの参照を追跡するため)。JDK 1.1 で利用可能な弱参照の機構では、VM は攻撃的でないキャッシングコレクションポリシー (ブラウザに最適) を使います。そのため、「弱く参照されている」オブジェクトは、ローカル GC が次のメモリ割り当てで、そのメモリが本当に必要だと判断するまではコレクションされません。これはアイドル状態のサーバには決して起こりませんが、メモリが必要な場合には参照されていないサーバオブジェクトがコレクションされます。
JDK 1.2 リリースには RMI が使用する新しいインフラストラクチャが含まれ、この問題が発生する状況を大幅に減らすことができます (現在、JDK 1.2 Beta4 で使用可能)。
System.exit
を使うのは賢明でしょうか。
System.exit()
が呼び出されると RMI ランタイムはサーバに適切な「参照なし」のメッセージを送ることができないので、これを呼び出したクライアントは異常終了とみなされます。終了の前にクライアントで System.runFinalizersOnExit
を実行するだけでは不十分です。その理由は、ファイナライザでは必要な処理がすべて行われるわけではなく、「参照なし」のメッセージがサーバに送られないからです (runFinalizersOnExit の使用は一般的に推奨できず、デッドロックを起こしやすい)。
クライアント VM を終了するために System.exit()
を使う必要がある場合は、VM 内に保持されているリモート参照がより早く確実に消去されるように、アクセス可能なリモート参照がすでに存在しないことを確認し (実行中のスレッドからアクセスできないローカルリファレンスをすべて明示的に null にする)、そのあと次のようにしてから終了してください。
System.gc(); System.runFinalization();
unreferenced()
メソッドが呼び出されます (レジストリはすべてのバインディングへの参照を保持しているため、レジストリもまた、この目的ではクライアントとなる)。
クライアントがリモート参照を保持している場合は、その参照のリースも保持しており、それを更新する (サーバにコンタクトして dirty()
呼び出しを行うことにより) 必要があります。エクスポートされたオブジェクトに対する最後のリースが期限切れになるか閉じられるかすると、そのオブジェクトは参照されていないとみなされ、java.rmi.Unreferenced
を実装している場合は unreferenced()
メソッドが呼び出されます。
複数のクライアントが同一のリモートオブジェクトへの参照を持っている場合は、そのオブジェクトに対するすべてのクライアントのリースが期限切れにならない限り、unreferenced()
メソッドは呼び出されません。したがって、この手法を使って個々のクライアントを追跡する場合は、個々のクライアントが Unreferenced
オブジェクトに対する参照を保持している必要があります。
unreferenced()
メソッドが呼び出されるまで 10 分かかります。この時間を短縮する方法を教えてください。
java.rmi.dgc.leaseValue
を使ってミリ単位で指定されます。この時間を短く (例: 30 秒) するには、次のようにしてサーバを起動します。java -Djava.rmi.dgc.leaseValue=30000 ServerMain
デフォルト値は 600000 ミリ秒 (10 分) です。
クライアントは期限の半分を過ぎると各自のリースを更新します。リース期間が短すぎると、クライアントは無駄なリース更新のためにネットワーク帯域幅を浪費することになります。リース期間が極端に短い場合にはクライアントのリース更新が間に合わなくなり、結果としてエクスポートされたオブジェクトが削除されることもあります。
RMI の将来のリリースでは、リースの更新が失敗するとリモート参照が無効になります (参照の完全性を維持するため)。失効したリモートオブジェクトへの参照の使用をあてにすべきではありません。
クライアントマシンがクラッシュした場合は、単にタイムアウトを待つだけで良いのです。接続が切れたときにクライアントがまだ何らかの制御を維持している場合は、クライアントはすばやく DGC clean 呼び出しを行い、Unreferenced
をタイムリーに使用できます。クライアントがリモートオブジェクトに対して保持している可能性のある参照をすべて null にしてから、System.gc()
を呼び出すことにより、この処理を促進することができます (1.1.x では、ファイナライザを同期して実行させてから、もう一度 GC を実行することが必要)。
クラッシュしたクライアントがあとで再起動してサーバにコンタクトすれば、その時にサーバはクライアントがそれまでの間にクラッシュしたかどうかを推察することができます。クライアントとサーバが対話している間、両者の間で TCP 接続がずっと開いているなら、あとでその接続への書き込み (1 時間ごとの TCP 維持パケットが有効な場合は、それを含む)が失敗したときに、サーバはクライアントが再起動したことを検出できます。しかし、そのような永続的な接続はスケーラビリティを損なう上にあまり役に立たないので、RMI は永続的な接続を必要としないように設計されています。
ネットワークピアがいつクラッシュしたり利用できなくなったかを簡単に判断することは、まったく不可能なので、ピアが応答しなくなったときのアプリケーションの動作を決めておく必要があります。
この作業に使う主な手段はタイムアウトとリセットです。タイムアウトが起きた場合、ピアが通信不可能になったと判断してもかまいません。しかし、そのピアがこちらへ通信しようとするのをやめるように、タイムアウトしたことがピアに分かる必要があります。リースのメカニズムは、これを半自動的に行うためのものです。
リセットとは、ピアのために保持されている状態を一掃することです。たとえば、クライアントはサーバに最初に登録したときにリセットを行なって、サーバがそのクライアントのためにそれまで保持していた状態を破棄するようにします (クライアントがそれ以前の「死んだ」セッションの記憶を持たずに再起動したと考えて)。
多くの場合、目的は、サーバでクライアントの明確なリストを持ち、誤りや失敗なしにそのリストを最新の状態に維持することです。ネットワークシステムでは故障や遅延はいつでも起こる可能性があるので、リストにはある程度の誤りがあることを予期する必要があります。リースなどのメカニズムを使ってタイムアウトを強制すれば、リソースの漏れの問題は解決します。失効したデータの問題がもっと深刻で、正常な動作を妨害するような場合があります。問題を取り除かなければ悪影響がある場合には、明示的に取り除く必要があります。
たとえば、ユーザが編集するため、あるビジネスオブジェクトがロックされているときにセッションが異常終了した場合には、何らかの方法でロックを解除する必要があります。この場合、ロックにはタイムアウトが必要かもしれません。しかし、すぐに同一ユーザがログインし、そのユーザはタイムアウトまで待つ必要はないと思っているなら、新しいセッションではロックを引き継ぐか、ユーザがロックを保持していないと断定する (サーバが安全にロックを解除できるようにする) 必要があります。
rmic
コマンドを実行させるにはどうすれば良いでしょうか。
call
コマンドを挿入する必要があります。たとえば、次のようにします。
call rmic ClientHandler call rmic Server call rmic ServerHandler call rmic Client
java.rmi.RemoteServer.getClientHost
メソッドが、現在のスレッド上の現在の呼び出し元のクライアントホスト名を返します。
そのため、リモートオブジェクト参照をサーバからクライアントへ渡してから、サーバに送り返し、元の実装クラスにキャストバックすることはできません。ただし、サーバ上のリモートオブジェクト参照を使ってそのオブジェクトへのリモート呼び出しを行うことはできます。
もう一度実装クラスを見つける必要がある場合は、リモート参照を実装クラスにマッピングするテーブルを保持する必要があります。
HotJava ブラウザは JDK 1.1 に完全に準拠しているので、RMI をサポートしています。
java.util.Observable
と java.util.Observer
を新しいインタフェースで「ラップ」することができます (それぞれを RemoteObservable と RemoteObserver と呼ぶ)。これらの新しいインタフェースで、各メソッドが java.rmi.RemoteException
をスローするようにします。次に、リモートオブジェクトでこれらのインタフェースを実装します。
リモートでないラップされたオブジェクトは java.rmi.server.UnicastRemoteObject
を継承していないので、そのオブジェクトを UnicastRemoteObject
の exportObject
メソッドを使って明示的にエクスポートする必要があります。ただし、これを行うと、equals
、hashCode
、toString
の各メソッドの java.rmi.server.RemoteObject
実装を失います。
rmiregistry
への接続が作成されます。一般に、リモート呼び出しのための新しい接続は、作成される場合と作成されない場合があります。接続は、将来の利用に備えて、RMI トランスポートによってキャッシュされます。そのため、あるリモート呼び出しの正しい呼び出し先への接続が空いているときは、その接続が使用されます。接続は RMI トランスポートのレベルで管理されているので、クライアントがサーバへの接続を明示的に閉じることはできません。接続は、一定期間使われないとタイムアウトになります。
select()
呼び出しでブロック化ではなくポーリングしているようなのですが、レジストリはポーリングで実装されているのですか。
rmic
コンパイラなどのツールには費用はかかりません。
LocateRegistry.getRegistry(String host)
メソッドは、ホスト上のレジストリに直接アクセスするのではなく、レジストリが存在するかどうかを確認するためにホストをルックアップするだけです。したがって、このメソッドが正常に終了しても、必ずしも指定されたホストでレジストリが実行中であるとは限りません。その時点でレジストリにアクセス可能なスタブが返されるだけです。
メーリングリスト rmi-users@java.sun.com では、RMI とオブジェクト直列化のどちらのユーザも他のユーザと様々な問題やヒントについて話し合うことができます。メーリングリストの購読を申し込むには、
subscribe RMI-USERSと書いたメールを listserv@java.sun.com にお送りください。購読を中止するには、
unsubscribe RMI-USERSと書いたメールをお送りください。
java.io.Serializable
インタフェースを実装すること」という条件は安易に決定されたわけではありません。予測可能で安全な機構を提供するため、設計には開発者からの要求とシステムの要求のバランスをとることが求められました。設計上のもっとも困難な制約は、Java プログラミング言語のクラスの安全性とセキュリティでした。
「クラスが直列化可能と明示されなければならない」という設計にすると、開発者が (その条件を忘れたり、無視するなどの理由で) クラスを Serializable
と宣言しなかった場合には、そのクラスが RMI で使用できなくなったり持続性を持たせるという目的に使用できなくなる恐れがあります。この条件によって、開発者に「あるクラスが将来、他人によってどのように使われるか」という本質的に予知不可能なことを考えなければならないという負担をかける恐れもあります。実際、予備設計では アルファ API に反映されているように、「デフォルトでは、クラス内のオブジェクトは直列化可能とする」と結論しました。その設計を変更したのは、セキュリティと正確さを考慮した結果、デフォルトではオブジェクトは直列化可能であってはならないと確信したからです。
オブジェクトを直列化可能にすると、このような制限を与えることはできなくなります。オブジェクトの直列化の結果であるバイトストリームは、そのストリームへアクセスできるどんなオブジェクトによっても読み出しと修正が可能です。これにより、直列化されたオブジェクトの状態にはどんなオブジェクトからもアクセスすることができるので、Java のユーザが期待する機密性の保証が破られます。さらに、ストリーム内のバイトを任意に変更できるので、Java プラットフォームによる保護下では不可能だった、オブジェクトの再構築が可能になります。このようなオブジェクト再構成により、Java プラットフォームのユーザが期待する機密性保証だけでなく、プラットフォームそのものの完全性が脅かされる場合があります。
このような破壊行為を防ぐことはできません。その理由は、直列化の概念そのものが、オブジェクトを Java プラットフォームの外部 (つまり、Java 環境の機密性と安全性の保証の圏外) に持ち出すことのできる形に変換し、そのあとで Java プラットフォームに戻すことを可能にするためのものだからです。オブジェクトを直列化可能に宣言するという条件は、「クラスの設計者は、機密面や安全面でのそのような侵害の可能性を許容するという積極的な決断を行う必要がある」ことを意味します。直列化に関する知識のない開発者が、知識の欠如のせいで無防備になってはなりません。また、クラスを Serializable と宣言する場合は、その宣言の結果をよく考慮することが望ましいのです。
この種のセキュリティ問題は、セキュリティマネージャの機構によって解決できる問題ではありません。直列化の目的は、 1 つの Virtual Machine から他の Virtual Machine へのオブジェクトの移送 (RMI の場合のような空間上の移送、または、ファイルにストリームを保存する場合のような時間上の移送) を可能にすることです。したがって、セキュリティに使用される機構は特定の Java Virtual Machine の実行時環境とは無関係であることが必要です。私たちは、可能な限り「ある Virtual Machine でオブジェクトを直列化でき、別の Virtual Machine ではそのオブジェクトを直列化解除できない」という問題を避けようとしました。セキュリティマネージャは実行時環境の一部なので、もし直列化にセキュリティマネージャを使用すれば、この要求を達成できなかったでしょう。
Serializable
インタフェースのサポートの宣言を要求することにより、そのクラスの直列化のプロセスについての考慮を促したいと考えました。
実例は数多くあります。多くのクラスは、特定のオブジェクトが存在する実行時環境でだけ意味を持つ情報 (ファイルハンドル、オープンソケット接続、セキュリティ情報など) を処理します。このようなデータは、フィールドを transient
と宣言するだけで簡単に取り扱うことができますが、このような宣言が必要なのはオブジェクトが直列化される予定のときだけです。不慣れなプログラマの場合は、フィールドを transient
と宣言するのを怠るかも知れず、同様にクラスが Serializable
インタフェースを実装していることを宣言するのを怠るかもしれません。こうしたケースが不正な動作につながらないようにする必要があります。そのための方法が、Serializable
インタフェースの実装が宣言されていないオブジェクトは、直列化しないことなのです。
別の例として、多数のオブジェクトにまたがるグラフのルートとなっている「単純な」オブジェクトのことを考えてみましょう。直列化はグラフ全体に機能するので、このオブジェクトを直列化すると、他の多くのオブジェクトを直列化することになります。このような直列化は意識的に決定すべきことであり、デフォルトの動作で引き起こされるべきことではありません。
基本 Java API クラスライブラリの検討作業中に、私たちはこの問題を考慮する必要性を痛感しました。当初はライブラリのシステムクラスが適宜、直列化可能と宣言されているという設計でした。私たちは当初、この設計を実現するのは簡単だと考えました。「ほとんどのシステムクラスに Serializable
インタフェースの実装を宣言できるのだから、これをデフォルトの実装にすれば他に何も変更しなくてもそのまま使用できるだろう」と考えたのです。しかし、ほとんどの場合、予想通りにはなりませんでした。非常に多くのクラスで、フィールドを transient
と宣言すべきかどうか、またそもそもクラスを直列化可能にすることに意味があるのかどうかについて慎重に考える必要がありました。
もちろん、プログラマやクラスの設計者がクラスを直列化可能と宣言するときに、実際にこの問題について考えるという保証はありません。しかし、クラスに Serializable
インタフェースの実装を宣言することを要求することにより、プログラマにはなんらかの考慮を払うことが求められます。直列化をオブジェクトのデフォルト状態とすると、考慮の不足によって、プログラムになんらかの悪影響 (Java プラットフォーム全体の設計で防ごうとしたもの) を及ぼす可能性があります。
対処法として、まずトップレベルのウィジェットをコンテナから削除してください (これで、ウィジェットは「アクティブ」ではなくなる)。この時点でピアは破棄され、AWT ウィジェットの状態だけを保存できます。あとでウィジェットを直列化解除して読み出すとき、フレームにトップレベルのウィジェットを追加して、AWT ウィジェットを表示させます。show
呼び出しを追加することが必要な場合があります。
JDK 1.1 以降の AWT ウィジェットは直列化可能です。java.awt.Component
クラスは Serializable
インタフェースを実装します。
RMI で直列化を使用する場合は、暗号化と暗号解読を下位のネットワークトランスポートに委ねます。安全なチャネルが必要な場合は、ネットワーク接続には SSL のようなものを使用することをお勧めします (「RMI と SSL について」を参照)。
ByteArrayInputStream
および ByteArrayOutputStream
オブジェクトを仲介する方法が利用できます。ByteArrayInputStream
および ByteArrayOutputStream
オブジェクトを使用してランダムアクセスファイルの読み書きを行い、バイトストリームから ObjectInputStream
および ObjectOutputStream
を作成してオブジェクトの転送を行います。バイトストリームにオブジェクト全体が入るように注意してください。そうしないと、オブジェクトへの読み書きに失敗します。
たとえば、java.io.ByteArrayOutputStream
を使って ObjectOutputStream
のバイトを受け取ります。バイト配列の形式で結果を 1 つ受け取ります。次に、このバイト配列を ByteArrayInputStream
で ObjectInput
ストリームへの入力として使用します。
ObjectOutputStream
には渡されません。ただし、そのオブジェクトのクラスがまだローカルで利用可能になっていない場合は、そのクラスを受信側でロードする必要があります。クラスファイルそのものではなく、クラス名だけが直列化されます。直列化を解除するときには、すべてのクラスは通常のクラスロード機構を使ってロード可能でなければなりません。アプレットの場合は AppletClassLoader
でロードされます。
ローカルオブジェクトがリモート VM に渡されるときは、オブジェクトの内容がコピーされて渡される (真の値渡し) ので、一貫性は保証されません。
ObjectOutputStream
から ObjectInputStream
を作成するには、どうすれば良いでしょうか。ObjectOutputStream
と ObjectInputStream
は、どんなストリームオブジェクトに対しても機能します。ByteArrayOutputStream
を使用し、配列を取得して ByteArrayInputStream
に挿入することもできます。同様に piped stream クラスも使用できます。OutputStream
クラスと InputStream
クラスを継承するすべての java.io クラスを使用できます。
writeObject
メソッドを使ってネット上を送信し、readObject
メソッドを使って受信します。次に、オブジェクトのフィールドの値を変更してから同様に送信すると、readObject
メソッドから返されるオブジェクトは最初のものと同じで、フィールドの新しい値が反映されていないようです。これは正しい動作なのですか。ObjectOutputStream
クラスは直列化した各オブジェクトを追跡し、それ以降そのオブジェクトがストリームに書き込まれる時は、ハンドルだけを送ります。これは、このクラスがオブジェクトのグラフを扱う方法です。対応する ObjectInputStream
は、生成したすべてのオブジェクトとハンドルを追跡し、もう一度ハンドルが参照されたときに、同じオブジェクトを返せるようにします。出力ストリームと入力ストリームは、どちらも開放されるまでこの状態を維持します。
また、ObjectOutputStream
クラスは reset
メソッドを実装しています。このメソッドを使うとオブジェクトを送信したという記録が破棄されるので、オブジェクトをもう一度送るとコピーが作成されます。
スレッドの直列化が難しいのは、スレッドは Virtual Machine に複雑に結び付いた多くの状態を持っているために、別の場所にコンテキストを再確立することが困難または不可能だからです。たとえば、VM 呼び出しスタックを保存するだけでは不十分です。その理由は、多くのネイティブメソッドが C のプロシージャを呼び出し、そのプロシージャが Java プラットフォームのコードを呼び出している場合、Java プログラミング言語の構造と C のポインタが複雑に混合したものに対処する必要があるからです。また、スタックを直列化することは、任意のスタック変数からアクセス可能なすべてのオブジェクトを直列化することを意味します。
同じ VM 内でスレッドが再開されると、そのスレッドは多くの状態を元のスレッドと共有します。両方のスレッドが同時に実行されると、ちょうど 2 つの C のスレッドがスタックを共有しようとした場合のように、予測不可能な動作を起こします。また、別の VM で直列化解除した場合の結果は予想できません。
ObjectOutputStream
を作成する必要があります。
ObjectOutputStream
は OutputStream
を生成します。zip オブジェクトが OutputStream
クラスを継承していれば、圧縮しても問題ありません。
オブジェクトのツリーの直列化の例を示します。
import java.io.*; class tree implements java.io.Serializable { public tree left; public tree right; public int id; public int level; private static int count = 0; public tree(int depth) { id = count++; level = depth; if (depth > 0) { left = new tree(depth-1); right = new tree(depth-1); } } public void print(int levels) { for (int i = 0; i < level; i++) System.out.print(" "); System.out.println("node " + id); if (level <= levels && left != null) left.print(levels); if (level <= levels && right != null) right.print(levels); } public static void main (String argv[]) { try { /* Create a file to write the serialized tree to. */ FileOutputStream ostream = new FileOutputStream("tree.tmp"); /* Create the output stream */ ObjectOutputStream p = new ObjectOutputStream(ostream); /* Create a tree with three levels. */ tree base = new tree(3); p.writeObject(base); // Write the tree to the stream. p.flush(); ostream.close(); // close the file. /* Open the file and set to read objects from it. */ FileInputStream istream = new FileInputStream("tree.tmp"); ObjectInputStream q = new ObjectInputStream(istream); /* Read a tree object, and all the subtrees */ tree new_tree = (tree)q.readObject(); new_tree.print(3); // Print out the top 3 levels of the tree } catch (Exception ex) { ex.printStackTrace(); } } }
Serializable
を実装せず、そのサブクラス B が Serializable
を実装している場合、クラス B を直列化したとき、クラス A のフィールドは直列化されますか。Serializable
オブジェクトのフィールドだけが書き出されて復元されます。オブジェクトは引数のないコンストラクタを持っている場合にだけ復元されます。このコンストラクタは直列化可能でないスーパータイプのフィールドを初期化します。サブクラスがスーパークラスの状態にアクセスする場合は、その状態の保存と復元用に writeObject
と readObject
を実装できます。
RMI 開発者のメーリングリスト: RMI-USERS
購読の申し込み (「subscribe rmi-users」と記したメールをお送りください):
テクニカルサポート情報:
このサイトに関する質問やコメントの送付先: webmaster@jse.east.sun.com
|