2401_86240842 2025-10-02 16:09 采纳率: 0%
浏览 19

Unexpected end of stream究竟怎么解决?

我在 Android 项目里用 OkHttp 调用自己服务器接口时,经常报错:
java.net.ProtocolException: unexpected end of stream

环境:

  • 客户端:Android,OkHttp 4.x
  • 服务器:gunicorn + flask
  • 请求频率:每隔 3s 调一次接口
  • 浏览器直接访问接口完全正常,只有 App 调用会出错

代码片段:


val client = OkHttpClient.Builder()
    .connectTimeout(30, TimeUnit.SECONDS)
    .readTimeout(30, TimeUnit.SECONDS)
    .writeTimeout(30, TimeUnit.SECONDS)
    // 下列配置都尝试过:
    .retryOnConnectionFailure(true/false)
    .connectionPool(ConnectionPool(0, 1, TimeUnit.NANOSECONDS))
    .protocols(listOf(Protocol.HTTP_1_1))
    .pingInterval(30, TimeUnit.SECONDS)
    // 强制 IPv4 也试过
    .dns { hostname ->
        InetAddress.getAllByName(hostname).filter { it is Inet4Address }
    }
    .build()

val request = Request.Builder()
    .url("http://xxx.xxx.xxx.xxx:8000/status")
    .get()
    // 这里也试过强制关闭 keep-alive
    .header("Connection", "close")
    .build()

client.newCall(request).execute().use { response ->
    if (!response.isSuccessful) throw Exception("Unexpected code $response")
    val body = response.body?.string() ?: return "fail"
    val json = JSONObject(body)
    return json.getString("status")
}

我尝试过的思路:

  1. retryOnConnectionFailure 开关都试过;
  2. 改过 connectionPool 配置(包括禁用连接池),问题依旧;
  3. 强制协议为 HTTP_1_1 也不行;
  4. 加了 pingInterval,没改善;
  5. request 里加了 .header("Connection", "close"),依旧会报错;
  6. 强制 IPv4 访问(过滤掉 IPv6 地址),依旧会出错。

现象:

  • 报错不稳定,浏览器访问完全正常,App 偶发报错;
  • 怀疑和 gunicorn 的 keep-alive/worker 配置,或者 OkHttp 循环请求复用连接有关。

想请教:

  1. 这种 “unexpected end of stream” 更可能是客户端还是服务器端的问题?
  2. gunicorn 端是否需要额外配置 keep-alive / worker 参数?
  3. OkHttp 端有没有更推荐的处理方式来避免这种报错?

谢谢大家!

  • 写回答

4条回答 默认 最新

  • 檀越@新空间 2025-10-02 16:10
    关注

    下午好🌅🌅🌅
    本答案参考通义千问

    你遇到的 java.net.ProtocolException: unexpected end of stream 是一个常见的网络问题,尤其是在使用 OkHttp 与后端服务器(如 gunicorn + Flask)进行频繁请求时。这个问题通常表示 客户端在等待服务器响应时,连接被意外关闭


    ✅ 一、问题分析

    1. 问题可能出在哪里?

    | 位置 | 可能原因 | |------|----------| | 客户端(Android + OkHttp) | 频繁请求导致连接复用或超时;未正确处理响应体;没有正确关闭连接;配置不当(如协议、keep-alive 等)。 | | 服务端(gunicorn + Flask) | worker 超时;没有正确处理 keep-alive;没有正确读取请求体;没有正确返回响应;worker 数量不足或配置不当。 |


    ✅ 二、更可能是哪一方的问题?

    结论:更可能是服务端的问题,但客户端也需要配合调整。

    • 浏览器访问正常:说明接口本身是正确的。
    • App 偶发报错:说明连接可能在某些情况下被服务器主动关闭,或者客户端没有正确处理响应。
    • OkHttp 的默认行为:如果服务器提前关闭连接,OkHttp 会抛出 unexpected end of stream 异常。

    ✅ 三、解决方案

    1. 服务端优化(gunicorn + Flask)

    (1) 增加 gunicorn 的超时时间

    gunicorn 默认有 --timeout 参数(默认 30 秒),如果你的请求需要超过这个时间,服务器可能会断开连接。

    gunicorn -w 4 -b 0.0.0.0:8000 --timeout 60 your_app:app
    

    解释:将超时时间设置为 60 秒,避免因为请求处理时间过长而被服务器中断。

    (2) 增加 worker 数量

    如果你的服务器负载较高,可以增加 worker 数量:

    gunicorn -w 8 -b 0.0.0.0:8000 your_app:app
    

    (3) 启用 keep-alive 支持

    确保 gunicorn 和 Flask 正确支持 HTTP Keep-Alive:

    # 在 Flask 应用中添加以下内容
    from flask import Flask
    import gunicorn
    
    app = Flask(__name__)
    
    @app.before_first_request
    def set_keepalive():
        # 设置 Flask 的连接保持
        app.wsgi_app = gunicorn.app.base.WSGIApplication(app.wsgi_app)
    

    注意:gunicorn 本身支持 keep-alive,但需要确认是否启用。


    2. 客户端优化(OkHttp)

    (1) 禁用 keep-alive(强制每次新建连接)

    虽然效率低,但可以避免因连接复用导致的异常:

    val request = Request.Builder()
        .url("http://xxx.xxx.xxx.xxx:8000/status")
        .get()
        .header("Connection", "close") // 强制关闭连接
        .build()
    

    建议:如果你的请求频率高,不推荐长期使用此方式。

    (2) 使用 connectionPool 时,合理设置参数

    val connectionPool = ConnectionPool(5, 1, TimeUnit.MINUTES)
    
    val client = OkHttpClient.Builder()
        .connectTimeout(30, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .writeTimeout(30, TimeUnit.SECONDS)
        .connectionPool(connectionPool)
        .protocols(listOf(Protocol.HTTP_1_1))
        .pingInterval(30, TimeUnit.SECONDS)
        .build()
    

    解释:使用合理的连接池,避免过多连接堆积。

    (3) 确保响应体被完全读取

    client.newCall(request).execute().use { response ->
        if (!response.isSuccessful) throw Exception("Unexpected code $response")
        val body = response.body?.string() ?: return "fail"
        val json = JSONObject(body)
        return json.getString("status")
    }
    

    注意:确保 body.string() 被调用,否则服务器可能认为请求未完成,提前关闭连接。

    (4) 添加重试逻辑(可选)

    你可以使用 OkHttp 的 RetryInterceptor 或手动实现重试机制,例如:

    class RetryInterceptor : Interceptor {
        override fun intercept(chain: Interceptor.Chain): Response {
            var response: Response? = null
            for (i in 0..2) { // 最多重试 2 次
                try {
                    response = chain.proceed(chain.request())
                    if (response.isSuccessful) return response
                } catch (e: IOException) {
                    Log.e("Retry", "Request failed, retrying...")
                }
            }
            return response ?: throw IOException("Failed to get response after retries")
        }
    }
    

    使用方法

    val client = OkHttpClient.Builder()
        .addInterceptor(RetryInterceptor())
        ...
        .build()
    

    ✅ 四、总结

    | 问题类型 | 推荐做法 | |----------|----------| | 服务端问题 | 增加 gunicorn 的 timeout 和 worker 数量,确保 keep-alive 正确工作。 | | 客户端问题 | 确保响应体被完全读取,适当禁用 keep-alive 或使用合理的连接池。 | | 通用建议 | 在客户端添加重试逻辑,提升容错能力。 |


    ✅ 五、最终推荐配置(OkHttp)

    val connectionPool = ConnectionPool(5, 1, TimeUnit.MINUTES)
    
    val client = OkHttpClient.Builder()
        .connectTimeout(30, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .writeTimeout(30, TimeUnit.SECONDS)
        .connectionPool(connectionPool)
        .protocols(listOf(Protocol.HTTP_1_1))
        .pingInterval(30, TimeUnit.SECONDS)
        .build()
    

    重点:确保 response.body?.string() 被调用,防止服务器提前关闭连接。


    如有更多细节(如服务器日志、请求体内容等),可以进一步排查问题根源。希望这些方案能帮助你解决 unexpected end of stream 问题!

    评论

报告相同问题?

问题事件

  • 创建了问题 10月2日