当前位置: 首页 > 知识库问答 >
问题:

改造OkHttp-“意外的流结束”

锺英彦
2023-03-14

我得到意外结束流,而使用改造(2.9.0)与OkHttp3(4.9.1)

改装配置:

interface ApiServiceInterface {

    companion object Factory{

        fun create(): ApiServiceInterface {
            val interceptor = HttpLoggingInterceptor()
            interceptor.level = HttpLoggingInterceptor.Level.BODY

            val client = OkHttpClient.Builder()
                .connectTimeout(30, TimeUnit.SECONDS)
                .writeTimeout(30, TimeUnit.SECONDS)
                .readTimeout(30,TimeUnit.SECONDS)
                .addInterceptor(Interceptor { chain ->
                    chain.request().newBuilder()
                        .addHeader("Connection", "close")
                        .addHeader("Accept-Encoding", "identity")
                        .build()
                        .let(chain::proceed)
                })
                .retryOnConnectionFailure(true)
                .connectionPool(ConnectionPool(0, 5, TimeUnit.MINUTES))
                .protocols(listOf(Protocol.HTTP_1_1))
                .build()
            val gson = GsonBuilder().setLenient().create()
            val retrofit = Retrofit.Builder()
                    .addCallAdapterFactory(CoroutineCallAdapterFactory())
                    .addConverterFactory(GsonConverterFactory.create(gson))
                    .baseUrl("http://***.***.***.***:****")
                    .client(client)
                    .build()
            return retrofit.create(ApiServiceInterface::class.java)
        }
    }


    @Headers("Content-type: application/json", "Connection: close", "Accept-Encoding: identity")
    @POST("/")
    fun requestAsync(@Body data: JsonObject): Deferred<Response>
}

到目前为止,我发现了以下几点:

  1. 这个问题只发生在我使用从Windows系列操作系统(7、10、11)运行的Android Studio模拟器时——这是在来自不同网络的两台不同笔记本电脑上复制的
  2. 如果在苹果操作系统下运行Android Studio模拟器,那么在所有情况下,问题都不会再现
  3. ARC/Postman客户端在完成对我后端的相同请求时从来没有任何问题
  4. 在Windows Android Studio模拟器上运行时,此问题会在约10-50%的请求中重现,其他请求也可以正常工作
  5. 相同的请求可能会导致此错误或成功完成
  6. 完成大约需要11秒的响应可能会导致成功,而完成大约需要100毫秒的响应可能会导致此错误
  7. 注释关闭<代码>。客户端(client)改进配置消除了这个问题,但我失去了使用拦截器和其他OkHttp功能的机会
  8. 添加标头(连接:关闭,接受编码:标识)并不能解决问题
  9. 打开或关闭retryOnConnectionFailure对问题也没有影响
  10. 更改HttpLoggingInterceptor级别或完全删除它并不能解决这个问题

服务器端配置:

const http = require('http');
const server = http.createServer((req, res) => {

    const callback = function(code, request, data) {
        let result = responser(code, request, data);
        res.writeHead(200, {
            'Content-Type' : 'x-application/json',
            'Connection': 'close',
            'Content-Length': Buffer.byteLength(result)
        });
        res.end(result);
    };

    ...
}

server.listen(process.env.PORT, process.env.HOSTNAME, () => {
    console.log(`Server is running`);
});

因此,基于1、2、3,这不太可能是服务器端问题
基于456-这不是与请求相关或与执行时间相关的格式错误问题。从7中猜测——这个问题的根源在于OkHttp,而不是改造本身。

我已经阅读了几乎一半的stackoverflow是关于解决方案的搜索,例如:意外的流结束改型关于连接错误的意外流结束改型以及Github上的OkHttp讨论:
https://github.com/square/okhttp/issues/3682
https://github.com/square/okhttp/issues/3715
但到目前为止没有任何帮助。

知道是什么导致了这个问题吗?

使现代化

我有更多关于情况的信息。

首先,我将后端的头更改为不传递内容长度,而是传递传输编码:标识。我不知道为什么,但邮递员给出了一个错误,如果theese头都存在,说这是不对的。

res.writeHead(200, {
            'Content-Type' : 'x-application/json',
            'Connection': 'close',
            'Transfer-Encoding': 'identity'
        });

在那之后,我开始在Windows托管的Android Studio模拟器上收到另一个错误(失败/成功与“意外流结束”的比率相等)

2021-12-09 14:58:19.696 401-401/? D/P2P-> FRG DEBUG:: java.io.EOFException: End of input at line 1 column 1807 path $.meta
        at com.google.gson.stream.JsonReader.nextNonWhitespace(JsonReader.java:1397)
        at com.google.gson.stream.JsonReader.doPeek(JsonReader.java:483)
        at com.google.gson.stream.JsonReader.hasNext(JsonReader.java:415)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:216)
        at retrofit2.converter.gson.GsonResponseBodyConverter.convert(GsonResponseBodyConverter.java:40)
        at retrofit2.converter.gson.GsonResponseBodyConverter.convert(GsonResponseBodyConverter.java:27)
        at retrofit2.OkHttpCall.parseResponse(OkHttpCall.java:243)
        at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:153)
        at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:519)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:764)

花了很多时间调试这个问题,我发现这个异常是由JsonReader生成的。java在方法nextNonWhitespace中尝试获取冒号、双引号和花括号或方括号,以从解码为字符数组缓冲区的json对象组成json对象。

该缓冲区本身在同一模块的fillBuffer方法中接收,其长度限制为1024个元素。在我的例子中,后端响应比这个值(1807个字符)长,所以当JsonReader。java将我的响应解析为json对象,它只需2次迭代。

每次迭代都会在此处填充缓冲区:

int total;
while ((total = in.read(buffer, limit, buffer.length - limit)) != -1) {
    limit += total;

    // if this is the first read, consume an optional byte order mark (BOM) if it exists
    if (lineNumber == 0 && lineStart == 0 && limit > 0 && buffer[0] == '\ufeff') {
        pos++;
        lineStart++;
        minimum++;
    }

    if (limit >= minimum) {
        return true;
    }
}

在响应库上调用读取方法。ktclass fromokhttp3

@Throws(IOException::class)
    override fun read(cbuf: CharArray, off: Int, len: Int): Int {
      if (closed) throw IOException("Stream closed")

      val finalDelegate = delegate ?: InputStreamReader(
          source.inputStream(),
          source.readBomAsCharset(charset)).also {
        delegate = it
      }
      return finalDelegate.read(cbuf, off, len)
    }

主要问题是:

在第一次迭代中,一切都很顺利,响应Body.kt“读取”前1024个字符并将它们提供给JsonReader.java,其中它构成了响应对象的一部分。

当第二次迭代到来时,响应库。kt“读取”响应的最后一部分,并用它填充char缓冲区的开头,因此char缓冲区现在包含响应的尾部作为其第一个元素,之后是第一次迭代后留在那里的所有元素。

主要问题是它在大多数情况下(大约80%)会从响应中丢失最后一个字符,大约10%的情况下会从响应中丢失2个最后一个字符,大约10%的情况下会读取所有字符。这是镜头:

在这种情况下,请求将成功。

更新2

似乎在RealBufferedSource.kt中获得了完整的缓冲区:

internal inline fun RealBufferedSource.commonSelect(options: Options): Int {
  check(!closed) { "closed" }

  while (true) {
    val index = buffer.selectPrefix(options, selectTruncated = true)
    when (index) {
      -1 -> {
        return -1
      }
      -2 -> {
        // We need to grow the buffer. Do that, then try it all again.
        if (source.read(buffer, Segment.SIZE.toLong()) == -1L) return -1
      }
      else -> {
        // We matched a full byte string: consume it and return it.
        val selectedSize = options.byteStrings[index].size
        buffer.skip(selectedSize.toLong())
        return index
      }
    }
  }
}

更新3

发现这个未解决的问题,这是完全相同的行为:
Retrofit Json数据截断
另评论来自Android Studio模拟器问题跟踪器:
https://issuetracker.google.com/issues/119027639#comment9


共有1个答案

松成和
2023-03-14

好吧,这花了一些时间,但我已经找到了问题所在以及如何解决。

当Android Studio的模拟器在Windows系列操作系统中运行时(检查7

我发现的解决方法是添加一个拦截器来进行改进,该拦截器将检查解码到的字符串体的最后符号是否与有效json响应的符号匹配,如果错过了,则添加它们。

interface ApiServiceInterface {

    companion object Factory{

        fun create(): ApiServiceInterface {
            val interceptor = HttpLoggingInterceptor()
            interceptor.level = HttpLoggingInterceptor.Level.BODY
            
            val stringInterceptor = Interceptor { chain: Interceptor.Chain ->
                val request = chain.request()
                val response = chain.proceed(request)
                val source = response.body()?.source()
                source?.request(Long.MAX_VALUE)
                val buffer = source?.buffer()
                var responseString = buffer?.clone()?.readString(Charset.forName("UTF-8"))
                if (responseString != null && responseString.length > 2) {
                    val lastTwo = responseString.takeLast(2)
                    if (lastTwo != "}}") {
                        val lastOne = responseString.takeLast(1)
                        responseString = if (lastOne != "}") {
                            "$responseString}}"
                        } else {
                            "$responseString}"
                        }
                    }
                }
                val contentType = response.body()?.contentType()
                val body = ResponseBody.create(contentType, responseString ?: "")
                return@Interceptor response.newBuilder().body(body).build()
            }

            val client = OkHttpClient.Builder()
                .connectTimeout(30, TimeUnit.SECONDS)
                .writeTimeout(30, TimeUnit.SECONDS)
                .readTimeout(30,TimeUnit.SECONDS)
                .addInterceptor(interceptor)
                .addInterceptor(stringInterceptor)
                .retryOnConnectionFailure(true)
                .connectionPool(ConnectionPool(0, 5, TimeUnit.MINUTES))
                .protocols(listOf(Protocol.HTTP_1_1))
                .build()
            val gson = GsonBuilder().create()
            val retrofit = Retrofit.Builder()
                .addCallAdapterFactory(CoroutineCallAdapterFactory())
                .addConverterFactory(GsonConverterFactory.create(gson))
                .addConverterFactory(ScalarsConverterFactory.create())
                .baseUrl("http://3.124.6.203:5000")
                .client(client)
                .build()
            return retrofit.create(ApiServiceInterface::class.java)
        }
    }


    @Headers("Content-type: application/json", "Connection: close", "Accept-Encoding: identity")
    @POST("/")
    fun requestAsync(@Body data: JsonObject): Deferred<Response>
}

在此更改后,问题没有发生。

 类似资料:
  • 我已经按照此链接刷新了访问令牌。在将身份验证器添加到okHttp时,从改造回调onFailure方法中获取okhttp3上的意外流结束错误

  • 我的服务接口如下所示 我像下面这样构建客户机,并将设置为90秒。 请求到达服务器,就在服务器响应时,我得到以下错误: null 它有时起作用。并不总是失败。(同一个文件总是与postman一起工作) 它始终适用于其他文件(从来没有问题)。 请求到达服务器,处理请求时没有任何错误,也会响应。在服务器完成处理并响应客户端后,我立即得到错误。 编辑1:我点击的服务器由gunicorn/20.0.4支持,

  • 我有以下代码: 为什么它会打印Java流?

  • 当我发现这个的时候,我已经有一段时间了。使用swankjesse提供的解决方案后,错误消失了。我只是不明白为什么这是一个解决办法。我在网上找不到任何解释这种方法解决错误的原因的东西。

  • 问题内容: Redis 3.0.5 Spring数据Redis 1.3.6 jedis 2.6.3- 我们的Web应用程序,它通过pub / sub从redis接收数据。 -还以键/值对的形式对redis进行数据读/写。 -读/写发生在侦听器线程,独立监视线程和http请求线程上。 -我们对侦听器和Redis模板使用了相同的连接工厂 -我们的Redis服务器已配置“ timeout = 30” 在

  • 我读过其他线程,谈到使用单线程进行读/写。但是在我们的情况下,很难使用单线程。同样,根据RedisTemplate文档,它是线程安全的。该问题是偶然的,我们无法在任何开发/测试/UAT环境中重现。因此无法找到相同的确切原因。我们做错了什么?