• OkHttp报unexcepted end of stream on...错误分析


    1. 问题背景

    • OkHttp版本:3.14.9
    • 问题描述
       在做网络请求优化过程中,首先根据耗时分析,发现接口在建立连接进行握手阶段耗时比较久,且每次都要进行建立连接过程,因为我通过修复实际项目中OKHttp的Keep-Alive失效问题,让短时间内频繁网络请求保持连接,对于界面网络数据刷新延迟收益明显,详情可以看:OkHttp请求时Keep-Alive无法生效问题修复记录
       但增加了"Connection":“Keep-Alive”之后,测试反馈有时候会遇到无网络的错误toast提示,然后再手动触发一次网络请求就能成功,随后抓取线上网络请求埋点,确实存在大量类似的错误。

    2. bug触发点定位

    问题发生在网路连接阶段,可以直接跟踪网络连接拦截器CallServerInterceptor:

    2.1 okhttp3.internal.http.CallServerInterceptor#intercept

    @Override public Response intercept(Chain chain) throws IOException {
    	...
    	responseBuilder = exchange.readResponseHeaders(true); //(1)
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2.2 okhttp3.internal.http1.Http1ExchangeCodec#readResponseHeaders

    • exchange.readResponseHeaders()最后会转调到Http1ExchangeCodec.readResponseHeaders()方法,关键代码如下:
    //okhttp3.internal.http1.Http1ExchangeCodec#readResponseHeaders():
    @Override public Response.Builder readResponseHeaders(boolean expectContinue) throws IOException {
        ...
        try {
          StatusLine statusLine = StatusLine.parse(readHeaderLine());
          Response.Builder responseBuilder = new Response.Builder()
              .protocol(statusLine.protocol)
              .code(statusLine.code)
              .message(statusLine.message)
              .headers(readHeaders());
    	  ...
          return responseBuilder;
        } catch (EOFException e) {
          ...
          //(1)找到了抛出发生错误的位置
          throw new IOException("unexpected end of stream on "
              + address, e);
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • (1)ExchangeCodec有两个实现类,分别是Http1ExchangeCodec和Http2ExchangeCodec,这里直接在两个实现类中搜索报错字符串,发现只有Http1ExchangeCodec实现类中存在该异常抛出逻辑。
    • (2)通过跟进源码可以知道Http2ExchangeCodec是给HTTP2使用的,发生该错误的http请求是HTTP1.1,从这个线索也可以直接看Http2ExchangeCodec这个实现类。
    • (3)Http1.1协议本身是支持链接复用的,同一个服务ip的tcp链接会在给定的Keep-Alive保持超时时间内复用,不用每次都重建连接。

    2.23 判断keep-alive超时逻辑

        首先是连接复用问题,可以聚焦到OkHttp的连接池ConnectionPool上,而ConnectionPool的实现类是RealConnectionPool,通过跟进连接池中Connection的放出和移出逻辑发现判断时机在cleanup()方法中:

    long cleanup(long now) {
      int inUseConnectionCount = 0;
      int idleConnectionCount = 0;
      RealConnection longestIdleConnection = null;
      long longestIdleDurationNs = Long.MIN_VALUE;
      // Find either a connection to evict, or the time that the next eviction is due.
      synchronized (this) {
        for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
          RealConnection connection = i.next();
          // If the connection is in use, keep searching.
          if (pruneAndGetAllocationCount(connection, now) > 0) {
            inUseConnectionCount++;
            continue;
          }
          idleConnectionCount++;
          // If the connection is ready to be evicted, we're done.
          long idleDurationNs = now - connection.idleAtNanos;
          if (idleDurationNs > longestIdleDurationNs) {
            longestIdleDurationNs = idleDurationNs;
            longestIdleConnection = connection;
          }
        }
        if (longestIdleDurationNs >= this.keepAliveDurationNs
            || idleConnectionCount > this.maxIdleConnections) {
          //(1)
          connections.remove(longestIdleConnection);
        } else if (idleConnectionCount > 0) {
          // A connection will be ready to evict soon.
          return keepAliveDurationNs - longestIdleDurationNs;
        } else if (inUseConnectionCount > 0) {
          // All connections are in use. It'll be at least the keep alive duration 'til we run again.
          return keepAliveDurationNs;
        } else {
          // No connections, idle or in use.
          cleanupRunning = false;
          return -1;
        }
      }
      //(2)
      closeQuietly(longestIdleConnection.socket());
      // Cleanup again immediately.
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 上述是连接池的维护逻辑,如果超过保持连接超时或空闲连接限制,则移除空闲时间最长的连接。
    • 虽然有超过链接复用的超时时间移除连接池逻辑,但是如果客户端不去请求并不能知道服务端已经单方面断开连接了,所以需要针对此类情况做兼容处理,当发现连接失败时触发重连等。
    • 触发判断移除连接的逻辑是在每次建立链接,将链接put进ConnectionPool时先触发一次cleanup逻辑
    • 将不符合继续缓存的连接移除后会同步阻塞的进行连接关闭逻辑

    配置Keep-Alive超时的位置:
    在这里插入图片描述
    在构建OKHttpClient时,Builder有开放connectPool()接口让使用方自己配置:
    在这里插入图片描述
    举例:

    OkHttpClient okHttpClient = new OkHttpClient.Builder()
            .readTimeout(1000, TimeUnit.SECONDS)
            .writeTimeout(1000, TimeUnit.SECONDS)
            //配置自定义连接池参数
            .connectionPool(new ConnectionPool(5, 60, TimeUnit.SECONDS)) 
            .build();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    3. 本地还原现场

    为了验证该问题,抛开实际项目中的额外逻辑(OkHttp的客制化逻辑等),我们采用本地模拟该条件进行还原,摸索是否能解决该问题。

    • 服务端:apache-tomcat-7.0.73
    • OkHttp版本:3.14.9

    4. 验证有效的解决方案

    在构建OKHttpClient的时候开启连接失败重试开关:

    OkHttpClient client = new OkHttpClient.Builder()
    	...
        .retryOnConnectionFailure(true) //开始连接失败时重连逻辑
        .build();
    
    • 1
    • 2
    • 3
    • 4

    修改后本地测试未复现,但因线上环境复杂,可能不同地区网络状态差异较大,该问题需要进一步分析线上埋点数据分析改善效果。

  • 相关阅读:
    【QT--使用百度地图API显示地图并绘制路线】
    byName自动装配和byType自动装配
    get_trade_detail_data函数使用
    Mac book pro 睡眠唤醒之后,外接显示器再也无法点亮,只能重启,怎么解决?
    「二叉树与递归的一些框架思维」「1464. 数组中两元素的最大乘积」(每日刷题打卡Day33)[C++]
    ES6初步了解生成器
    深入了解自适应布局与响应式布局的区别
    MySQL基础入门教程(InsCode AI 创作助手)
    数学建模【多元线性回归模型】
    网络层重点协议——IP协议
  • 原文地址:https://blog.csdn.net/yyg_2015/article/details/124359580