Java的URLConnection有兩種逾時(Timeout)設定,分別是建立連結(Connect)時的逾時-ConnectTimeout,以及傳遞串流資料(Stream)時的逾時-ReadTimeout。當Timeout發生時,URLConnection會拋出SocketTimeoutException,並立即中止連線。但是最近筆者發現HttpsURLConnection(或是URLConnection本身)使用POST Request(或是其他Request)發生ReadTimeout(或是ConnectTimeout)時,在拋出SocketTimeoutException前又會自動重新重新嘗試(Retry)同樣的POST Request,導致伺服器出現了兩次同樣的Request紀錄。
發生過程
事情是這樣的發生的,筆者在Android上使用HttpsURLConnection傳送POST Request給Server,但是Server處理Request時花的時間太長,太久沒有回應,使得HttpsURLConnection拋出SocketTimeoutException。理論上連線應該只有一次才對,但是Server竟然收到兩次同樣的Request。而且第二次的Request,似乎是在別的執行緒(未知的執行緒)上執行的,因此無法得知Request的結果。第二次的Request不管有沒有成功,都不會再有第三次的Request。
推測原因
單就問題的發生過程來看,Android的Java Runtime環境可能有BUG,會導致HttpsURLConnection傳送POST Request時,在因發生ReadTimeout而拋出SocketTimeoutException前,自動以未知的執行緒重新嘗試傳送同樣的Request。
最遭的情況是,任意平台下的Java Runtime環境可能都有這個BUG,會導致URLConnection傳送任意的Request時,在因發生ReadTimeout或ConnectTimeout而拋出SocketTimeoutException前,自動以未知的執行緒重新嘗試傳送同樣的Request。
但是筆者在PC上以HttpURLConnection來測試ReadTimeout,並沒有發生Retry的狀況,所以也許情況並沒有那麼遭。
Google搜尋了一下,在Stack Overflow上也有人遇到一樣的問題,請參考以下圖片:
處理方式與結論
Stack Overflow上發問者的解決方式是使用Apache的HttpClient來代替HttpsURLConnection,HttpClient能夠設定是否要Retry。但如果還是要使用HttpsURLConnection的話,建議手動實作Timeout,避免在SocketTimeoutException時發出兩次同樣的Request,程式如下:
try {
final URL url = new URL(urlString);
final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setReadTimeout(0); //禁用Timeout
conn.setConnectTimeout(0); //禁用Timeout
conn.setDoInput(true);
conn.setDoOutput(true);
conn.setRequestMethod("POST"); //以POST為例
conn.setRequestProperty("Content-Length", String.valueOf(postData.getBytes(DECODER).length));
//開新的Thread來計時
connecting = true; //是否正在連結的旗標
new Thread() {
@Override
public void run() {
try {
Thread.sleep(TIMEOUT); //TIMEOUT為逾時的時間(毫秒)
while (connecting) { //不斷嘗試斷開連結,直到連結真的完全斷開
conn.disconnect();
Thread.sleep(200);
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}.start();
//上傳POST的資料
final DataOutputStream dos = new DataOutputStream(conn.getOutputStream());
dos.writeBytes(postData); //postData為字串,例如:"id=10&page=1"
dos.flush();
dos.close();
//下載Server回傳的結果
final BufferedInputStream bis = new BufferedInputStream(conn.getInputStream());
final ByteArrayOutputStream bao = new ByteArrayOutputStream();
int c;
byte[] buffer = new byte[2048];
while ((c = bis.read(buffer)) >= 0) {
bao.write(buffer, 0, c);
}
bis.close();
String text = new String(bao.toByteArray(), DECODER);
bao.close();
System.out.println(text);
} catch (Exception ex) {
ex.printStackTrace();
} finally {
connecting = false;
}
使用ByteArrayOutputStream可以避免InputStreamReader可能造成的BUG,詳細請看這篇。