OKHttp 是一个来自 Square 的 HTTP 客户端框架,用于 Java 和 Android 应用程序。它的设计是为了更快地加载资源并节省带宽。
OKHttp 源码地址:https://github.com/square/okhttp
OKHttp 的简单使用
下面让我们从一个简单的 Demo 来开始 OKHttp 的源码阅读。
class OKHttpGetDemo {
private val client = OkHttpClient();
fun run(url: String): String? {
val request = Request.Builder()
.url(url)
.build()
return client.newCall(request).execute().body?.use {
it.string()
}
}
}
fun main() {
val example = OKHttpGetDemo()
val response = example.run("https://rwa.github.com/square/okhttp/master/README.md")
println(response)
}
OKHttpClient
在使用 OKHttp 时,我们首先需要一个 OKHttpClient
。它作为一个请求发起的 Client,保存了很多关于处理请求的配置。当然这里使用了 OKHttpClient
的无参构造,也就是说我们使用的都是默认的配置。 如果我们想要对 OKHttpClient 进行配置,那么就需要使用 OKHttpClient.Build
来配置 OKHttpClient
。注意,这里使用到了建造者模式,这是因为 OKHttpClient
有很多参数。日常开发中,如果遇到一个类的构造需要很多参数的情况,也可以尝试使用建造者模式。
Request
Request 类表示请求,可以看到这里也是用了建造者模式。
open class Builder {
internal var url: HttpUrl? = null
internal var method: String
internal var headers: Headers.Builder
internal var body: RequestBody? = null
/** A mutable map of tags, or an immutable empty map if we don't have any. */
internal var tags: MutableMap, Any> = mutableMapOf()
// ...
}
想要发起一个请求,我们需要知道
- 请求的 url,
- 请求的方法,
- 请求头,
- 以及请求体(非必须)
RealCall
在创建好 Request 后,就可以通过 OKHttpClient 去发起请求,这里调用了下面的方法:
client.newCall(request).execute()
该方法最终会返回一个 Response
对象。
可以看到我们先通过 client.newCall(request)
得到了一个 Call
,那这个 Call 是什么呢?
A call is a request that has been prepared for execution. A call can be canceled. As this object represents a single request/response pair (stream), it cannot be executed twice.
Call 是一个已经准备好执行的 Request,一个 Call 实例能够被取消。该实例表示的是单个请求/响应(流),因此不能被执行两次。
也就是说 Call 会支持一个已经准备好的 Request 然后调用 execute
方法可以得到 Response
。
override fun newCall(request: Request): Call = RealCall(this, request, forWebSocket = false)
newCall
的方法如上所示。
最终又返回了一个 RealCall
对象。继续往下看看 RealCall 的 execute
方法的实现:
override fun execute(): Response {
check(executed.compareAndSet(false, true)) { "Already Executed" }
timeout.enter()
callStart()
try {
client.dispatcher.executed(this)
return getResponseWithInterceptorChain()
} finally {
client.dispatcher.finished(this)
}
}
executed
是一个 AtomicBoolean
类型的一个对象,这里通过 CPS 来确保一个 RealCall 只能执行一次。这里的这个 CPS 的含义是将 executed 的值和 false 比较,如果比较通过则返回 更新为 true,并且返回 true,否则返回 false。
timeout.enter()
表示开始进入超时的倒计时。
private val timeout = object : AsyncTimeout() {
override fun timedOut() {
cancel()
}
}.apply {
timeout(client.callTimeoutMillis.toLong(), MILLISECONDS)
}
接下来看看 callStart
方法:
private fun callStart() {
this.callStackTrace = Platform.get().getStackTraceForCloseable("response.body().close()")
eventListener.callStart(this)
}
也可以使用 RealCall
进行异步请求。下面我们来看一个异步请求的 Demo。
class OKHttpPostDemo {
private val client = OkHttpClient();
fun run(url: String) {
val request = Request.Builder()
.url(url)
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
throw e
}
override fun onResponse(call: Call, response: Response) {
println(response.body?.string())
}
})
}
}
fun main() {
val example = OKHttpPostDemo()
example.run("https://rwa.github.com/square/okhttp/master/README.md")
}
接下来我们来看看异步的 enquene
方法:
override fun enqueue(responseCallback: Callback) {
// 检查当前请求是否正在执行
check(executed.compareAndSet(false, true)) { "Already Executed" }
callStart()
client.dispatcher.enqueue(AsyncCall(responseCallback))
}
Dispatcher
的意思是调度或者分发,内部维护了一个请求的队列,而这里 enqueue
方法就像它的名字一样,会将当前的请求压入队列中。
Platform
可以看到 callStart
方法中调用了 Platform
类的方法。Platform
是将一些与平台相关的代码抽取到了这个类中。
Platform 类现在又如下的实现:
OKHttp 中对 Platform
分为了两类,Andoird 和 JVM。这里以 JVM 为例。
private fun findJvmPlatform(): Platform {
if (isConscryptPreferred) {
val conscrypt = ConscryptPlatform.buildIfSupported()
if (conscrypt != null) {
return conscrypt
}
}
if (isBouncyCastlePreferred) {
val bc = BouncyCastlePlatform.buildIfSupported()
if (bc != null) {
return bc
}
}
if (isOpenJSSEPreferred) {
val openJSSE = OpenJSSEPlatform.buildIfSupported()
if (openJSSE != null) {
return openJSSE
}
}
// An Oracle JDK 9 like OpenJDK, or JDK 8 251+.
val jdk9 = Jdk9Platform.buildIfSupported()
if (jdk9 != null) {
return jdk9
}
// An Oracle JDK 8 like OpenJDK, pre 251.
val jdkWithJettyBoot = Jdk8WithJettyBootPlatform.buildIfSupported()
if (jdkWithJettyBoot != null) {
return jdkWithJettyBoot
}
return Platform()
}
前面判断了一堆的内容,这里有对 JVM 版本的判断,以及其他的一些判断。我们假定最终返回了默认的 Platform
,他的 getStackTraceForClosable
方法如下:
open fun getStackTraceForCloseable(closer: String): Any? {
return when {
logger.isLoggable(Level.FINE) -> Throwable(closer) // These are expensive to allocate.
else -> null
}
}
判断为 log 的级别为 FINE,返回一个异常。这里的 Log 级别是 Java 中的 log 级别。
Java 中的 log 级别分的很多,每一个级别都对应一个值,值越高级别越大,从高到低依次是:
级别 | 描述 |
---|---|
OFF | 关 |
SEVERE | 最高级别的日志,输出一些导致程序不能正常运行的信息 |
WARNING | 警告 |
INFO | 输出一些比较重要的信息 |
CONFIG | 输出一些配置信息,例如 CPU 类型等 |
FINE | 根据信息的日志级别 |
FINER | 根据信息的日志级别 |
FINEST | 根据信息的日志级别 |
ALL | 全部输出 |
所以在 JVM 平台 callStackTrace
可能是 Throwable("response.body().close()")
或者 null。
callStart
方法接下来的一行是 eventListener.callStart(this)
。
EventListener
EventListener
是 OKHttp 中的事件监听器,监听了请求链路中的所有事件。我们可以通过 OKHttp.Build 来配置 EventListener
。配置的方式有两种:
eventListenerFactory
eventListener
一个是配置一个 EventListener.Factory
,一个是配置 EventListener
。
/**
- Configure a single client scoped listener that will receive all analytic events for this
- client.
*
- @see EventListener for semantics and restrictions on listener implementations.
*/
fun eventListener(eventListener: EventListener) = apply {
this.eventListenerFactory = eventListener.asFactory()
}
/**
- Configure a factory to provide per-call scoped listeners that will receive analytic events
- for this client.
*
- @see EventListener for semantics and restrictions on listener implementations.
*/
fun eventListenerFactory(eventListenerFactory: EventListener.Factory) = apply {
this.eventListenerFactory = eventListenerFactory
}
如果我们配置的是 EventListener
那么会在方法内部通过 asFactory()
方法转换成一个工厂对象。方法如下:
fun EventListener.asFactory() = EventListener.Factory { this }
工厂的方法如下:
fun interface Factory {
/**
- Creates an instance of the [EventListener] for a particular [Call]. The returned
- [EventListener] instance will be used during the lifecycle of [call].
*
- This method is invoked after [call] is created. See [OkHttpClient.newCall].
*
- **It is an error for implementations to issue any mutating operations on the [call] instance
- from this method.**
*/
fun create(call: Call): EventListener
}
asFactory 方法是 Kotlin 的一个扩展方法,后面的 EventListener.Factory { this } 是通过 lambda 创建的一个工厂对象,每次调用这个工厂对象的 create 方法都会返回 this,也就是我们传入的 EventListener 对象。
所以,如果我们调用 eventListener
方法,那么后续通过该配置创建的 OKHttpClient 对象产生的请求都使用的是同一个 EventListener
实例。
而 **eventListenerFactory**
方法不同,需要我们传入一个工厂对象,在这个工厂对象的 **create**
方法中可以定义自已创建 **EventListener**
的逻辑。
每一个 RealCall 被创建处理的时候都会调用一次 eventListenerFactory
的 create
方法。如下:
internal val eventListener: EventListener = client.eventListenerFactory.create(this)
callStart 是一个请求的第一个方法,从名字也可以看出来。
Dispatcher 任务分发
Dispatcher 是 OKHttp 中进行任务分发的一个类。我们来介绍一些 Dispatcher 类中重要属性:
-
readyAsyncCalls
: 准备好执行的 call。 -
runningAsyncCalls
: 正在运行的 call。
internal fun enqueue(call: AsyncCall) {
synchronized(this) {
// 将 call 添加到 readyAsyncCalls 中
readyAsyncCalls.add(call)
// Mutate the AsyncCall so that it shares the AtomicInteger of an existing running call to
// the same host.
if (!call.call.forWebSocket) {
// 查找拥有相同 host 的 call
val existingCall = findExistingCallWithHost(call.host)
if (existingCall != null) call.reuseCallsPerHostFrom(existingCall)
}
}
promoteAndExecute()
}
上面这个方法会将需要执行的 call 添加到准备队列中,并且会在 readyAsyncCalls
和 runningAsyncCalls
中查找能够复用的 call。
private fun findExistingCallWithHost(host: String): AsyncCall? {
for (existingCall in runningAsyncCalls) {
if (existingCall.host == host) return existingCall
}
for (existingCall in readyAsyncCalls) {
if (existingCall.host == host) return existingCall
}
return null
}
上面的方法是从 readyAsyncCalls
和 runningAsyncCalls
中查找于给定 host 相同的 call。
@Volatile var callsPerHost = AtomicInteger(0)
private set
fun reuseCallsPerHostFrom(other: AsyncCall) {
this.callsPerHost = other.callsPerHost
}
这里的 reuseCallsPerHostFrom 可不是指复用 call,而是一个限流的作用,callPerHost 表示当前的 host 一共有多少的 call,也就是有多少个请求。
private fun promoteAndExecute(): Boolean {
this.assertThreadDoesntHoldLock()
val executableCalls = mutableListOf()
val isRunning: Boolean
synchronized(this) {
val i = readyAsyncCalls.iterator()
while (i.hasNext()) {
val asyncCall = i.next()
// 如果 runningAsyncCalls 的大小已经达到了最大的请求数量
if (runningAsyncCalls.size >= this.maxRequests) break // Max capacity.
// 这里就是前面提到的限制同一个 host 同时存在太多的请求
if (asyncCall.callsPerHost.get() >= this.maxRequestsPerHost) continue // Host max capacity.
i.remove()
// callsPerhost + 1
asyncCall.callsPerHost.incrementAndGet()
// 添加到正在执行的队列中
executableCalls.add(asyncCall)
runningAsyncCalls.add(asyncCall)
}
isRunning = runningCallsCount() > 0
}
for (i in 0 until executableCalls.size) {
val asyncCall = executableCalls[i]
// 在 executorService 上执行 call
asyncCall.executeOn(executorService)
}
return isRunning
}
这个方法会将符合条件的 call 从 readyAsyncCalls
迁移到 runningAsyncCalls
,并执行这些 Call。promoteAndExecute
方法的返回值表示当前是否还有任务正在运行。 Call 的执行事通过 Call 的 executeOn
方法,executorService
是 OKhttp 的线程池。
@get:Synchronized
@get:JvmName("executorService") val executorService: ExecutorService
get() {
if (executorServiceOrNull == null) {
// 如果没有指定线程池,会创建一个默认的线程池
executorServiceOrNull = ThreadPoolExecutor(0, Int.MAX_VALUE, 60, TimeUnit.SECONDS,
SynchronousQueue(), threadFactory("$okHttpName Dispatcher", false))
}
return executorServiceOrNull!!
}
可以看到如果我们没有指定线程池,则会创建一个默认的线程池。
fun executeOn(executorService: ExecutorService) {
client.dispatcher.assertThreadDoesntHoldLock()
var success = false
try {
executorService.execute(this)
success = true
} catch (e: RejectedExecutionException) {
val ioException = InterruptedIOException("executor rejected")
ioException.initCause(e)
noMoreExchanges(ioException)
responseCallback.onFailure(this@RealCall, ioException)
} finally {
if (!success) {
client.dispatcher.finished(this) // This call is no longer running!
}
}
}
上面代码中执行网络请求的代码是 executorService.execute(this)
。也就是说 RealCall 是继承自 Runnable 的,网络请求的核心代码在 run 方法中。下面我们来看看 run 方法。
override fun run() {
// threadName 方法会修改当前线程的名称,并执行 block 块中的代码
threadName("OkHttp ${redactedUrl()}") {
var signalledCallback = false
// 开始检测是否超时
timeout.enter()
try {
// 获取 response
val response = getResponseWithInterceptorChain()
signalledCallback = true
responseCallback.onResponse(this@RealCall, response)
} catch (e: IOException) {
// 这里是防止有两次 callback
if (signalledCallback) {
// Do not signal the callback twice!
Platform.get().log("Callback failure for ${toLoggableString()}", Platform.INFO, e)
} else {
// 有 IOException 表示请求失败
responseCallback.onFailure(this@RealCall, e)
}
} catch (t: Throwable) {
cancel()
if (!signalledCallback) {
val canceledException = IOException("canceled due to $t")
canceledException.addSuppressed(t)
responseCallback.onFailure(this@RealCall, canceledException)
}
throw t
} finally {
// dispatcher 调用当前 call 执行完毕
client.dispatcher.finished(this)
}
}
}
}
可以看到在 RealCall 的 run 方法中执行了真正的网络请求,并处理了请求的 callback。在执行完毕了调用了 dispatcher 的 finished 方法,表示当前 call 的执行结束了。
internal fun finished(call: AsyncCall) {
call.callsPerHost.decrementAndGet()
finished(runningAsyncCalls, call)
}
private fun finished(calls: Deque, call: T) {
val idleCallback: Runnable?
synchronized(this) {
// 从 runningAsyncCalls 中删除当前 call
if (!calls.remove(call)) throw AssertionError("Call wasn't in-flight!")
idleCallback = this.idleCallback
}
// 如果当前还在运行
val isRunning = promoteAndExecute()
// 如果当前没有任务运行了,那么调用空闲的一个回调
if (!isRunning && idleCallback != null) {
idleCallback.run()
}
}
Interceptor
接下来我们来看 getResponseWithInterceptorChain
方法:
@Throws(IOException::class)
internal fun getResponseWithInterceptorChain(): Response {
// Build a full stack of interceptors.
val interceptors = mutableListOf()
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)
if (isCanceled()) {
response.closeQuietly()
throw IOException("Canceled")
}
return response
} catch (e: IOException) {
calledNoMoreExchanges = true
throw noMoreExchanges(e) as Throwable
} finally {
if (!calledNoMoreExchanges) {
noMoreExchanges(null)
}
}
}
拦截器链是 OKHttp 的一个核心,我们首先来看看 Interceptor 和 Chain 的接口定义:
fun interface Interceptor {
@Throws(IOException::class)
fun intercept(chain: Chain): Response
companion object {
/**
* Constructs an interceptor for a lambda. This compact syntax is most useful for inline
* interceptors.
*
* ```
* val interceptor = Interceptor { chain: Interceptor.Chain ->
* chain.proceed(chain.request())
* }
* ```
*/
inline operator fun invoke(crossinline block: (chain: Chain) -> Response): Interceptor =
Interceptor { block(it) }
}
interface Chain {
fun request(): Request
@Throws(IOException::class)
fun proceed(request: Request): Response
/**
* Returns the connection the request will be executed on. This is only available in the chains
* of network interceptors; for application interceptors this is always null.
*/
fun connection(): Connection?
fun call(): Call
fun connectTimeoutMillis(): Int
fun withConnectTimeout(timeout: Int, unit: TimeUnit): Chain
fun readTimeoutMillis(): Int
fun withReadTimeout(timeout: Int, unit: TimeUnit): Chain
fun writeTimeoutMillis(): Int
fun withWriteTimeout(timeout: Int, unit: TimeUnit): Chain
}
}
Interceptor
接口中的 intercept
方法接收一个 Chain
返回一个 Response
。
这里首先要理解拦截器这种设计模式。(如果不懂,可以去网上搜一些文章先看看)
在 OKHttp.Build 中提供了添加和获取 interceptor 的方法。
fun addInterceptor(interceptor: Interceptor) = apply {
interceptors += interceptor
}
fun addNetworkInterceptor(interceptor: Interceptor) = apply {
networkInterceptors += interceptor
}
// ...
可以看到 interceptor 被分为了两类:
- interceptor
- networkInterceptor
从上面的代码中可以看出,在拦截器链中首先添加的就是 OKHttpClient 的 interceptors,之后又添加了 RetryAndFollowUpInterceptor
、BridgeInterceptor
、CacheInterceptor
以及 ConnectInterceptor
之后才会添加 OKHttpClient 的 networkInterceptors
。所以点一个区别就是添加的顺序。添加的顺序不同,导致 networkInterceptors 中的 Interceptor 能获取到更多的信息。
第二个不同是在 websocket 请求时是不会将 networkInterceptors 添加到拦截器链中的。
在下面的代码中构造了 RealInterceptorChain
实例,并调用了 chain.proceed
方法。
@Throws(IOException::class)
override fun proceed(request: Request): Response {
check(index < interceptors.size)
calls++
if (exchange != null) {
check(exchange.finder.sameHostAndPort(request.url)) {
"network interceptor ${interceptors[index - 1]} must retain the same host and port"
}
check(calls == 1) {
"network interceptor ${interceptors[index - 1]} must call proceed() exactly once"
}
}
// Call the next interceptor in the chain.
val next = copy(index = index + 1, request = request)
val interceptor = interceptors[index]
@Suppress("USELESS_ELVIS")
val response = interceptor.intercept(next) ?: throw NullPointerException(
"interceptor $interceptor returned null")
if (exchange != null) {
check(index + 1 >= interceptors.size || next.calls == 1) {
"network interceptor $interceptor must call proceed() exactly once"
}
}
check(response.body != null) { "interceptor $interceptor returned a response with no body" }
return response
}
Chain 类像一个链条一样用来组织这些 Interceptor。RealInterceptorChain 中有一个 index
参数表示当前执行的第几个过滤器。在拦截器链的开始输出 Request ,最终返回一个 Response。每个 Interceptor 的 intercept 会调用 Chain 的 process 方法来执行后面的过滤器。它的时序图如下图所示。
接下来我们逐个分析这些拦截器。
RetryAndFollowUpInterceptor
这个拦截器的主要作用是处理重试以及重定向。
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val realChain = chain as RealInterceptorChain
var request = chain.request
val call = realChain.call
var followUpCount = 0
var priorResponse: Response? = null
var newExchangeFinder = true
var recoveredFailures = listOf()
while (true) {
// 这个方法是为后续的处理创建一个连接寻找器
call.enterNetworkInterceptorExchange(request, newExchangeFinder)
var response: Response
var closeActiveExchange = true
try {
if (call.isCanceled()) {
throw IOException("Canceled")
}
try {
// 进行后续的拦截器处理
response = realChain.proceed(request)
newExchangeFinder = true
} catch (e: RouteException) {
// 连接一个路由失败了,请求还没有发送
// The attempt to connect via a route failed. The request will not have been sent.
if (!recover(e.lastConnectException, call, request, requestSendStarted = false)) {
throw e.firstConnectException.withSuppressed(recoveredFailures)
} else {
recoveredFailures += e.firstConnectException
}
// 不需要新的 ExchangeFinder 了
newExchangeFinder = false
// 重试
continue
} catch (e: IOException) {
// 与服务器的交流沟通失败了,请求已经发送了
// An attempt to communicate with a server failed. The request may have been sent.
if (!recover(e, call, request, requestSendStarted = e !is ConnectionShutdownException)) {
throw e.withSuppressed(recoveredFailures)
} else {
recoveredFailures += e
}
newExchangeFinder = false
continue
}
// 下面的这些代码是请求已经完成其他拦截器链,并且没有发生异常
// 如果之前没有创建一个 body 为空的 Response,在这里创建一个。
// Attach the prior response if it exists. Such responses never have a body.
if (priorResponse != null) {
response = response.newBuilder()
.priorResponse(priorResponse.newBuilder()
.body(null)
.build())
.build()
}
val exchange = call.interceptorScopedExchange
// followUpRequest 会进行一些异常处理和重定向
val followUp = followUpRequest(response, exchange)
// 如果 followUp 为空,说明不允许或者不需要重定向,直接返回 Response
if (followUp == null) {
if (exchange != null && exchange.isDuplex) {
call.timeoutEarlyExit()
}
closeActiveExchange = false
return response
}
val followUpBody = followUp.body
// isOneShot 如果仅允许一次重定向
if (followUpBody != null && followUpBody.isOneShot()) {
closeActiveExchange = false
return response
}
response.body?.closeQuietly()
// 重定向次数超过最大重定向次数,抛一个异常
if (++followUpCount > MAX_FOLLOW_UPS) {
throw ProtocolException("Too many follow-up requests: $followUpCount")
}
request = followUp
priorResponse = response
} finally {
// 释放 exchange
call.exitNetworkInterceptorExchange(closeActiveExchange)
}
}
}
该拦截器中有一个 while
死循环,以便当失败的时候进行重试。在开始的时候进行了一些数据准备,然后调用 proceed
进行后续的请求,如果这些拦截器抛出了异常会在 catch 中进行拦截,方便进行恢复和重试。
RouteException
当发生 RouteException,也就是连接路由失败了,然后会进行调用 recover
来进行恢复。此时的请求还没有发出去。
private fun recover(
e: IOException,
call: RealCall,
userRequest: Request,
requestSendStarted: Boolean
): Boolean {
// The application layer has forbidden retries.
if (!client.retryOnConnectionFailure) return false
// We can't send the request body again.
if (requestSendStarted && requestIsOneShot(e, userRequest)) return false
// This exception is fatal.
if (!isRecoverable(e, requestSendStarted)) return false
// No more routes to attempt.
if (!call.retryAfterFailure()) return false
// For failure recovery, use the same route selector with a new connection.
return true
}
如果在创建 OKHttp 的时候设置了 retryOnConnectionFailure
为 false,那么表示将不允许重试,该值默认为 true。
private fun isRecoverable(e: IOException, requestSendStarted: Boolean): Boolean {
// If there was a protocol problem, don't recover.
if (e is ProtocolException) {
return false
}
// If there was an interruption don't recover, but if there was a timeout connecting to a route
// we should try the next route (if there is one).
if (e is InterruptedIOException) {
return e is SocketTimeoutException && !requestSendStarted
}
// Look for known client-side or negotiation errors that are unlikely to be fixed by trying
// again with a different route.
if (e is SSLHandshakeException) {
// If the problem was a CertificateException from the X509TrustManager,
// do not retry.
if (e.cause is CertificateException) {
return false
}
}
if (e is SSLPeerUnverifiedException) {
// e.g. a certificate pinning error.
return false
}
// An example of one we might want to retry with a different route is a problem connecting to a
// proxy and would manifest as a standard IOException. Unless it is one we know we should not
// retry, we return true and try a new route.
return true
}
当然不是所有的异常情况都能恢复,如果发生了下面的这些异常时是不能恢复的:
- ProtocolException
- InterruptedIOException,IO 中断异常。当然如果请求没有开始并且是 SocketTimeoutException,也就是 Socket 连接超时了是可以进行恢复的。
- SSLHandshakeException 并且是因为 CertificateException(证书异常) 导致的
- SSLPeerUnverifiedException 证书过期或者失效
call.retryAfterFailure()
用来判断是否还有更多的路由,如果没有了就不进行重试了。
IOException
IOException 和 RouteException 类似,也都是调用了 recover
来进行恢复,区别是是如果发生了 IOException 表示请求已经发送出去了。
重定向
val followUp = followUpRequest(response, exchange)
if (followUp == null) {
if (exchange != null && exchange.isDuplex) {
call.timeoutEarlyExit()
}
closeActiveExchange = false
return response
}
val followUpBody = followUp.body
if (followUpBody != null && followUpBody.isOneShot()) {
closeActiveExchange = false
return response
}
response.body?.closeQuietly()
if (++followUpCount > MAX_FOLLOW_UPS) {
throw ProtocolException("Too many follow-up requests: $followUpCount")
}
request = followUp
priorResponse = response
调用 followUpRequest
来获取重定向的 Request,如果返回 null,表示不支持或者不需要重定向。
如果重定向请求的 body 不为空并且 isOneShot
返回 true,直接返回 response
。
这里的 isOneShot
是什么呢?
/**
* Returns true if this body expects at most one call to [writeTo] and can be transmitted
* at most once. This is typically used when writing the request body is destructive and it is not
* possible to recreate the request body after it has been sent.
*
* This method returns false unless it is overridden by a subclass.
*
* By default OkHttp will attempt to retransmit request bodies when the original request fails
* due to any of:
*
* * A stale connection. The request was made on a reused connection and that reused connection
* has since been closed by the server.
* * A client timeout (HTTP 408).
* * A authorization challenge (HTTP 401 and 407) that is satisfied by the [Authenticator].
* * A retryable server failure (HTTP 503 with a `Retry-After: 0` response header).
* * A misdirected request (HTTP 421) on a coalesced connection.
*/
open fun isOneShot(): Boolean = false
如果请求的 body 最多只能进行一次 writeTo
的调用并且最多可以传输一次,则返回 true。因为在有些情况下,调用 writeTo
方法后会调用请求体的数据被破环。例如输入流。
从源码中可以看到,下面的这个子类返回的是 true。
/** Returns a new request body that transmits this. */
@JvmStatic
@JvmName("create")
fun FileDescriptor.toRequestBody(contentType: MediaType? = null): RequestBody {
return object : RequestBody() {
override fun contentType() = contentType
override fun isOneShot(): Boolean = true
override fun writeTo(sink: BufferedSink) {
FileInputStream(this@toRequestBody).use {
sink.buffer.writeAll(it.source())
}
}
}
}
上面的代码是从文件创建一个 RequestBody。当该 RequestBody 调用 writeTo 方法的时候,文件流一个被读取了,读取完成后这个流就不可以再次被读取了,所以这里的 isOneShot
方法返回 true。
在默认情况下 isOneShot
返回的是 false,当请求以如下的原因失败的时候,OkHttp 将尝试重新传输请求体。
- 旧的连接。请求在一个重用的连接上发出的,该重用的连接已被服务器关闭。
- 客户端超时(HTTP 408)
- 一个授权请求(HTTP 401 和 407),并且
Authenticator
能够满足授权。 - 可重试的服务器故障(HTTP 503 并且有
Retry-After: 0
的响应头) - HTTP 421,这个是OKHttp4.x以后新加的,即使域名不同,OkHttp也可以合并HTTP/2连接,如果服务器返回了421,会进行重试。
接下来我们看看 followUpRequest
方法:
@Throws(IOException::class)
private fun followUpRequest(userResponse: Response, exchange: Exchange?): Request? {
val route = exchange?.connection?.route()
val responseCode = userResponse.code
val method = userResponse.request.method
when (responseCode) {
HTTP_PROXY_AUTH -> {
val selectedProxy = route!!.proxy
if (selectedProxy.type() != Proxy.Type.HTTP) {
throw ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy")
}
return client.proxyAuthenticator.authenticate(route, userResponse)
}
HTTP_UNAUTHORIZED -> return client.authenticator.authenticate(route, userResponse)
HTTP_PERM_REDIRECT, HTTP_TEMP_REDIRECT, HTTP_MULT_CHOICE, HTTP_MOVED_PERM, HTTP_MOVED_TEMP, HTTP_SEE_OTHER -> {
return buildRedirectRequest(userResponse, method)
}
HTTP_CLIENT_TIMEOUT -> {
// 408's are rare in practice, but some servers like HAProxy use this response code. The
// spec says that we may repeat the request without modifications. Modern browsers also
// repeat the request (even non-idempotent ones.)
if (!client.retryOnConnectionFailure) {
// The application layer has directed us not to retry the request.
return null
}
val requestBody = userResponse.request.body
if (requestBody != null && requestBody.isOneShot()) {
return null
}
val priorResponse = userResponse.priorResponse
if (priorResponse != null && priorResponse.code == HTTP_CLIENT_TIMEOUT) {
// We attempted to retry and got another timeout. Give up.
return null
}
if (retryAfter(userResponse, 0) > 0) {
return null
}
return userResponse.request
}
HTTP_UNAVAILABLE -> {
val priorResponse = userResponse.priorResponse
if (priorResponse != null && priorResponse.code == HTTP_UNAVAILABLE) {
// We attempted to retry and got another timeout. Give up.
return null
}
if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
// specifically received an instruction to retry without delay
return userResponse.request
}
return null
}
HTTP_MISDIRECTED_REQUEST -> {
// OkHttp can coalesce HTTP/2 connections even if the domain names are different. See
// RealConnection.isEligible(). If we attempted this and the server returned HTTP 421, then
// we can retry on a different connection.
val requestBody = userResponse.request.body
if (requestBody != null && requestBody.isOneShot()) {
return null
}
if (exchange == null || !exchange.isCoalescedConnection) {
return null
}
exchange.connection.noCoalescedConnections()
return userResponse.request
}
else -> return null
}
}
HTTP_CLIENT_TIMEOUT
(408):响应状态码**408 Request Timeout**
表示服务器想要将没有在使用的连接关闭。一些服务器会在空闲连接上发送此信息,即便是在客户端没有发送任何请求的情况下。这种情况下会进行重试。client.retryOnConnectionFailure
如果设置了不允许重试,这里就直接返回了requestBody !=
_null_
&& requestBody.isOneShot()
上面分析过这种情况,重试不了。retryAfter(userResponse, 0) > 0
这里服务端通过Retry-After
字段来告诉客户端多久后可以重试。当然如果在服务端返回的响应头中没有获取到该字段,返回就返回默认值 0。如果返回的结果不为 0,也就是不能立即重试,这里就返回 null 了。HTTP_UNAVAILABLE
(503):服务不可用。HTTP_MISDIRECTED_REQUEST
(421):这个是OKHttp4.x以后新加的,即使域名不同,OkHttp也可以合并HTTP/2连接,如果服务器返回了421,会进行重试。HTTP_PROXY_AUTH
(407):客户端使用了代理服务器,并且在请求头中添加了Proxy Authentication Required
要求代理服务器进行授权重定向。HTTP_UNAUTHORIZED
(401):未授权,要求用户进行授权,授权完成后重定向。-
HTTP_PERM_REDIRECT
,HTTP_TEMP_REDIRECT
,HTTP_MULT_CHOICE
,HTTP_MOVED_PERM
,HTTP_MOVED_TEMP
,HTTP_SEE_OTHER
:-
HTTP_MULT_CHOICE
(300):表示该请求拥有多种可能的响应。用户代理或者用户自身应该从中选择一个。由于没有如何进行选择的标准方法,这个状态码极少使用。 -
HTTP_MOVED_PERM
(301):永久重定向,在浏览器接收到重定向之前的 url 时直接替换成缓存(假如已经请求过了并且有缓存)中的 url。 -
HTTP_SEE_OTHER
(303):HTTP 303 See Other 重定向状态码,通常作为PUT
或 POST 操作的返回结果,它表示重定向链接指向的不是新上传的资源,而是另外一个页面,比如消息确认页面或上传进度页面。而请求重定向页面的方法要总是使用[GET](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Methods/GET)
。 -
HTTP_MOVED_TEMP
(302):临时重定向,与永久重定向不同,临时重定向浏览器还会每次请求重定向之前的 url。 -
HTTP_TEMP_REDIRECT
(307):HTTP307 Temporary Redirect
,临时重定向响应状态码,表示请求的资源暂时地被移动到了响应的 Location 首部所指向的 URL 上。 -
HTTP_PERM_REDIRECT
(308):308与301定义一致,唯一的区别在于,308状态码不允许浏览器将原本为POST的请求重顶到GET请求上。
-
-
HTTP_CLIENT_TIMEOUT
(408):响应状态码408 Request Timeout
表示服务器想要将没有在使用的连接关闭。一些服务器会在空闲连接上发送此信息,即便是在客户端没有发送任何请求的情况下。这种情况下会进行重试。-
client.retryOnConnectionFailure
如果设置了不允许重试,这里就直接返回了 -
requestBody != null && requestBody.isOneShot()
上面分析过这种情况,重试不了。 -
retryAfter(userResponse, 0) > 0
这里服务端通过Retry-After
字段来告诉客户端多久后可以重试。当然如果在服务端返回的响应头中没有获取到该字段,返回就返回默认值 0。如果返回的结果不为 0,也就是不能立即重试,这里就返回 null 了。
-
HTTP_UNAVAILABLE
(503):服务不可用。HTTP_MISDIRECTED_REQUEST
(421):这个是OKHttp4.x以后新加的,即使域名不同,OkHttp也可以合并HTTP/2连接,如果服务器返回了421,会进行重试。
BridgeInterceptor
BridgeInterceptor
是把应用的一些代码转换成网络的代码。首先会从一个用户发起的请求构建出一个网络的请求。然后调用 proceed
方法进行其他的拦截器的处理。最终从网络的 Response
构建一个用户的 Response
。说白了就是把我们的一个设置转换成网络的设置。
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val userRequest = chain.request()
val requestBuilder = userRequest.newBuilder()
val body = userRequest.body
if (body != null) {
val contentType = body.contentType()
if (contentType != null) {
requestBuilder.header("Content-Type", contentType.toString())
}
val contentLength = body.contentLength()
if (contentLength != -1L) {
requestBuilder.header("Content-Length", contentLength.toString())
requestBuilder.removeHeader("Transfer-Encoding")
} else {
requestBuilder.header("Transfer-Encoding", "chunked")
requestBuilder.removeHeader("Content-Length")
}
}
if (userRequest.header("Host") == null) {
requestBuilder.header("Host", userRequest.url.toHostHeader())
}
if (userRequest.header("Connection") == null) {
requestBuilder.header("Connection", "Keep-Alive")
}
// If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
// the transfer stream.
var transparentGzip = false
if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
transparentGzip = true
requestBuilder.header("Accept-Encoding", "gzip")
}
// 处理 cookie
val cookies = cookieJar.loadForRequest(userRequest.url)
if (cookies.isNotEmpty()) {
requestBuilder.header("Cookie", cookieHeader(cookies))
}
if (userRequest.header("User-Agent") == null) {
requestBuilder.header("User-Agent", userAgent)
}
val networkResponse = chain.proceed(requestBuilder.build())
cookieJar.receiveHeaders(userRequest.url, networkResponse.headers)
val responseBuilder = networkResponse.newBuilder()
.request(userRequest)
if (transparentGzip &&
"gzip".equals(networkResponse.header("Content-Encoding"), ignoreCase = true) &&
networkResponse.promisesBody()) {
val responseBody = networkResponse.body
if (responseBody != null) {
val gzipSource = GzipSource(responseBody.source())
val strippedHeaders = networkResponse.headers.newBuilder()
.removeAll("Content-Encoding")
.removeAll("Content-Length")
.build()
responseBuilder.headers(strippedHeaders)
val contentType = networkResponse.header("Content-Type")
responseBuilder.body(RealResponseBody(contentType, -1L, gzipSource.buffer()))
}
}
return responseBuilder.build()
}
这里会把我们设置的一些属性转换成 Request Header。
Content-Type 这个请求头表示的是内容的类型。在 OkHttp 中它对应的是 MediaType 类型。如果我们想要发送一个 JSON 请求,就需要设置 MediaType 。
下面演示一下如何使用 OkHttp 发送一个 json 请求。首先我们使用 ktor 简单写一个 echo 接口。
fun main() {
val server = embeddedServer(Netty, port = 8080) {
install(ContentNegotiation) {
routing {
post("/echo") {
val post = call.receiveText()
call.respondText(
contentType = ContentType.parse("application/json"),
text = post
)
}
}
}
}
server.start(wait = true)
}
fun main() {
val okHttpClient = OkHttpClient()
val data = """
{
"Hello": "hello"
}
""".trimIndent()
val requestBody = data.toRequestBody("Application/json".toMediaType())
val request = Request.Builder()
.url("http://localhost:8080/echo")
.post(requestBody)
.build()
val response = okHttpClient.newCall(request).execute()
println(response.body?.string())
}
Content-Length
表示请求体的长度。
Transfer-Encoding: chunked
表示请求的长度不能确定。
Host
主机。
Connection
长连接。
Accept-Encoding
表示客户端可以接受的压缩格式。如果没有设置这里指定 gzip。
Range
表示请求部分资源。
Cookie
Cookie 。
OkHttp 中并没有缓存 Cookie,不过提供了 CookieJar 来把缓存 Cookie 的工作交给了使用方。
可以通过 OkHttp.Build 来设置 CookieJar 。
interface CookieJar {
/**
* Saves [cookies] from an HTTP response to this store according to this jar's policy.
*
* Note that this method may be called a second time for a single HTTP response if the response
* includes a trailer. For this obscure HTTP feature, [cookies] contains only the trailer's
* cookies.
*/
fun saveFromResponse(url: HttpUrl, cookies: List)
/**
* Load cookies from the jar for an HTTP request to [url]. This method returns a possibly
* empty list of cookies for the network request.
*
* Simple implementations will return the accepted cookies that have not yet expired and that
* [match][Cookie.matches] [url].
*/
fun loadForRequest(url: HttpUrl): List
companion object {
/** A cookie jar that never accepts any cookies. */
@JvmField
val NO_COOKIES: CookieJar = NoCookies()
private class NoCookies : CookieJar {
override fun saveFromResponse(url: HttpUrl, cookies: List) {
}
override fun loadForRequest(url: HttpUrl): List {
return emptyList()
}
}
}
}
CookieJar 提供了两个方法:
- saveFromResponse 保存响应头中的 Cookie。
- loadForRequest 为请求添加 Cookie。
// 载入 Cookie
val cookies = cookieJar.loadForRequest(userRequest.url)
if (cookies.isNotEmpty()) {
requestBuilder.header("Cookie", cookieHeader(cookies))
}
if (userRequest.header("User-Agent") == null) {
requestBuilder.header("User-Agent", userAgent)
}
val networkResponse = chain.proceed(requestBuilder.build())
// 保存 Cookie
cookieJar.receiveHeaders(userRequest.url, networkResponse.headers)
fun CookieJar.receiveHeaders(url: HttpUrl, headers: Headers) {
if (this === CookieJar.NO_COOKIES) return
val cookies = Cookie.parseAll(url, headers)
if (cookies.isEmpty()) return
saveFromResponse(url, cookies)
}
OKHttpClient 中默认的 Cookie 是 CookieJar 的伴生对象中的 NO_COOKIES 。所以默认是不会对 Cookie 进行保存的,会直接丢弃掉。
User-Agent 可以理解为告诉服务器发起请求的客户端是什么。如果没有添加,OkHttp 的默认为:
const val userAgent = "okhttp/${OkHttp.VERSION}
接下来调用了 val networkResponse = chain.proceed(requestBuilder.build()) 处理其他的拦截器了,当其他拦截器处理完毕后,构建返回的 Response。
val responseBuilder = networkResponse.newBuilder()
.request(userRequest)
// 如果请求头中的 Accept-Encoding 是 gzip 并且返回的 Content-Encoding 也是 gzip
// 并且有响应体,那么就调用 gzip 进行解压
if (transparentGzip &&
"gzip".equals(networkResponse.header("Content-Encoding"), ignoreCase = true) &&
networkResponse.promisesBody()) {
val responseBody = networkResponse.body
if (responseBody != null) {
// GzipSource 包装了 responseBody.source
val gzipSource = GzipSource(responseBody.source())
val strippedHeaders = networkResponse.headers.newBuilder()
.removeAll("Content-Encoding")
.removeAll("Content-Length")
.build()
responseBuilder.headers(strippedHeaders)
val contentType = networkResponse.header("Content-Type")
// 重新构造了正真的 ResponseBody
responseBuilder.body(RealResponseBody(contentType, -1L, gzipSource.buffer()))
}
}
/**
* Returns true if the response headers and status indicate that this response has a (possibly
* 0-length) body. See RFC 7231.
*/
fun Response.promisesBody(): Boolean {
// HEAD requests never yield a body regardless of the response headers.
if (request.method == "HEAD") {
return false
}
val responseCode = code
if ((responseCode < HTTP_CONTINUE || responseCode >= 200) &&
responseCode != HTTP_NO_CONTENT &&
responseCode != HTTP_NOT_MODIFIED) {
return true
}
// If the Content-Length or Transfer-Encoding headers disagree with the response code, the
// response is malformed. For best compatibility, we honor the headers.
if (headersContentLength() != -1L ||
"chunked".equals(header("Transfer-Encoding"), ignoreCase = true)) {
return true
}
return false
}
在有些情况下是没有响应体的:
- HEAD 当请求的 medthod 为 HEAD 的时候是没有响应体的。
- HTTP status 204: HTTP 204 No Content 成功状态响应码,表示该请求已经成功了,但是客户端客户不需要离开当前页面。默认情况下 204 响应是可缓存的。一个 ETag 标头包含在此类响应中。
- HTTP status 304: HTTP 304 未改变说明无需再次传输请求的内容,也就是说可以使用缓存的内容。
这里当 Content-Length 为 -1 或者 Transfer-Encoding: chunked 仅仅表示长度未知,不能说明没有响应体。
CacheInterceptor
未完待续...
ConnectInterceptor
处理连接的 Interceptor。
/**
* Opens a connection to the target server and proceeds to the next interceptor. The network might
* be used for the returned response, or to validate a cached response with a conditional GET.
*/
object ConnectInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val realChain = chain as RealInterceptorChain
val exchange = realChain.call.initExchange(chain)
val connectedChain = realChain.copy(exchange = exchange)
return connectedChain.proceed(realChain.request)
}
}