| |
So, this little tip popped up
as a side note on all the worries and problems with the - maybe overly
- generic URLConnection design. A discussion on the Robots
mail list made me remember it and a lifelong dedication to things
I should let slide - after all: there are free third party solutions ready
to use already - made me write this trick. But before we begin, two notes
of warning:
Warning: This trick subclasses some undocumented
classes from Sun shipped with the JRE, and Sun might change them at any
time. We will keep the changes as small as possible to allow us to fast
adopt such a change, but I felt a note of warning was justified.
Warning: This trick is not tested properly.
It is merely a fast patch, so use it carefully, review it, and send me
your comments.
These two notes said, I'd just like to stress the second again. For example:
The use of fall over for proxy servers is crude and I have not tried it
on keep-alive connections. Please mail me if you figure it out and I can
add it up to this article. You'll find the entire source of this article
here as a test class. Now, let's
start:
The setup:
The idea of this trick is to set a timeout to
URL connections. By the way: I will use the HTTP classes for this article,
but the trick should be applicable to all URLConnections. What we need
to do is to find the class that actually makes the socket connection to
the server, add the timeout to that same socket, modify the URLConnection
class to use our new timed-out client, and provide a stream handler that
can be passed to the URL constructor. First out is:
The sun.net.www.http.HttpClient class:
This is the class that connects to a server, sets
up the parameters for the HTTP call and handles keep-alive connections,
and much more. (Actually, the socket to the remote server is created by
the HttpClient parent class the NetworkClient, but we don't have to care
about that). So, we'll subclass the HttpClient with our own timeout version
and provide a reference to the socket the client is using and a constructor:
private Socket socket;
public TimeoutHttpClient(URL
url,
String proxy,
int proxyPort) throws IOException
{
super(url, proxy, proxyPort);
}
This is the constructor that actually does the work and for convenience
we'll also provide an overloading of the constructor using the default
values for the String and int parameters through this call:
public TimeoutHttpClient(URL
url)
throws
IOException {
this(url, null, -1);
}
Having done that, we need two methods. One method that overwrites the
"doConnect" method from the NetworkClient and one static "New"
method:
protected Socket doConnect(String
host, int port)
throws
IOException, UnknownHostException {
socket = new Socket(host, port);
socket.setSoTimeout(TimeoutHttpURLConnection.TIMEOUT);
return socket;
}
In this method "socket" is the reference to the instance member
we created above. In this example I also use a static variable in the
"TimeoutHttpURLConnection" that we will
create below to hold our default timeout value for simplicity, but it
could easily be a private member of the TimeoutHttpClient class.
public static HttpClient
New(URL url_1)
throws
IOException {
HttpClient httpc = (HttpClient)kac.get(url_1);
if(httpc == null) {
httpc = new TimeoutHttpClient(url_1, null,
-1);
} else httpc = HttpClient.New(url_1);
return httpc;
}
This is trickier. This is a static method used by the HttpURLConnection
class to check for keep-alive connections stored in the "kac"
member. So we'll remember this method for later. Note that we call the
parent class static "New" method - this is because the HttpClient
class wants to do a security check before assigning the new URL to the
connection. Now we'll do two small "get" and "set"
methods for the timeout and then the client is ready.
public void setTimeout(int millis) throws SocketException
{
if(socket != null) socket.setSoTimeout(millis);
}
public int
getTimeout() throws SocketException {
if(socket != null) return socket.getSoTimeout();
else return -1;
}
The sun.net.www.protocol.http.HttpURLConnection class:
Next in line is the actual URLConnection instance. Here
we need to make sure it does not create anything but our new timeout client
class as client instance. We will also provide the static variable for
the actual length of the timeout in milliseconds:
public static int
TIMEOUT = 30000;
public TimeoutHttpURLConnection(URL url,
String proxy,
int proxyPort) throws IOException {
super(url, proxy, proxyPort);
}
public TimeoutHttpURLConnection(URL url)
throws
IOException {
this(url, null, -1);
}
protected HttpClient getNewClient(URL url)
throws
IOException {
return new TimeoutHttpClient(url, null, -1);
}
protected HttpClient
getProxiedClient(URL url,
String proxy,
int proxyPort) throws IOException {
return new TimeoutHttpClient(url, proxy, proxyPort);
}
That's the easy bit. The hard bit comes now: We now need to implement
a "connect" method that gets a HttpClient and an output stream
for the connection. The catch is that the HttpClient uses a fall-over
mechanism before using an eventual proxy host, and the boolean variable
that determines that function is private. We'll work around that with
the assumption that if the "proxySet" system property is set to "true"
we're going to use a proxy if we can parse the system property value correctly.
If you have better ideas on how to perform this action please mail
me.
public void connect()
throws
IOException {
if(connected) return;
Properties prop = System.getProperties();
String set = (String)prop.get("proxySet");
if(set != null && set.equalsIgnoreCase("true"))
{
String host = (String)prop.get("proxyHost");
int port = -1;
try {
port = Integer.parseInt((String)prop.get("proxyPort"));
} catch(Exception e) { }
if(host != null && port != -1) {
http = new TimeoutHttpClient(url,
host, port);
} else http = TimeoutHttpClient.New(url);
} else http = TimeoutHttpClient.New(url);
if(http instanceof TimeoutHttpClient) {
((TimeoutHttpClient)http).setTimeout(TIMEOUT);
}
ps = (PrintStream)http.getOutputStream();
connected = true;
}
The URLStreamHandler class:
public TimeoutHttpStreamHandler()
{
super();
}
public URLConnection openConnection(URL url)
throws
IOException {
return new TimeoutHttpURLConnection(url);
}
The roundup:
Now, having done the above lines of code and compiled
them, it is time to test them. Personally I always have a dummy class
- which you'll find here - about
to copy code into and test. I'm sure you've figured it all out by know,
but for the sake of the article I'll provide my main method:
public static void main(String[]
args) {
try {
TimeoutHttpURLConnection.TIMEOUT = Integer.parseInt(args[1]);
URL base = new URL(args[0]);
URL url = new URL(base, "", new TimeoutHttpStreamHandler());
URLConnection con = url.openConnection();
InputStream in = con.getInputStream();
int tmp = 0;
while((tmp = in.read()) != -1) System.out.print((char)tmp);
in.close();
} catch(Exception e) {
e.printStackTrace();
}
}
The class is called with the URL you want to test it on as first parameter
and the timeout in milliseconds as second:
% java
mydummyclass www.yahoo.com 30000
Try to set the timeout to 1 millisecond (which should be too fast for
any setup) and watch your reward - the InterruptedIOException - appear
on the screen.
Now please try this out and... I'll say it again: This is an experiment.
Mail me your ideas and comments
and we can make this article complete together.
|
|