2013年04月26日

Java RMI ConnectException: Connection refused to host: 127.0.0.1 エラーの正しい理解と対策

Java RMI による分散オブジェクト開発の経験があれば一度は目にする・躓くエラーが Connection refused to host: 127.0.0.1 じゃないでしょうか(特にLinuxサーバー上でRMIサーバー・アプリケーションを利用する場合に)。 勿論、僕も経験して解決したクチなんですが、その時からずーっと気になっている事があったんです。 それは、この例外の発生メカニズム・対策に関する体系的な説明が見当たらないこと。

rmi connection refuse

良質かはわかりませんが、今日は Java RMI, Connection refused to host 127.0.0.1 エラーの原因と対策について全体像も含めて紐解いてみます。


RMI (Remote Method Invocation) の概要とサンプ・ルコード


RMIを使うと、リモート・サーバー上で動作するJavaプロセスと通信し、まるでローカルのJavaオブジェクトのようにリモート・オブジェクトを取り扱うことが出来きます。

Java RMI Server and Client

大規模/J2EEアプリケーションであれば分散オブジェクト・フレームワーク/ミドルウエアの一部として組み込まれ、知らないうちに利用していることも多いかもしれませんね。

今回は、echoという名前のリモート・メソッドを持ち、引数に指定された文字列(Stringオブジェクト)をほぼそのまま呼び出し元に返すRMIサーバー・アプリケーションとこれを実際に呼び出し・利用するRMIクライアントを用意して Connection refused to host: 127.0.0.1 エラーの原因と対策について考えてみます。

まずはRMIサーバー・アプリケーション。 実装はこちら(MyEchoServer.java)。

import java.net.InetAddress;
import java.rmi.AlreadyBoundException;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;

public class MyEchoServer implements Echo {

 /**
  * @param args
  */
 public static void main(String[] args) {
  MyEchoServer server = new MyEchoServer();
  try {
   System.out.println("------ Info -------");
   String hostAddress = InetAddress.getLocalHost().getHostAddress();
   System.out.println("Host Address(A)  : " + hostAddress);
System.out.println("ServerHostName(B): " + System.getProperties().getProperty( "java.rmi.server.hostname")); System.out.println("Host Name(C) : " + InetAddress.getLocalHost().getHostName()); System.out.println("Host FQDN(C) : " + InetAddress.getByName(hostAddress).getHostName()); System.out.println("LocalHostName?: " + System.getProperties().getProperty( "java.rmi.server.useLocalHostName")); System.out.println("------------------");
// start RMI server server.createAndBindRegistry("Echo", server, 1099); System.out.println("Ready!");
} catch (RemoteException e) { e.printStackTrace(); } catch (AlreadyBoundException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } } public String echo(String s) { s = "ECHO: " + s; System.out.println(s); return s; } public void createAndBindRegistry(String refName, Remote r, int port) throws RemoteException, AlreadyBoundException {
System.out.println("Exporting stub..."); // export remote method stub Remote stub = UnicastRemoteObject.exportObject(r, 0); System.out.println("Creating registry..."); // get RMI registry on port 1099 Registry registry = LocateRegistry.createRegistry(port); System.out.println("Binding registry..."); // bind stub to registry registry.bind(refName, stub); } }

mainメソッドでRMIサービス開始前に出力しているホスト名、アドレス、Javaプロパティの値はRMIの動作には直接は必要ありませんが、これからお話する例外発生のメカニズムを効率的に理解してもらう為に追加しています。

createAndBindRegistryメソッドが具体的なRMIレジストリ・サーバーの生成とEchoという名前でリモート・スタブのbind(登録)処理を行なっている部分。

implementsしているEchoインタフェースはこちら。これはRMIでリモート・メソッドを実装する際の必要なルールで今回の目的では無いので詳しい説明は割愛しますね。

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface Echo extends Remote {
 public String echo(String s) throws RemoteException;
}

RMIのサーバー、クライアント・コードの中身をもっと基礎から学びたい人はこちらをどうぞ。

Java RMI 入門

このコードをビルド、jarファイルにエクスポートしてLinuxサーバー上で起動してみましょう。僕が用意した Connection refused to host: 127.0.0.1 例外が発生するLinuxサーバー上では起動すると次のようなメッセージが出力されます。

$ java -classpath /path/to/MyServer.jar MyEchoServer
------ Info -------
Host Address(A)  : 127.0.0.1
ServerHostName(B): null
Host Name(C)     : echoserver
Host FQDN(C)     : echoserver.rmi.com
LocalHostName?: null
-------------------
Exporting stub...
Creating registry...
Binding registry...
Ready!


さあ、これでサーバー側は終わり。 クライアントは MyRmiClient.java という名前で次のような実装コードになります。

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class MyRmiClient {

 /**
  * @param args
  * 
  */
 public static void main(String[] args) {
  String host = "172.16.0.100";
  int port = 1099;
try {
Registry registry = LocateRegistry.getRegistry(host, port); Echo stub = (Echo) registry.lookup("Echo"); String s = stub.echo("Hello RMI!"); System.out.println(s);
} catch (Exception e) { e.printStackTrace(); } } }


このRMIクライアントを実行、リモート・メソッドを呼び出すと次のような例外メッセージが出力され、結果は失敗に終わります。

java.rmi.ConnectException: Connection refused to host: 127.0.0.1; nested exception is: 
  java.net.ConnectException: Connection refused
  at sun.rmi.transport.tcp.TCPEndpoint.newSocket(TCPEndpoint.java:619)
  at sun.rmi.transport.tcp.TCPChannel.createConnection(TCPChannel.java:216)
  at sun.rmi.transport.tcp.TCPChannel.newConnection(TCPChannel.java:202)
  at sun.rmi.server.UnicastRef.invoke(UnicastRef.java:128
  --- snip ---

これこれ!、このエラー・メッセージ。

初めて見ると少しパニックになるはずです。 だって、正しいRMIサーバーのIPアドレス(又はホスト名。ここでは172.16.0.100)を指定しているにも関わらず、例外メッセージには 172.16.0.100 では無く、127.0.0.1、つまりローカル・ホストから接続拒否されたと出力されるんですから。

さあ、早速この謎を解いて行きましょう。


RMI が隠蔽しているリモート・オブジェクトの取得シーケンスと名前解決の仕組み


クライアントからオブジェクトへの参照要求があるとregistry.lookupメソッドはRMIレジストリ・サーバーへの問い合わせを行います。
※今回用意したRMIサーバー・アプリケーションは自分自身でRMIレジストリ・サーバーを createRegistry(port) しています。

また、その要求を受けたRMIレジストリ・サーバーは指定された名前でRMIサーバー/アプリケーションのスタブが登録されていれば、そのオブジェクトへのリモート参照を呼び出し元 (registry.lookup) へ返し、これが最終的にクライアントへオブジェクトの参照として戻ります。

コードとの関係を図にしたのがこちら。

RMI Naming lookup overview

リモート・オブジェクトへの参照を取得したクライアント、つまり僕達プログラマはリモート・メソッド(echo)を呼び出します。

すると、あの不思議な例外(Connection refused to host: 127.0.0.1)が発生します。何故でしょうか?

実は隠蔽されたリモート・オブジェクトへの参照内部にはRMIレジストリ・サーバーによって、最終的にメソッドを実行するRMIサーバー(MyEchoServerアプリケーションが動作するホスト)のIPアドレスとポート番号が設定されているんです。

RMI Remote Object

だんだん核心に近づいてきましたね。 RMIレジストリはどうやってリモート・ホストのIPアドレスを解決・設定しているんでしょうか。

そのフローがこちら。

Java RMI 名前解決のフロー

運命は大きくA、B、Cの3つに別れます。最も一般的・標準動作になるのがホストのIPアドレスを取得する ケース A です。
 
これはコードで書くなら、次の値になります。

InetAddress.getLocalHost().getHostAddress();

今回のエラー、失敗の原因もこの結果が127.0.0.1だから。


続いて、今回のエラーの解決策として一言で説明されることが多い ケース B 。
これはJavaプロパティとして外部から強制的にRMIサーバーのIPアドレスを指定する方法です。

java -Djava.rmi.server.hostname=RMIサーバーのIPアドレス

今回のケースであれば、起動時に次のように指定します。

$ java -Djava.rmi.server.hostname=172.16.0.100 -classpath /path/to/MyServer.jar MyRmiServer


最後は同じくJavaプロパティとして指定できる ケース C 。

java.rmi.server.useLocalHostNameの値にtrueを設定すると、RMIレジストリはIPアドレスでは無く、ホストのFQDN名を参照先ホストとして設定します。

FQDN名はInetAddress.getLocalHost().getHostName()の戻り値にドット(.)が含まれればそれを、含まれない場合はホストのIPアドレス(ここではhostAddress)を使ってInetAddress.getByName(hostAddress).getHostName()として取得します。
(上記2つの方法でもFQDN名が取得できない場合には ケース A に戻る)

FQDN名の取得ロジックを簡単に表すなら次のようなコードになります。

public String getFQDN() {
String hostName = InetAddress.getLocalHost().getHostName();
if (hostName.indexOf('.') < 0) { String hostAddress = InetAddress.getHostAddress(); String name = InetAddress.getByName(hostAddress).getHostName();
if (name.indexOf('.') < 0) { hostName = hostAddress; } else { hostName = name; } } return name; }

実際にはタイムアウト処理も含まれるなどもう少し複雑な実装になっています。興味のある人は sun.rmi.transport.tcp.TCPEndpoint のコードを見て下さい。


Connection refused to host: 127.0.0.1; nested exception への正しい理解と対応


仕組みを理解・整理出来たところで、具体的な対策を考えてみましょう。 確かに僕のRMIサーバー・アプリケーションでも InetAddress.getLocalHost().getHostAddress() の結果は 127.0.0.1 になっていました。

この原因はOS(Linux)のホスト名とそれに対応するIPアドレスの対応がループバック・アドレスに設定されているから。 

この通り、ホスト名に対してpingを打つとその行き先IPアドレスは127.0.0.1になっています。

$ hostname | xargs ping -c1
PING echoserver.rmi.com (127.0.0.1) 56(84) bytes of data.
64 bytes from echoserver.rmi.com (127.0.0.1): icmp_seq=1 ttl=64 time=0.894 ms

--- echoserver.rmi.com ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.894/0.894/0.894/0.000 ms

ホスト名のアドレス解決には幾つかの手順・設定がありますが、基本になるのが/etc/hostsファイル。

エラーが起こる場合は、大抵次のような記述になっています。

$ cat /etc/hosts
# Do not remove the following line, or various programs
# that require network functionality will fail.
127.0.0.1		echoserver.rmi.com echoserver localhost.localdomain localhost
::1		localhost6.localdomain6 localhost6

もし、OS設定で問題を解決するならば、例えば次のように、ホスト名に対応するIPアドレスをクライアントに通知したいIPアドレスに変更してみましょう(解法1)。

# Do not remove the following line, or various programs
# that require network functionality will fail.
127.0.0.1		localhost.localdomain localhost
::1		localhost6.localdomain6 localhost6
172.16.0.100		echoserver.rmi.com echoserver


解法2は java.rmi.server.useLocalHostName プロパティの利用。

次のようにしてRMIサーバー・アプリケーション起動時にこのプロパティの値を true に設定します。

$ java -Djava.rmi.server.useLocalHostName=true -classpath /path/to/MyServer.jar MyEchoServer
------ Info -------
Host Address(A)  : 127.0.0.1
ServerHostName(B): null
Host Name(C)     : echoserver
Host FQDN(C)     : echoserver.rmi.com
LocalHostName?: true
-------------------
Exporting stub...
Creating registry...
Binding registry...
Ready!

これでクライアント側に渡されるリモート・オブジェクトへの参照にはRMIサーバーのIPアドレスではなくホスト名が設定され、クライアントはローカルのhostsファイル又はDNSを使ってRMIサーバーのIPアドレスを解決します。
 
この方法はクライアントがDNSを使ってRMIサーバーの名前解決が可能な場合にお勧めです。 但し、名前解決が出来なければ次のように例外の種類が変わるだけであり、問題は解決しないので注意しましょう。

java.rmi.UnknownHostException: Unknown host: echoserver.rmi.com; nested exception is: 
  java.net.UnknownHostException: echoserver.rmi.com
  at sun.rmi.transport.tcp.TCPEndpoint.newSocket(TCPEndpoint.java:616)
  at sun.rmi.transport.tcp.TCPChannel.createConnection(TCPChannel.java:216)
  at sun.rmi.transport.tcp.TCPChannel.newConnection(TCPChannel.java:202)
  at sun.rmi.server.UnicastRef.invoke(UnicastRef.java:128)
  --- snip ---


最後に java.rmi.server.hostnameプロパティを使ったRMIサーバー・アプリケーションへのIPアドレス指定(解法3)。

$ java -Djava.rmi.server.hostname=172.16.0.100 -classpath /path/to/MyServer.jar MyEchoServer
------ Info -------
Host Address(A)  : 127.0.0.1
ServerHostName(B): 172.16.0.100
Host Name(C)     : echoserver
Host FQDN(C)     : echoserver.rmi.com
LocalHostName?: null
-------------------
Exporting stub...
Creating registry...
Binding registry...
Ready!

この方法はインターネットで検索すると解決策として挙げられることが多いのですが、利用には注意が必要です。

何故でしょうか?

もし、この方法を採用するとするとアプリケーション起動スクリプト内に将来変化するかもしれないIPアドレスの情報が静的に埋め込まれ、しかも複数のスクリプトに分散配置される可能性があるからです。

もし、RMIサーバー・アプリケーションが動作するホストのIPアドレスが変わった場合、関係する全ての起動スクリプトを探し出し、それを修正しなければクライアントは存在しないIPアドレスへの接続を試み・エラーが発生してしまいます。

Connection refused to host: 127.0.0.1; nested exception 問題を解消するには3つのアプローチ(解法)がありますが、ここまで読んだ貴方は自身の利用するホスト環境・条件に合わせて3つの中から最適な解法を選択できるはずです。
 
僕のRMIクライントも無事動きましたよ。

ECHO: Hello RMI!

もっと詳しく知りたいという方は Oralceの公式RMI/FAQ をどうぞ。

それでは、より良い Remote Method Invocation ライフを!


アドバンスドJavaネットワーキング―ネットワークの基礎からソケット、RMI、CORBAまで
ディック ステフリック パラシャント スリドハルン
ピアソンエデュケーション
売り上げランキング: 1,183,921

Java分散オブジェクト入門―Java RMI、CORBA、IDL、Jini、JavaSpaces対応
中山 茂
技報堂出版
売り上げランキング: 807,582

Posted by netbuffalo at 20:30│Comments(2)TrackBack(0)Java | プログラミング


この記事へのトラックバックURL

http://trackback.blogsys.jp/livedoor/netbuffalo/4446395
この記事へのコメント
5
たいへん勉強になりました。
ありがとうございました。
Posted by tacchang at 2014年02月01日 22:05
tacchangさん、お役に立つことが出来たようで何よりです。
Posted by netbuffalo at 2014年02月03日 11:36

コメントする

名前
URL
 
  絵文字