从一个后台数据问题来看 OkHttp 的网络请求流程

这是我参与8月更文挑战的第 2 天,活动详情查看:8月更文挑战

遇到问题了

OkHttp 在使用时对于后台返回的数据调用string()方法,转成字符串然后进行序列化相关处理即可。最近对接一个很老的系统,后台返回的数据使用的格式不是UTF-8而是GB2312,而且在Header里面没有设置任何Content-Type字段,导致直接获取数据时,OkHttp 会默认使用UTF-8来转换数据,导致乱码。具体解析源码如下:

  fun string(): String = source().use { source ->
    source.readString(charset = source.readBomAsCharset(charset()))
  }
  
  private fun charset() = contentType()?.charset(UTF_8) ?: UTF_8
复制代码

最终是在Exchange中获取ContentType的,

val contentType = response.header("Content-Type")
复制代码

为了需求,只能在使用过程中就直接通过Response获取字节数组然后手动转换,这样导致封装的调用方法需要增加一些额外的操作,暂时只能先这样。

寻求解决办法

等到后面翻看代码时,想到是不是可以通过拦截器进行处理,当然事实证明还是太年轻。

首先写一个拦截器,然后在OkHttp构造方法时设置即可。

class LoggingInterceptor implements Interceptor {
    @Override public Response intercept(Interceptor.Chain chain) throws IOException {
        Request request = chain.request();
        Response response = chain.proceed(request);
        if (response.request().url().encodedPath().toString().equals("/xxx/xxx")) {
            return response.newBuilder().header("Content-Type", "GB2312").build();
        }
        return response;
    }
}
复制代码

等设置完成后,觉得这个问题肯定解决了。没想到乱码还是没有任何变化,经过一阵点点点,找找找,似乎看到了一些眉目。

我们调用ResponseBody.string()方法来获取数据,这里调用source.readString(charset)方法进行处理,这里面的charset最终是在Exchange里面设置进去的,代码如下,

  fun openResponseBody(response: Response): ResponseBody {
    try {
      val contentType = response.header("Content-Type")
      val contentLength = codec.reportedContentLength(response)
      val rawSource = codec.openResponseBodySource(response)
      val source = ResponseBodySource(rawSource, contentLength)
      return RealResponseBody(contentType, contentLength, source.buffer())
    } catch (e: IOException) {
      eventListener.responseFailed(call, e)
      trackFailure(e)
      throw e
    }
  }
复制代码

我们的拦截器是生效了的,但是在这里的response是没有Content-Type的,只能说明这个方法是在我们拦截器之前调用的。那么是不是这样的呢,最好的解决办法就是去阅读源码了。

首先我们看下构造方法,

这里用了建造者模式来构造对象,

class Builder constructor() {
    internal var dispatcher: Dispatcher = Dispatcher()
    internal var connectionPool: ConnectionPool = ConnectionPool()
    // 传入的拦截器
    internal val interceptors: MutableList<Interceptor> = mutableListOf()
    internal val networkInterceptors: MutableList<Interceptor> = mutableListOf()
    ......
  }
复制代码

构造完成后,我们创建一个Request通过mOkHttpClient.newCall(request).enqueue()发起请求。Request类里面是一些请求相关参数,我们看下newCall方法,将我们的请求封装成了一个RealCall对象,并调用了enqueue方法。我们继续看下这个方法。

override fun newCall(request: Request): Call = RealCall(this, request, forWebSocket = false)
复制代码
override fun enqueue(responseCallback: Callback) {
  check(executed.compareAndSet(false, true)) { "Already Executed" }

  callStart()
  client.dispatcher.enqueue(AsyncCall(responseCallback))
}
复制代码

先校验下是否运行过,没有运行过才会继续执行。接着回调下方法,第三行里面将我们自己的回调封装成AsyncCall然后调用dispatcher.enqueue方法,我们继续跟下去。

internal fun enqueue(call: AsyncCall) {
  synchronized(this) {
    readyAsyncCalls.add(call)
    // 查找相同 host 的请求
    if (!call.call.forWebSocket) {
      val existingCall = findExistingCallWithHost(call.host)
      if (existingCall != null) call.reuseCallsPerHostFrom(existingCall)
    }
  }
  // 执行请求
  promoteAndExecute()
}

// 代码有删减
private fun promoteAndExecute(): Boolean { 
  synchronized(this) {
    while (i.hasNext()) {
      // 这里会限制最大请求数和每个 host 的最大请求数
      if (runningAsyncCalls.size >= this.maxRequests) break 
      if (asyncCall.callsPerHost.get() >= this.maxRequestsPerHost) continue 
      // ......
    }
  }
  for (i in 0 until executableCalls.size) {
    val asyncCall = executableCalls[i]
    // 通过线程池执行
    asyncCall.executeOn(executorService)
  }
  return isRunning
}
复制代码

这里的线程池的核心线程数是0最大线程数是Int.MAX_VALUE,继续看下去。

fun executeOn(executorService: ExecutorService) {
  var success = false
  try {
    executorService.execute(this)
    success = true
  } catch (e: RejectedExecutionException) {
    responseCallback.onFailure(this@RealCall, ioException)
  }
}
复制代码

这里可以看到对于请求失败调用了我们的回调方法。这里的AsyncCall就是一个Runnable,然后交给线程池运行,所以我们找到run方法继续看下去。

override fun run() {
  threadName("OkHttp ${redactedUrl()}") {
    try {
      // 通过这里进行请求,这里就是开始责任链模式调用了
      val response = getResponseWithInterceptorChain()
      signalledCallback = true
      responseCallback.onResponse(this@RealCall, response)
    } catch (e: IOException) {
      if (signalledCallback) {
      } else {
        responseCallback.onFailure(this@RealCall, e)
      }
    } catch (t: Throwable) {
      ......
    } finally {
    }
  }
}

internal fun getResponseWithInterceptorChain(): Response {
  // 添加上我们的拦截器和系统的拦截器
  val interceptors = mutableListOf<Interceptor>()
  interceptors += client.interceptors
  interceptors += RetryAndFollowUpInterceptor(client)
  interceptors += BridgeInterceptor(client.cookieJar)
  interceptors += CacheInterceptor(client.cache)
  interceptors += ConnectInterceptor
  if (!forWebSocket) {
    interceptors += client.networkInterceptors
  }
  interceptors += CallServerInterceptor(forWebSocket)
  // 构建请求链
  val chain = RealInterceptorChain(
      call = this,
      interceptors = interceptors,
      index = 0,
      exchange = null,
      request = originalRequest,
      connectTimeoutMillis = client.connectTimeoutMillis,
      readTimeoutMillis = client.readTimeoutMillis,
      writeTimeoutMillis = client.writeTimeoutMillis
  )
  var calledNoMoreExchanges = false
  try {
    // 开始调用
    val response = chain.proceed(originalRequest)
  } catch (e: IOException) {
  } finally {
  }
}
复制代码

到这里可以看到先是添加上我们的拦截器和系统的拦截器,然后构建链条,开始调用,我们看看proceded方法。

override fun proceed(request: Request): Response {
  ......
  // Call the next interceptor in the chain.
  val next = copy(index = index + 1, request = request)
  val interceptor = interceptors[index]
  val response = interceptor.intercept(next) ?: throw NullPointerException(
      "interceptor $interceptor returned null")
  ......
  return response
}
复制代码

这里面通过copy方法获取下一个Chain然后调用拦截器列表中的下一个拦截器进行请求,下一个拦截器又会依次调用,最终形成一个调用链条。

这里的逻辑比较绕,每一个拦截器都会调用proceed方法从上到下获取数据,最终会调用到CallServerInterceptor这个拦截器然后通过网络请求获取数据。获取数据成功后,又会将数据返回回去,这样又按照顺序从下到上回到拦截器的调用方法处,最终将结果返回到我们。

总结

这样整个流程我们大致简单的分析了一遍,当然这其中还是有很多东西我们并没有看到。那么我们回到开始的问题吧,可以发现在CallServerInterceptor这个拦截器中会调用exchange.createRequestBody().buffer()对出参进行了封装。这样也就解释通了为什么我们自定义拦截器增加Content-Type对数据处理没有影响了。虽然这里我们没有很好的解决我们开头的问题,但是通过阅读源码我们还是学习到了很多知识的,看来这波不亏。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享