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,程式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
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,詳細請看這篇。