Socket Timeoutを java.net.URLConnectionに追加する

BSDソケットAPIは、タイムアウト・オプション(SO_TIMEOUT)をサポートしており、それは、java.net.socketでサポートされています。 しかしながら残念なことに、java.net.URLConnectionは、基底のソケットクラスを公開していません。 そのため、死んでいる(すなわち、正しい形式のURLで存在しているが サイトがダウンしている)URLに接続しようとする場合には、ソケットは OSのデフォルトタイムアウトが結局使用されるでしょう(Windows NTの場合には 420秒です)。 例えばリンクを張るプログラムやURLチェックのためには、このタイムアウトは非常に長い時間です。

以下のファイルは、実際のjavaソースコードを元にURL接続にソケットタイムアウトを導入するための テクニックを説明します。 (オープンソースコミュニティライセンスを参照してください JavaSoft)。

基底クラス、というかURLConnection内部

Javaのネットワークの実装は、オブジェクト指向と同様にプロトコルとは独立しています。 したがって、実装は人が想像するかもしれないほど、単純ではありません。

URLConnection は、「ファクトリー」デザイン・パターンと同様にクライアント/サーバー・モデルを 使用している内部のいくつかのクラスに依存しています。 クライアントの基底クラスは sun.net.www.http.HttpClient です。 このクラスは、ソケットを公開する目的のために継承されています。

デフォルト・ファクトリーはURLStreamHandlerFactoryです。そして、それは HTTPプロトコルに特有であるクラス(sun.net.www.protocol.http.Handler)を インスタンス化することによって間接的にHTTPクライアントの作成を「取り扱い」ます。

実際問題として、ファクトリーは、模倣の(mimic)javaの実装に必要なだけですが、 本当に必要なのはハンドラーだけです。

派生クラス

我々は、javaソースコードで対称性を維持するために4つのクラスを導出します。

HttpURLConnectionTimeout は sun.net.www.protocol.http.HttpURLConnection を継承する
HttpTimeoutHandler は sun.net.www.protocol.http.Handler を継承する
HttpTimeoutFactory は java.net.URLStreamHandlerFactory を実装する
HttpClientTimeout は sun.net.www.http.HttpClient を継承する

ソースコードでは


HttpClientTimeout

// whatever package you want
import sun.net.www.http.HttpClient;
import java.net.*;
import java.io.*;
public class HttpClientTimeout extends HttpClient
{
    public HttpClientTimeout(URL url, String proxy, int proxyPort) throws IOException
    {
        super(url, proxy, proxyPort);
    }
	
    public HttpClientTimeout(URL url) throws IOException
    {
        super(url, null, -1);
    }
	
    public void SetTimeout(int i) throws SocketException { 
        serverSocket.setSoTimeout(i); 
    } 
	
    /* このクラスは、HTTP用に公開コンストラクタはありません。
     * このメソッドは、指定したURLのHttpClientを得るために使用されます。
     * もし現在アクティブなHttpClinetが存在している場合には、その
     * サーバ/ポートを取得します。
     *
     * no longer syncrhonized -- it slows things down too much
     * synchronize at a higher level
     */
    public static HttpClientTimeout GetNew(URL url)  
    throws IOException {
	/* see if one's already around */
	HttpClientTimeout ret = (HttpClientTimeout) kac.get(url);
	if (ret == null) {
	    ret = new HttpClientTimeout (url);  // CTOR called openServer()
	} else {
	    ret.url = url;
	}
	// don't know if we're keeping alive until we parse the headers
	// for now, keepingAlive is false
	return ret;
    }
	
    public void Close() throws IOException
    {
        serverSocket.close();
    }
	
    public Socket GetSocket()
    {
        return serverSocket;
    }
	
}

HttpTimeoutFactory

import java.net.*;

public class HttpTimeoutFactory implements URLStreamHandlerFactory
{
    int fiTimeoutVal;
    public HttpTimeoutFactory(int iT) { fiTimeoutVal = iT; }
    public URLStreamHandler createURLStreamHandler(String str)
    {
        return new HttpTimeoutHandler(fiTimeoutVal); 
    }
	
}

HttpTimeoutHandler

import java.net.*;
import java.io.IOException;

public class HttpTimeoutHandler extends sun.net.www.protocol.http.Handler
{
    int fiTimeoutVal;
    HttpURLConnectionTimeout fHUCT;
    public HttpTimeoutHandler(int iT) { fiTimeoutVal = iT; }
	
    protected java.net.URLConnection openConnection(URL u) throws IOException {
        return fHUCT = new HttpURLConnectionTimeout(u, this, fiTimeoutVal);
    }

    String GetProxy() { return proxy; }		// breaking encapsulation
    int GetProxyPort() { return proxyPort; }    // breaking encapsulation
	
    public void Close() throws Exception 
    {
        fHUCT.Close();
    }
	
    public Socket GetSocket()
    {
        return fHUCT.GetSocket();
    }
}

HttpURLConnectionTimeout

import java.net.*;
import java.io.*;
import sun.net.www.http.HttpClient;

public class HttpURLConnectionTimeout extends sun.net.www.protocol.http.HttpURLConnection
{
    int fiTimeoutVal;
    HttpTimeoutHandler fHandler;
    HttpClientTimeout fClient;

    public HttpURLConnectionTimeout(URL u, HttpTimeoutHandler handler, int iTimeout) throws IOException
    {   
        super(u, handler);
        fiTimeoutVal = iTimeout;
    }
    
    public HttpURLConnectionTimeout(URL u,  String host, int port) throws IOException
    {  
        super(u, host, port);
    }
	
    public void connect() throws IOException {
        if (connected) {
	    return;
	}
	try {
	    if ("http".equals(url.getProtocol()) /* && !failedOnce <- PRIVATE */ ) {
                // for safety's sake, as reported by KLGroup
                synchronized (url)
                {
                    http = HttpClientTimeout.GetNew(url);
                }
                fClient = (HttpClientTimeout)http;
                ((HttpClientTimeout)http).SetTimeout(fiTimeoutVal);
	    } else {
	        // make sure to construct new connection if first
	        // attempt failed
	        http = new HttpClientTimeout(url, fHandler.GetProxy(), fHandler.GetProxyPort());
	    }
	    ps = (PrintStream)http.getOutputStream();
	} catch (IOException e) {
	    throw e;
        }
    }

    /**
     * 新たに HttpClient オブジェクトを作成し、HTTPクライアント
     * オブジェクト/コネクションのキャッシュを迂回する.
     *
     * @param url	the URL being accessed
     */
    protected HttpClient getNewClient (URL url)
    throws IOException {
        HttpClientTimeout client = new HttpClientTimeout (url, (String)null, -1);
        try {
            client.SetTimeout(fiTimeoutVal);
        } catch (Exception e) {
            System.out.println("Unable to set timeout value");
        }
        return (HttpClient)client; 
    }

    /**
     * 同一ホストへのリダイレクトだけストリームのオープンを許可する.
     */
    public static InputStream openConnectionCheckRedirects(URLConnection c)
	throws IOException
    {
        boolean redir;
        int redirects = 0;
        InputStream in = null;

        do {
            if (c instanceof HttpURLConnectionTimeout) {
                ((HttpURLConnectionTimeout) c).setInstanceFollowRedirects(false);
            }
 
            // getHeaderField()他がIOExceptionを飲み込むので、
            // ヘッダを得る前に入力ストリームをオープンしたい。
            in = c.getInputStream();
            redir = false;
 
            if (c instanceof HttpURLConnectionTimeout) {
                HttpURLConnectionTimeout http = (HttpURLConnectionTimeout) c;
                int stat = http.getResponseCode();
                if (stat >= 300 && stat <= 305 &&
                        stat != HttpURLConnection.HTTP_NOT_MODIFIED) {
                    URL base = http.getURL();
                    String loc = http.getHeaderField("Location");
                    URL target = null;
                    if (loc != null) {
                        target = new URL(base, loc);
                    }
                    http.disconnect();
                    if (target == null
                        || !base.getProtocol().equals(target.getProtocol())
                        || base.getPort() != target.getPort()
                        || !HostsEquals(base, target)
                        || redirects >= 5)
                    {
                        throw new SecurityException("illegal URL redirect");
		    }
                    redir = true;
                    c = target.openConnection();
                    redirects++;
                }
            }
        } while (redir);
        return in;
    }

    // java.net.URL.hostsEqual と同じ


    static boolean HostsEquals(URL u1, URL u2) 
    { 
        final String h1 = u1.getHost();
        final String h2 = u2.getHost();

        if (h1 == null) {
            return h2 == null;
        } else if (h2 == null) {
            return false;
        } else if (h1.equalsIgnoreCase(h2)) {
            return true;
        }
        // 比較する前にアドレスの解決をしなければならない。そうでないと
        // tachyon と tachyon.eng という名前の比較で異なってしまう。
        final boolean result[] = {false};

        java.security.AccessController.doPrivileged(
            new java.security.PrivilegedAction() {
                public Object run() {
                    try {
                        InetAddress a1 = InetAddress.getByName(h1);
                        InetAddress a2 = InetAddress.getByName(h2);
                        result[0] = a1.equals(a2);
                    } catch(UnknownHostException e) {
                    } catch(SecurityException e) {
                    }
                    return null;
                }
            }
        );

        return result[0];
    }
	
    void Close() throws Exception
    {
        fClient.Close();
    }
	
    Socket GetSocket()
    {
        return fClient.GetSocket();
    }
}

使用例その1

import java.net.*;
public class MainTest
{

    public static void main(String args[])
    {
        int i = 0;
        try {
            URL theURL = new URL((URL)null, "http://www.snowball.com", new HttpTimeoutHandler(150)); // タイムアウト値をミリ秒で指定
            // 次のステップはオプション
            theURL.setURLStreamHandlerFactory(new HttpTimeoutFactory(150));

           URLConnection theURLconn = theURL.openConnection();
            theURLconn.connect();
            i = theURLconn.getContentLength();
        } catch (InterruptedIOException e) {
            System.out.println("timeout on socket");
        }
        System.out.println("Done, Length:" + i);
    }
}

使用例その2

    try {
        HttpTimeoutHandler xHTH = new HttpTimeoutHandler(10);	// タイムアウト値をミリ秒で指定
        URL theURL = new URL((URL)null, "http://www.javasoft.com", xHTH);
        HttpURLConnection theUC = theURL.openConnection();
		.
		.
		.
    } catch (InterruptedIOException e) {
		// socket timed out

    }

備考: このコードは、スレッドセーフ。

More to come