安卓 HttpUrlConnection EOFException

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)

有很多类似的错误报告和帖子提交到 Stack Overflow,但我无法理解是否真的存在问题,如果有的话,受影响的 Android 版本是哪些以及提出的修复/解决方法是什么。

以下是我所指的一些类似报告:

这里有一个可能的 Android 框架修复方案:

我知道,早期的 Froyo 中存在连接池中的错误连接问题,但这些问题仅在新的 ICS+ 设备上出现。如果后来的设备有问题,我会期望官方的 Android 文档中提到这个问题。


这个解决方法怎么样?有什么问题吗?https://dev59.com/r2Mm5IYBdhLWcg3wZ-XQ#17638671 - Darpan
@Darpan 你可以尝试一下,虽然根据堆栈跟踪看起来似乎无关。 - Kevin
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
}

2
嗨,马特。我正在尝试这个,但它不起作用。我想确认你的伪代码如下:
  1. 通过url.openConnection()打开连接。
  2. 如果在调用getResponseCode()时收到EOF,则断开连接,将HttpURLConnection设置为null并重新连接。
  3. 尝试这样做,直到达到最大重试次数。 这就是你所做的吗?谢谢。
- Juan Acevedo
1
是的,每次尝试请求时,我建议您重新创建HttpURLConnection,而不要尝试重用相同的实例。当您说它不起作用时,您指的是什么? - Matt Accola
1
谢谢您的回答。我的意思是我按照您说的做了,但仍然遇到EOFException问题。 - Juan Acevedo
你能否提供更多关于你遇到的问题的细节?无论你尝试多少次,你是不是一直都会收到EOFException异常?你在测试哪些设备或安卓版本? - Matt Accola
现在它可以正常工作了,而且没有改变任何代码,很奇怪...但是现在我的SSL引擎出问题了。虽然是不同的问题,但还是感谢您的帮助。 - Juan Acevedo
这种方法是一个不错的开始,但并不总是有效。一种可靠的替代方案是仅在需要时使用setRequestProperty("Connection", "close"),结合反复尝试的次数,以反映最大可能的陈旧连接数。请参见我的答案,此处https://dev59.com/iGIk5IYBdhLWcg3wdd4l#23795099。 - James Wald

7

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

这些连接本质上是套接字,而默认情况下,这个库不会关闭这些套接字。有时候可能会出现一种情况,即一个当前未被使用且仍然存在于池中的连接(套接字)由于服务器选择在一段时间后终止该连接而无法再使用。因此,即使服务器已经关闭了连接,该库也并不知道,并且认为该连接/套接字仍然处于连接状态。因此,它使用这个过期的连接发送新请求,从而导致我们得到EOFException异常。

处理这个问题的最好方法是在每次发送请求之后检查响应头。服务器在终止连接之前总是发送“Connection: Close”(HTTP 1.1)。因此,您可以使用getHeaderField()并检查“Connection”字段。还需要注意的是,只有在即将终止连接时,服务器才会发送这个连接字段。因此,在正常情况下(服务器没有关闭连接),需要通过编码来处理可能获得的“null”值。


谢谢您解释发生了什么,而不是只给出一个未经解释的命令让别人执行!非常感谢。[+1] - naki

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));
}

4
是的。在Android平台上有一个问题,具体来说,在Android libcore 4.1-4.3中存在问题。
这个问题是在这个提交中引入的:https://android.googlesource.com/platform/libcore/+/b2b02ac6cd42a69463fd172531aa1f9b9bb887a8 Android 4.4将http库切换到了“okhttp”,它没有这个问题。
问题的解释如下:
在Android 4.1-4.3上,当您使用URLConnection / HttpURLConnection进行POST并设置“ChunkedStreamingMode”或“FixedLengthStreamingMode”时,如果重用的连接已过期,则URLConnection / HttpURLConnection不会进行静默重试。您应该在代码中最多重试POST“http.maxConnections +1”次,就像以前的答案建议的那样。

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错误。


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;
}

-5

连接.addRequestProperty("Accept-Encoding", "gzip");

就是答案。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接