Javaネットワークとプロキシ

1)はじめに

現在のネットワーク環境、特に企業のネットワーク環境では、アプリケーション開発者はシステム管理者と同じほど頻繁にプロキシを操作する必要があります。アプリケーションがシステムのデフォルト設定を使用すべき場合もあれば、プロキシを経由する情報やどのプロキシを経由するかを厳密に制御したい場合もあります。また、大半のアプリケーションは、大半のブラウザと同様に、ある時点でプロキシ設定用のGUIを提供してユーザーに決定を任せます。

どちらの場合でも、Javaのような開発プラットフォームは、これらのプロキシに対応する強力かつ柔軟なメカニズムを提供する必要があります。残念なことに、この分野におけるJavaプラットフォームの柔軟性は、最近までそれほど高いものではありませんでした。しかし、この短所を克服する新しいAPIがJava SE 5.0で導入されたことにより、状況は大きく変化しました。このドキュメントでは、引き続き有効な古いものや新しいものも含めて、そういったすべてのAPIとメカニズムについて詳しく説明します。

2)システム・プロパティ

Java SE 1.4まで、システム・プロパティはJavaネットワークAPI内部でプロトコル・ハンドラ用のプロキシ・サーバーを設定する唯一の方法でした。しかも、これらプロパティの名前がリリースごとに変化してきたこと、および一部のプロパティは互換性を維持する目的でサポートされていても旧式になっているために、事態はさらに複雑になっています。

システム・プロパティを使用する上での主な制限事項は、切替えが「全部かゼロか」になっていることです。このため、特定のプロトコル用にプロキシが設定されると、そのプロトコルに対応したすべての接続が影響を受けます。これは、VM全体にわたる動作です。

システム・プロパティの設定方法は、次の2つに大別できます。

ここで、プロキシの設定に使用できるプロパティをプロトコルごとに見てみることにしましょう。すべてのプロキシは、ホスト名とポート番号で定義されます。後者はオプションです。指定しない場合は、標準のデフォルト・ポートが使用されます。

2.1) HTTP

HTTPプロトコル・ハンドラが使用するプロキシを指定するため、次の3つのプロパティを設定できます。

GetURLクラスのmainメソッドの実行を試みている場合の例をいくつか見てみましょう。

$ java -Dhttp.proxyHost=webcache.example.com GetURL

すべてのHTTP接続は、ポート80上で待機しているwebcache.example.comのプロキシ・サーバーを経由して接続されます(ポートの指定は行なっていないため、デフォルト・ポートが使用される)。

$ java -Dhttp.proxyHost=webcache.example.com -Dhttp.proxyPort=8080
-Dhttp.nonProxyHosts=”localhost|host.example.com” GetURL

2番目の例でも、プロキシ・サーバーは引き続きwebcache.example.com上に存在しますが、待機するポートが8080になります。また、localhostまたはhost.example.comへの接続時には、プロキシは使用されません。

すでに説明したように、これらのオプションを使って呼び出されたVMの寿命全体で、すべてのHTTP接続がこれらの設定の影響を受けます。ただし、System.setProperty()メソッドを使用すれば、動作を若干動的にできます。

次のコードにその方法を示します。

//Set the http proxy to webcache.example.com:8080

System.setProperty("http.proxyHost", "webcache.example.com");
System.setProperty("http.proxyPort", "8080");

// Next connection will be through proxy.
URL url = new URL("http://java.example.org/");
InputStream in = url.openStream();

// Now, let's 'unset' the proxy.
System.clearProperty("http.proxyHost");

// From now on http connections will be done directly.

これは、ほぼ良好に動作しますが、アプリケーションがマルチスレッドに対応している場合は扱いにくくなることがあります。システム・プロパティは「VM全体」の設定であるため、すべてのスレッドが影響を受けることに留意してください。このため、あるスレッド内のコードの副作用として、その他のスレッド内のコードが動作不能になる可能性があります。

2.2) HTTPS

HTTPS (HTTP over SSL)プロトコル・ハンドラは、独自のプロパティ・セットを保持します。

これらの動作は対応するHTTPの動作と正確に同じであることが推測できるため、詳細には触れません。ただし、この場合、デフォルトのポート番号は443になり、HTTPSプロトコル・ハンドラが使用する「非プロキシ・ホスト」リストのデフォルトのポート番号は、HTTPハンドラ(http.nonProxyHosts)と同じになります。

2.3) FTP

FTPプロトコル・ハンドラの設定は、HTTPの場合と同じ規則に従います。唯一の相違点は、各プロパティ名の接頭辞が「http.」ではなく「ftp.」になることです。

このため、システム・プロパティは次のようになります。

ここで、「非プロキシ・ホスト」リストに対して別個のプロパティが存在することに留意してください。また、HTTPの場合と同様、デフォルトのポート番号値は80になります。プロキシを経由するとき、FTPプロトコル・ハンドラは実際にはHTTPを使用してプロキシ・サーバーにコマンドを発行することに留意してください。デフォルトのポート番号が同じなのは、このためです。

簡潔な例を見てみることにしましょう。

$ java -Dhttp.proxyHost=webcache.example.com
-Dhttp.proxyPort=8080 -Dftp.proxyHost=webcache.example.com -Dftp.proxyPort=8080 GetURL

この例では、HTTPとFTPの両方のプロトコル・ハンドラが、webcache.example.com:8080にある同じプロキシ・サーバーを使用します。

2.4) SOCKS

RFC 1928に定義されているように、SOCKSプロトコルは、クライアント・サーバー・アプリケーションがTCPとUDPの両方のレベルでファイアウォールを安全にトラバースするためのフレームワークを提供します。この点では、このプロトコルは、より高レベルのプロキシ(HTTPまたはFTP固有のプロキシ)よりも高い汎用性を持ちます。Java SE 5.0は、クライアントTCPソケット用のSOCKSをサポートします。

SOCKS関連のシステム・プロパティには、次の2つがあります。

ここでは、接頭辞のあとにドット(「.」)を付けないことに留意してください。これには、歴史的な理由とともに、下位互換性を維持する目的があります。この方法でSOCKSプロキシが指定されると、すべてのTCP接続がこのプロキシを介して試みられます。

例:

$ java -DsocksProxyHost=socks.example.com GetURL

この場合、コードの実行中に、外向きのTCPソケットが、socks.example.com:1080でSOCKSプロキシ・サーバーを経由します。

では、SOCKSプロキシとHTTPプロキシの両方が定義されている場合はどうなるでしょうか。その場合は、HTTPやFTPなど、より高レベルのプロトコルの設定がSOCKS設定に優先されます。このため、この場合は、HTTP接続が確立されるとSOCKSプロキシの設定は無視され、HTTPプロキシへの連絡が行われます。例を見てみましょう。

$ java -Dhttp.proxyHost=webcache.example.com -Dhttp.proxyPort=8080
-DsocksProxyHost=socks.example.com GetURL

ここでは、HTTP設定が優先されるため、HTTP URLはwebcache.example.com:8080を経由します。では、FTP URLについてはどうでしょうか。FTPには特定のプロキシ設定が割り当てられておらず、FTPはTCPの最上位に位置するため、SOCKSプロキシ・サーバー(socks.example.com:1080)経由でFTP接続が試みられます。FTPプロキシが指定されている場合は、そのプロキシが代わりに使用されます。

3) Proxyクラス

すでに説明したとおり、システム・プロパティは強力ですが、柔軟性に欠けています。その「全部かゼロか」という動作は、大半の開発者には厳しすぎる制限に感じられます。このため、Java SE 5.0では、プロキシ設定に基づく接続を可能にする、より柔軟性の高い新規APIの導入が決定されました。

この新規APIのコアは、プロキシ定義を表すProxyクラスです。通常、プロキシ定義にはタイプ(HTTP、SOCKS)およびソケット・アドレスが含まれます。Java SE 5.0では、次の3つのタイプを指定可能です。

このため、HTTPプロキシ・オブジェクトを作成するには、次の呼出しを実行します。

SocketAddress addr = new
InetSocketAddress("webcache.example.com", 8080);
Proxy proxy = new Proxy(Proxy.Type.HTTP, addr);

この新規プロキシ・オブジェクトはプロキシ定義を表すだけで、それ以上の意味はありません。このオブジェクトをどのように使用したらよいでしょうか。URLクラスに追加された新しいopenConnection()メソッドで、引数としてProxyを指定します。この場合の動作は、指定したプロキシ経由で接続を強制的に確立し、前述のシステム・プロパティを含む、その他の設定すべてを無視することを除き、引数を指定せずにopenConnection()を実行した場合と同じです。

このため、次のコードを追加して前述の例を完了させることができます。

URL url = new URL("http://java.example.org/");
URLConnection conn = url.openConnection(proxy);

使い方は、このように簡単です。

イントラネット上のURLなど、特定のURLへの直接接続を指定する場合にも、同じメカニズムを使用できます。このような場合に、DIRECTタイプが役立ちます。ただし、DIRECTタイプでは、プロキシ・インスタンスを作成する必要はありません。静的メンバーNO_PROXYを使用するだけです。

URL url2 = new URL("http://infos.example.com/");
URLConnection conn2 = url2.openConnection(Proxy.NO_PROXY);

これにより、その他のプロキシ設定を無視して、特定のURLを直接接続で確実に取得できます。これは、便利な方法です。

URLConnectionにSOCKSプロキシを強制的に経由させることもできます。

SocketAddress addr = new InetSocketAddress("socks.example.com", 1080);
Proxy proxy = new Proxy(Proxy.Type.SOCKS, addr);
URL url = new URL("ftp://ftp.gnu.org/README");
URLConnection conn = url.openConnection(proxy);

このFTP接続は、指定したSOCKSプロキシを介して試みられます。見ておわかりのように、これはきわめて直接的な方法です。

最後に、新たに導入されたソケット・コンストラクタを使って、個別のTCPソケット用のプロキシを指定することもできます。説明する順番は最後になりますが、これも重要な方法です。

SocketAddress addr = new InetSocketAddress("socks.example.com", 1080);
Proxy proxy = new Proxy(Proxy.Type.SOCKS, addr);
Socket socket = new Socket(proxy);
InetSocketAddress dest = new InetSocketAddress("server.example.org", 1234);
socket.connect(dest);

ここでは、ソケットは、指定したSOCKSプロキシ経由で、宛先アドレス(server.example.org:1234)への接続を試みます。

URLの場合は、グローバルな設定の影響を受けることなく、同じメカニズムを使って直接(プロキシを経由しない)接続を確実に試みることができます。

Socket socket = new Socket(Proxy.NO_PROXY);
socket.connect(new InetAddress("localhost", 1234));

Java SE 5.0では、この新規コンストラクタが受け入れるプロキシは、SOCKSまたはDIRECT (NO_PROXYインスタンス)の2種類のみです。

4) ProxySelector

これまでの説明からおわかりのように、Java SE 5.0では、開発者はプロキシを強力かつ柔軟に制御できます。それでも、プロキシ間で負荷分散を実行したり、宛先に応じてプロキシを変更したりするなど、これまで説明されたAPIでは実現が難しい状況で、使用するプロキシを動的に決定したい場面に遭遇することがあります。このような場合に、ProxySelectorが役立ちます。

簡潔に説明すると、ProxySelectorは、指定されたURLで使用すべきプロキシが存在する場合に、そのことをプロトコル・ハンドラに伝えるコードです。たとえば、次のコードを考えてみましょう。

URL url = new URL("http://java.example.org/index.html");
URLConnection conn = url.openConnection();
InputStream in = conn.getInputStream();

この時点で、HTTPプロトコル・ハンドラが呼び出され、proxySelectorへのクエリーを実行します。クエリーの内容は、次のようなものになります。

ハンドラ: やあ、java.example.orgまでたどり着きたいんだけど、プロキシを使うべきかな。
ProxySelector: どのプロトコルを使うつもりなんだい。
ハンドラ: もちろん、HTTPさ。
ProxySelector: ポートはデフォルト・ポートかい。
ハンドラ: 確認してみるからちょっと待って... そのとおり、デフォルト・ポートだ。
ProxySelector: わかった。じゃあ、プロキシにはwebcache.example.comを使って。ポートは8080でね。
ハンドラ: ありがとう。<休止>ねえ、webcache.example.com:8080が応答しないみたいなんだけど。他の方法はないの。
ProxySelector: それは困ったね。OK、じゃあwebcache2.example.comをポート8080で試してみて。
ハンドラ: 了解。今度はうまくいっているみたいだ。ありがとう。
ProxySelector: お安いご用さ。じゃあね。

もちろん、この会話はいくらか脚色してありますが、やり取りの内容は理解できるでしょう。

ProxySelectorのもっともすばらしい点は、プラグインが可能であることです。このため、デフォルトでは望みの機能を得られない場合、かわりを記述してそれをプラグインとして使用できます。

では、ProxySelectorとはどのようなものでしょうか。クラス定義を見てみることにしましょう。

public abstract class ProxySelector {
        public static ProxySelector getDefault();
        public static void setDefault(ProxySelector ps);
        public abstract List<Proxy> select(URI uri);
        public abstract void connectFailed(URI uri,
                SocketAddress sa, IOException ioe);
}

ここからわかるように、ProxySelectorはデフォルト実装の設定用と取得用の2つの静的メソッドを持つabstractクラスです。使用するプロキシを決定するため、またはプロキシへの到達が不可能のようであることを通知するため、プロトコル・ハンドラにより2つのインスタンス・メソッドが使用されます。独自のProxySelectorを提供する場合に実行すべきことは、このクラスを拡張し、これら2つのインスタンス・メソッドの実装を提供してから、新規クラスのインスタンスを引数として渡してProxySelector.setDefault()を呼び出すことです。この時点で、プロトコル・ハンドラは、HTTPやFTPと同様に、新規ProxySelectorへのクエリーを実行して使用するプロキシを決定します。

この種のProxySelectorの記述方法の詳細を説明する前に、デフォルトのProxySelectorについて説明しましょう。Java SE 5.0の提供するデフォルト実装では、下位互換性が維持されています。つまり、デフォルトのProxySelectorは、前述のシステム・プロパティをチェックして使用するプロキシを決定します。ただし、新しいオプション機能も存在します。最近のWindowsシステムやGnome 2.xプラットフォームでは、デフォルトのProxySelectorに対してシステムのプロキシ設定の使用を指示できます(最近のバージョンのWindowsおよびGnome 2.xでは、ユーザー・インタフェースからプロキシをグローバルに設定することが可能)。システム・プロパティjava.net.useSystemProxiesがtrueに設定されている場合(デフォルトでは互換性維持の目的でfalseに設定されている)、デフォルトのProxySelectorはこれらの設定の使用を試みます。このシステム・プロパティは、コマンド行で設定することも、JREインストール・ファイルlib/net.propertiesを編集して設定することもできます。このような方法で、所定のシステムでシステム・プロパティを一度だけ変更する必要があります。

それでは、新規ProxySelectorを記述およびインストールする方法について説明しましょう。

実現したいことは、次のとおりです。デフォルトのProxySelectorの動作にはかなり満足していますが、HTTPおよびHTTPSについては変更が必要です。ネットワーク上には、これらのプロトコルに対応したプロキシが複数存在するため、アプリケーションがこれらのプロキシを順番に試みるようにしたいと考えています。つまり、最初のプロキシが応答しない場合に、2番目のプロキシの使用を試み、2番目のプロキシが応答しない場合には3番目のプロキシの使用を試みる、という具合です。さらに、あるプロキシが頻繁に失敗するようであれば、それをリストから削除して最適化を行います。

実行する作業は、java.net.ProxySelectorをサブクラス化すること、およびselect()メソッドとconnectFailed()メソッドの実装を提供することだけです。

宛先への接続を試みる前に、プロトコル・ハンドラによりselect()メソッドが呼び出されます。渡される引数は、リソース(プロトコル、ホスト、およびポート番号)が記述されたURIです。そのあと、このメソッドにより、プロキシのリストが返されます。たとえば、次にコードを示します。

URL url = new URL("http://java.example.org/index.html");
InputStream in = url.openStream();

このコードにより、次の擬似呼出しがプロトコル・ハンドラ内でトリガーされます。

List<Proxy> l = ProxySelector.getDefault().select(new URI("http://java.example.org/"));

この実装で行うべきことは、URIからのプロトコルが本当にHTTP (またはHTTPS)であるかを確認し、そのとおりであればプロキシのリストを返し、そうでなければデフォルト・プロキシに委譲することだけです。この場合、使用するプロトコルがデフォルトになるため、コンストラクタ内に以前のデフォルトへの参照を格納する必要があります。

このため、コードの先頭部分は次のようになります。

public class MyProxySelector extends ProxySelector {
        ProxySelector defsel = null;
        MyProxySelector(ProxySelector def) {
                defsel = def;
        }
        
        public java.util.List<Proxy> select(URI uri) {
                if (uri == null) {
                        throw new IllegalArgumentException("URI can't be null.");
                }
                String protocol = uri.getScheme();
                if ("http".equalsIgnoreCase(protocol) ||
                        "https".equalsIgnoreCase(protocol)) {
                        ArrayList<Proxy> l = new ArrayList<Proxy>();
                        // Populate the ArrayList with proxies
                        return l;
                }
                if (defsel != null) {
                        return defsel.select(uri);
                } else {
                        ArrayList<Proxy> l = new ArrayList<Proxy>();
                        l.add(Proxy.NO_PROXY);
                        return l;
                }
        }
}

最初に、コンストラクタが以前のデフォルト・セレクタへの参照を維持することに留意してください。次に、仕様に準拠するために、select()メソッド内の不正な引数をチェックすることに注目してください。最後に、以前のデフォルトが存在する場合、必要に応じてそれを保留することに注目します。ArrayListの生成方法については、格別興味を引くものではないため、この例では詳しく説明しません。関心がある場合には、付録にコード全体が記載されているので、それを参照してください。

見てわかるとおり、connectFailed()メソッドの実装が含まれていないため、このクラスは完全なものではありません。次の手順で、このメソッドを実装します。

select()メソッドから返されたいずれかのプロキシへの接続が失敗した場合は常に、connectFailed()メソッドがプロトコル・ハンドラにより呼び出されます。渡される引数は、ハンドラが到達を試みたURI (select()の呼出し時に使用されたもの)、ハンドラがアクセスを試みたプロキシのSocketAddress、プロキシへの接続を試みたときにスローされたIOExceptionの3つです。この情報を使用して、次の操作を実行します。このプロキシがリスト内に存在し、かつ3回以上失敗した場合は、リストから削除して以降は使用されないようにします。コードは次のようになります。

public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
        if (uri == null || sa == null || ioe == null) {
            throw new IllegalArgumentException("Arguments can't be null.");
        }
        InnerProxy p = proxies.get(sa); 
        if (p != null) {
                if (p.failed() >= 3)
                   proxies.remove(sa);
        } else {
                if (defsel != null)
                    defsel.connectFailed(uri, sa, ioe);
        }
}

とても簡単ですね。再び、引数の有効性を確認する必要があります(これも仕様に準拠するため)。ここで考慮するのはSocketAddressのみです。SocketAddressがリスト内のプロキシのいずれかであればそれを処理し、そうでなければデフォルト・セレクタとして再度保留します。

これで実装がほぼ完成しました。アプリケーション内でさらに実行することは、登録だけです。それを行いましょう。

public static void main(String[] args) {
        MyProxySelector ps = new MyProxySelector(ProxySelector.getDefault());
        ProxySelector.setDefault(ps);
        // rest of the application
}

わかりやすくするために、いくらか簡略化して記述しています。特に、例外のキャッチについてあまり記述していないことにお気づきでしょう。自分でコードを記述する際には、省略してある部分を補ってください。

基盤となるプラットフォームやコンテナ(Webブラウザなど)との統合性を改善するため、Java Plug-inとJava Webstartの両方でデフォルトのProxySelectorをカスタムのProxySelectorに置き換えていることに留意してください。このため、ProxySelectorを操作する際には、デフォルトのProxySelectorは、通常、基盤となるプラットフォームおよびJVM実装に固有のものであることを念頭に置く必要があります。このため、カスタムのものを提供する場合、前述の例で示したように以前のものへの参照を保持して、必要に応じて使用するのはよい方法です。

5)結論

ここまでの内容から、Java SE 5.0ではプロキシを操作する方法が非常に多く用意されていることがわかります。システム・プロキシ設定を使用する非常に簡単な方法から、ProxySelectorを変更する非常に柔軟性の高いもの(熟練した開発者専用)まで提供されています。Proxyクラスを接続ごとに選択することも可能です。

付録

ここでは、このドキュメントで開発してきたProxySelectorのソース全体を示します。これは、教育を目的とするものであり、簡潔性を意図して記述されていることを念頭に置いてください。

import java.net.*;
import java.util.List;
import java.util.ArrayList;
import java.util.HashMap;
import java.io.IOException;

public class MyProxySelector extends ProxySelector {
        // Keep a reference on the previous default
    ProxySelector defsel = null;
        
        /*
         * Inner class representing a Proxy and a few extra data
         */
        class InnerProxy {
        Proxy proxy;
                SocketAddress addr;
                // How many times did we fail to reach this proxy?
                int failedCount = 0;
                
                InnerProxy(InetSocketAddress a) {
                        addr = a;
                        proxy = new Proxy(Proxy.Type.HTTP, a);
                }
                
                SocketAddress address() {
                        return addr;
                }
                
                Proxy toProxy() {
                        return proxy;
                }
                
                int failed() {
                        return ++failedCount;
                }
        }
        
        /*
         * A list of proxies, indexed by their address.
         */
        HashMap<SocketAddress, InnerProxy> proxies = new HashMap<SocketAddress, InnerProxy>();

        MyProxySelector(ProxySelector def) {
          // Save the previous default
          defsel = def;
          
          // Populate the HashMap (List of proxies)
          InnerProxy i = new InnerProxy(new InetSocketAddress("webcache1.example.com", 8080));
          proxies.put(i.address(), i);
          i = new InnerProxy(new InetSocketAddress("webcache2.example.com", 8080));
          proxies.put(i.address(), i);
          i = new InnerProxy(new InetSocketAddress("webcache3.example.com", 8080));
          proxies.put(i.address(), i);
          }
          
          /*
           * This is the method that the handlers will call.
           * Returns a List of proxy.
           */
          public java.util.List<Proxy> select(URI uri) {
                // Let's stick to the specs. 
                if (uri == null) {
                        throw new IllegalArgumentException("URI can't be null.");
                }
                
                /*
                 * If it's a http (or https) URL, then we use our own
                 * list.
                 */
                String protocol = uri.getScheme();
                if ("http".equalsIgnoreCase(protocol) ||
                        "https".equalsIgnoreCase(protocol)) {
                        ArrayList<Proxy> l = new ArrayList<Proxy>();
                        for (InnerProxy p : proxies.values()) {
                          l.add(p.toProxy());
                        }
                        return l;
                }
                
                /*
                 * Not HTTP or HTTPS (could be SOCKS or FTP)
                 * defer to the default selector.
                 */
                if (defsel != null) {
                        return defsel.select(uri);
                } else {
                        ArrayList<Proxy> l = new ArrayList<Proxy>();
                        l.add(Proxy.NO_PROXY);
                        return l;
                }
        }
        
        /*
         * Method called by the handlers when it failed to connect
         * to one of the proxies returned by select().
         */
        public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
                // Let's stick to the specs again.
                if (uri == null || sa == null || ioe == null) {
                        throw new IllegalArgumentException("Arguments can't be null.");
                }
                
                /*
                 * Let's lookup for the proxy 
                 */
                InnerProxy p = proxies.get(sa); 
                        if (p != null) {
                                /*
                                 * It's one of ours, if it failed more than 3 times
                                 * let's remove it from the list.
                                 */
                                if (p.failed() >= 3)
                                        proxies.remove(sa);
                        } else {
                                /*
                                 * Not one of ours, let's delegate to the default.
                                 */
                                if (defsel != null)
                                  defsel.connectFailed(uri, sa, ioe);
                        }
     }
}

Copyright © 1993, 2020, Oracle and/or its affiliates. All rights reserved.