32

我想知道 Android 上是否存在 HttpUrlConnection 和 POST 请求的已知问题。从 Android 客户端发出 POST 请求时,我们遇到了间歇性的 EOFExceptions。重试相同的请求最终会起作用。这是一个示例堆栈跟踪:

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 框架修复

我确实知道在 Froyo 之前的连接池中存在中毒连接的问题,但这些问题仅发生在新的 ICS+ 设备上。如果以后的设备出现问题,我希望有某种官方的 Android 文档来解决这个问题。

4

7 回答 7

12

我们的结论是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
}
于 2013-11-30T16:40:06.500 回答
7

HttpURLConnection 库在内部维护一个连接池。因此,每当发送请求时,它首先检查池中是否已经存在现有连接,并据此决定创建一个新连接。

这些连接只不过是套接字,默认情况下这个库不会关闭这些套接字。有时可能会发生当前未使用且存在于池中的连接(套接字)不再可用,因为服务器可能会选择在一段时间后终止连接。现在,由于连接即使被服务器关闭,库也不知道它并假设连接/套接字仍然连接。因此它使用这个陈旧的连接发送新请求,因此我们得到 EOFException。

处理此问题的最佳方法是在您发送每个请求后检查响应标头。服务器在终止连接(HTTP 1.1)之前总是发送“连接:关闭”。因此,您可以使用 getHeaderField() 并检查“连接”字段。另一件需要注意的是,服务器仅在即将终止连接时才发送此连接字段。因此,您需要在正常情况下(当服务器没有关闭连接时)获得“null”的可能性来解决这个问题

于 2014-04-03T07:04:11.993 回答
4

此解决方法往往可靠且高效:

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请求属性将导致底层 HTTP 引擎在connection.disconnect()被调用时关闭套接字。通过最多重试 6 次(最大连接数 + 1),我们确保最后一次尝试将始终获得一个新的套接字。

如果没有连接处于活动状态,请求可能会经历额外的延迟,但这肯定比EOFException. 在这种情况下,最后的发送尝试不会立即关闭新打开的连接。这是唯一可以进行的实际优化。

5您可以自己配置系统属性,而不是依赖于 的神奇默认值。请记住,此属性由 KitKat 的ConnectionPool.java中的静态初始化程序块访问,并且它在较旧的 Android 版本中也是如此。因此,该属性可能在您有机会设置它之前就被使用。

static final int MAX_CONNECTIONS = 5;

static {
    System.setProperty("http.maxConnections", String.valueOf(MAX_CONNECTIONS));
}
于 2014-05-21T22:45:28.430 回答
4

是的。Android平台存在问题,具体来说,Android libcore 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 并设置“ChunkedStreamingMode”或“FixedLengthStreamingMode”时,如果重用连接已过时,URLConnection/HttpURLConnection将不会进行静默重试。正如以前的答案所建议的那样,您应该在代码中最多重试“http.maxConnections+1”次。

于 2016-04-29T03:18:35.080 回答
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 。

于 2015-01-08T17:20:15.903 回答
0

这对我有用。

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;
}
于 2015-12-15T10:19:14.157 回答
-5

connection.addRequestProperty("Accept-Encoding", "gzip");

是答案

于 2014-06-30T06:48:49.200 回答