AndroidおよびPOSTリクエスト)に既知の問題があるかどうかを知りたい。intermittentPOSTクライアントからのリクエスト時のEOFExceptions Androidクライアント。同じリクエストを再試行すると、最終的には機能します。ここにサンプルスタックトレースがあります:
Java.io.EOFException
at libcore.io.Streams.readAsciiLine(Streams.Java:203)
at libcore.net.http.HttpEngine.readResponseHeaders(HttpEngine.Java:579)
at libcore.net.http.HttpEngine.readResponse(HttpEngine.Java:827)
at libcore.net.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.Java:283)
at libcore.net.http.HttpURLConnectionImpl.getResponseCode(HttpURLConnectionImpl.Java:497)
at libcore.net.http.HttpsURLConnectionImpl.getResponseCode(HttpsURLConnectionImpl.Java:134)
多くの同様のバグレポートとスタックオーバーフローへの投稿がありますが、本当に問題があるかどうか、そしてその場合、Androidのどのバージョンが影響を受け、提案された修正/回避策が何であるか)を理解できません。
ここに私が言及している同様のレポートのいくつかがあります:
これが潜在的なAndroidフレームワークの修正です
Pre-Froyoの接続プールで汚染された接続に問題があったことは知っていますが、これらの問題は新しいICS +デバイスでのみ発生しています。それ以降のデバイスで問題が発生した場合、ある種の公式のAndroid問題のドキュメントが必要です。
私たちの結論は、Androidプラットフォームに問題があるということです。私たちの回避策は、EOFExceptionをキャッチし、リクエストをN回再試行することでした。以下は擬似コードです。
private static final int MAX_RETRIES = 3;
private ResponseType fetchResult(RequestType request) {
return fetchResult(request, 0);
}
private ResponseType fetchResult(RequestType request, int reentryCount) {
try {
// attempt to execute request
} catch (EOFException e) {
if (reentryCount < MAX_RETRIES) {
fetchResult(request, reentryCount + 1);
}
}
// continue processing response
}
HttpURLConnectionライブラリは、内部的に接続のプールを維持します。そのため、リクエストが送信されるときは常に、まずプールに既存の接続が存在するかどうかを確認し、それに基づいて新しい接続を作成することを決定します。
これらの接続はソケットにすぎず、このライブラリはデフォルトではこれらのソケットを閉じません。現在使用されておらず、プールに存在する接続(ソケット)が、サーバーがしばらくして接続を終了することを選択したために使用できなくなっている場合があります。これで、サーバーによって接続が閉じられていても、ライブラリはそれを認識せず、接続/ソケットが接続されていると見なします。したがって、この古い接続を使用して新しいリクエストを送信するため、EOFExceptionが発生します。
これを処理する最良の方法は、送信する各要求の後に応答ヘッダーを確認することです。サーバーは常に、接続を終了する前に "Connection:Close"を送信します(HTTP 1.1)。したがって、getHeaderField()を使用して「接続」フィールドを確認できます。注意すべきもう1つのことは、サーバーが接続を終了しようとしているときにのみ、この接続フィールドを送信することです。したがって、通常の場合(サーバーが接続を閉じていない場合)に「null」を取得する可能性があるため、これを回避してコーディングする必要があります。
この回避策は、信頼性とパフォーマンスが高い傾向があります。
static final int MAX_CONNECTIONS = 5;
T send(..., int failures) throws IOException {
HttpURLConnection connection = null;
try {
// initialize connection...
if (failures > 0 && failures <= MAX_CONNECTIONS) {
connection.setRequestProperty("Connection", "close");
}
// return response (T) from connection...
} catch (EOFException e) {
if (failures <= MAX_CONNECTIONS) {
disconnect(connection);
connection = null;
return send(..., failures + 1);
}
throw e;
} finally {
disconnect(connection);
}
}
void disconnect(HttpURLConnection connection) {
if (connection != null) {
connection.disconnect();
}
}
この実装は、サーバーで開くことができるデフォルトの接続数が5
(Froyo-KitKat)であるという事実に依存しています。これは、最大5つの古い接続が存在する可能性があり、それぞれを閉じる必要があることを意味します。
試行が失敗するたびに、Connection:close
リクエストプロパティにより、connection.disconnect()
が呼び出されたときに、基になるHTTPエンジンがソケットを閉じます。最大6回(最大接続数+ 1)を再試行することにより、最後の試行に常に新しいソケットが与えられるようにします。
有効な接続がない場合、リクエストで追加のレイテンシが発生する可能性がありますが、EOFException
よりも確実に優れています。その場合、最後に送信を試みても、新しく開かれた接続はすぐには閉じられません。それが可能な唯一の実用的な最適化です。
5
の魔法のデフォルト値に依存する代わりに、システムプロパティを自分で設定できる場合があります。このプロパティはKitKatの ConnectionPool.Java の静的初期化ブロックによってアクセスされ、古いAndroidバージョンでも同様に機能します。その結果、プロパティは、設定する前に使用される場合があります。
static final int MAX_CONNECTIONS = 5;
static {
System.setProperty("http.maxConnections", String.valueOf(MAX_CONNECTIONS));
}
はい。 Androidプラットフォーム、具体的にはAndroid libcore with version 4.1-4.3。)に問題があります。
このコミットで問題が発生します: https://Android.googlesource.com/platform/libcore/+/b2b02ac6cd42a69463fd172531aa1f9b9bb887a8
Android 4.4は、http libをこの問題のない「okhttp」に切り替えました。
問題は次のように説明されています:
Android 4.1-4.3、URLConnection/HttpURLConnectionを使用している場合POST with "ChunkedStreamingMode"または "FixedLengthStreamingMode" set、URLConnection/HttpURLConnectionは、再利用された接続が古い場合、サイレント再試行を行いません。再試行する必要がありますPOST最大でコード内で "http.maxConnections + 1"回、以前の回答が示唆しているように。
ここで障害のあるサーバーである可能性があり、HttpURLConnectionは他の実装ほど寛容ではありません。それが私のEOFExceptionの原因でした。私の場合、これは断続的ではないと思われます(N再試行回避策をテストする前に修正された)ため、上記の回答は他の問題に関連しており、それらの場合の正しい解決策です。
私のサーバーはpython SimpleHTTPServerを使用しており、成功を示すために必要なのは次のことだけであると誤って想定していました。
self.send_response(200)
これにより、初期応答ヘッダー行、サーバー、日付ヘッダーが送信されますが、追加のヘッダーも送信できる状態でストリームが残ります。 HTTPでは、ヘッダーが終了したことを示すために、ヘッダーの後に追加の新しい行が必要です。 HttpURLConnectionを使用して結果の本文のInputStreamや応答コードなどを取得しようとしたときにこの新しい行が存在しない場合は、EOFExceptionをスローします(これは実際に考えれば妥当です)。一部のHTTPクライアントは短い応答を受け入れ、成功の結果コードを報告したため、おそらくHttpURLConnectionを不当に指差してしまいました。
代わりにこれを行うようにサーバーを変更しました:
self.send_response(200)
self.send_header("Content-Length", "0")
self.end_headers()
そのコードでは、EOFExceptionはもうありません。
これでうまくいきました。
public ResponseObject sendPOST(String urlPrefix, JSONObject payload) throws JSONException {
String line;
StringBuffer jsonString = new StringBuffer();
ResponseObject response = new ResponseObject();
try {
URL url = new URL(POST_URL);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoInput(true);
connection.setDoOutput(true);
connection.setReadTimeout(10000);
connection.setConnectTimeout(15000);
connection.setRequestMethod("POST");
connection.setRequestProperty("Accept", "application/json");
connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
OutputStream os = connection.getOutputStream();
os.write(payload.toString().getBytes("UTF-8"));
os.close();
BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream()));
while ((line = br.readLine()) != null) {
jsonString.append(line);
}
response.setResponseMessage(connection.getResponseMessage());
response.setResponseReturnCode(connection.getResponseCode());
br.close();
connection.disconnect();
} catch (Exception e) {
Log.w("Exception ",e);
return response;
}
String json = jsonString.toString();
response.setResponseJsonString(json);
return response;
}