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

1. 问题背景

  • OkHttp版本:3.14.9
  • 问题描述
     在做网络请求优化过程中,首先根据耗时分析,发现接口在建立连接进行握手阶段耗时比较久,且每次都要进行建连过程。我通过修复实际项目中OKHttp的Keep-Alive失效问题,让短时间内(keep-alive有一个默认的timeout时间,详情可以看ConnectionPool类的构造方法)对同一域名的网络请求连复用,对于降低网络数据刷新延迟效果明显,详情可以看: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)
	...
}
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)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;
}
  • 上述是连接池的维护逻辑,如果超过保持连接超时或空闲连接限制,则移除空闲时间最长的连接。
  • 虽然有超过链接复用的超时时间移除连接池逻辑,但是如果客户端不去请求并不能知道服务端已经单方面断开连接了,所以需要针对此类情况做兼容处理,当发现连接失败时触发重连等。
  • 触发判断移除连接的逻辑是在每次建立链接,将链接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();

3. 本地还原现场

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

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

4. 验证有效的解决方案

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

OkHttpClient client = new OkHttpClient.Builder()
	...
    .retryOnConnectionFailure(true) //开启连接失败时重连逻辑
    .build();

修改后本地测试未复现,但因线上环境复杂,可能不同地区网络状态差异较大,该问题需要进一步分析线上埋点数据分析改善效果。
!!开启了失败重连务必测试重连带来的服务器压力和重试次数。

根据提供的引用内容,我可以为您介绍一下"Exception in thread 'OkHttp Dispatcher' java.lang.NullPointerException"错误的解决方法。 这个错误通常是由于在使用OkHttp库时,发生了空指针异常导致的。空指针异常意味着您的代码尝试访问一个空对象的属性或方法。 要解决这个问题,您可以按照以下步骤进行操作: 1. 检查代码中的空引用:首先,您需要检查代码中是否存在空引用。确保在使用OkHttp库的地方,没有使用空对象。 2. 检查网络连接:如果您的代码涉及到网络连接,那么请确保您的设备已经连接到网络,并且网络连接是正常的。如果网络连接不稳定或者断开,可能会导致OkHttp库出现空指针异常。 3. 检查OkHttp版本:如果您使用的是较旧的OkHttp版本,那么可能会存在一些已知的问题和bug。请尝试升级到最新版本的OkHttp库,以解决可能存在的问题。 4. 检查代码逻辑:仔细检查您的代码逻辑,确保在使用OkHttp库的地方,没有遗漏任何必要的参数或者方法调用。确保您正确地使用了OkHttp库提供的方法和功能。 5. 使用调试工具:如果以上步骤都没有解决问题,您可以尝试使用调试工具来定位问题。通过在代码中设置断点,并使用调试工具逐步执行代码,您可以找到导致空指针异常的具体位置,并进行修复。 希望以上解决方法能够帮助您解决"Exception in thread 'OkHttp Dispatcher' java.lang.NullPointerException"错误。如果您还有其他问题,请随时提问。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TechMix

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值