前言
OkHttp是一个开源网络库,封装了http协议的一整套协议,并且被引人到安卓源码中,所以要学习网络相关的知识,绕不开OkHttp,今天我们就开始完整的读一下OkHttp的源码,以对其整体实现有一个充分认识。
一个常规的OKhttp请求是这样的,创建出OkHttpClient,然后创建request,设置url(示例代码为get请求),再通过client.newCall得到一个Call对象(RealCall),通过调用enqueue异步进行网络请求(同步请求execute),今天我们就以示例代码为入口来读源码。
vbscript
OkHttpClient client = new OkHttpClient().newBuilder()
.build();
Request request = new Request.Builder()
.url("https://www.jnnews.tv/guanzhu/p/2023-11/25/1016488.html")
.build();
Call call = client.newCall(request);
//异步请求
call.enqueue(new Callback());
//同步请求
Response execute = call.execute();
enqueue方法
为了控制字数,有些简单的源码就绕过了,我们直接从enqueue方法跟随断点调试入手。首先要知道newCall得到的Call是一个接口,它的唯一实现是RealCall,所以进入RealCall的enqueue方法。
kotlin
override fun enqueue(responseCallback: Callback) {
//client.dispatcher为转发器,负责转发请求,它有一个默认的实现类Dispatcher
//new一个AsyncCall,将responseCallback传入,以AsyncCall为参数执行enqueue
client.dispatcher.enqueue(AsyncCall(responseCallback))
}
readyAsyncCalls是一个ArrayDeque类型的队列,将本次的call添加进去,然后试图寻找一个已经存在的和本次请求host相同的call,如果有,则记录下同一个host的请求次数,记录到RealCall中的callsPerHost变量上(OkHttp对同一域名下能同时进行的请求数量有控制)。
kotlin
internal fun enqueue(call: AsyncCall) {
synchronized(this) {
readyAsyncCalls.add(call)
//http请求为true,
if (!call.call.forWebSocket) {
//寻找已经开始请求或者等待开始请求的具有相同host的call对象,这里可以认为一定不会返回空,
//因为上一步刚刚讲新创建的call加入readyAsyncCalls队列。
val existingCall = findExistingCallWithHost(call.host)
if (existingCall != null) call.reuseCallsPerHostFrom(existingCall)
}
}
//准备开始执行
promoteAndExecute()
}
进入promoteAndExecute要准备开始执行请求了,执行请求之前有一些控制条件,首先我们要了解,OKhttp内部有三个队列(runningSyncCalls我们先不关心了,属于同步请求的逻辑),默认情况下,能同时进行的异步请求最大数量为64,所以runningAsyncCalls的最大容量为64,超过这个数量则会加入到readyAsyncCalls队列进行等待,另外同一个host的请求同时最大不能超过5个。
java
//准备好可以执行的异步请求队列,存放未进行的异步任务
private val readyAsyncCalls = ArrayDeque<AsyncCall>()
//正在执行的异步请求队列,存放正在执行的异步任务
private val runningAsyncCalls = ArrayDeque<AsyncCall>()
//正在执行的同步请求队列,存放正在执行的同步任务
private val runningSyncCalls = ArrayDeque<RealCall>()
所以在promoteAndExecute方法中。
kotlin
private fun promoteAndExecute(): Boolean {
val executableCalls = mutableListOf<AsyncCall>()
synchronized(this) {
//遍历所有准备好的异步请求
val i = readyAsyncCalls.iterator()
while (i.hasNext()) {
val asyncCall = i.next()
//如果正在进行的请求数量大于64,则跳出循环,本次不请求
if (runningAsyncCalls.size >= this.maxRequests) break
//当前请求域名正在请求的数量是否超过5,如果是,跳过这个请求
if (asyncCall.callsPerHost.get() >= this.maxRequestsPerHost) continue
//走到这了,说明满足请求的条件
i.remove()
//callsPerHost自增1,表示当前host正在请求的数量增加了1
asyncCall.callsPerHost.incrementAndGet()
//加入list
executableCalls.add(asyncCall)
//加入runningAsyncCalls队列,记录正在请求的call,请求完成后会从这里移除
runningAsyncCalls.add(asyncCall)
}
}
for (i in 0 until executableCalls.size) {
val asyncCall = executableCalls[i]
//executableCalls中记录的是本次可以执行的call,所以遍历一遍,统统丢入线程池执行
asyncCall.executeOn(executorService)
}
}
看下asyncCall的executeOn方法,这里提交到线程池执行后,会在finally中判断是否成功,如果提交失败(这里失败的几率极低,finish方法还有其他执行的场景),会执行finished方法,前边我们分析时,提到过加入最大请求数超过64或者同一host请求数超过5,是不会立即执行新的请求,而是加入了等待执行队列中,那么等待执行队列中的请求是什么时候开始执行的,就跟finished方法有关,我们看一下它的实现。
kotlin
fun executeOn(executorService: ExecutorService) {
var success = false
try {
//在线程池中执行
executorService.execute(this)
success = true
} catch (e: RejectedExecutionException) {
//这里表示提交到线程池失败,执行了线程池的拒绝策略,通常不会走到这里,因为okhttp的线程池是一个
//可以认为是无限容量的,所以这里这样做主要是为了代码的逻辑正常
responseCallback.onFailure(this@RealCall, ioException)
} finally {
if (!success) {
client.dispatcher.finished(this)
}
}
}
finished方法顾名思义就是一个请求完成后执行的逻辑,不管这个请求是成功了还是失败了,都要去执行,前边executeOn中执行的时机是提交到线程池失败后执行。
kotlin
internal fun finished(call: AsyncCall) {
//将host下正在进行的请求数量-1
call.callsPerHost.decrementAndGet()
finished(runningAsyncCalls, call)
}
private fun <T> finished(calls: Deque<T>, call: T) {
//idleCallback默认为空,有点类似于安卓中消息队列中的idlehandler,用于在请求空闲的时候执行任务
//可以通过Dispatcher的setIdleCallback方法设置
val idleCallback: Runnable?
synchronized(this) {
//将当前请求从runningAsyncCalls中移除
if (!calls.remove(call)) throw AssertionError("Call wasn't in-flight!")
idleCallback = this.idleCallback
}
//从runningAsyncCalls中移除一个请求之后,正在请求的数量就会小于64了(没有其他线程添加的情况下)
//此时再次尝试从准备队列中拿到等待的请求去执行,promoteAndExecute方法我们前边已经看过了
val isRunning = promoteAndExecute()
//没有任何请求在执行的时候才会返回isRunning为false,所以这个时候请求一定是空闲的,执行idleCallback
if (!isRunning && idleCallback != null) {
idleCallback.run()
}
}
回到执行中的任务,任务被提交到线程池,被提交的任务指的就是AsyncCall,它实现了Runnable接口,看下它的run方法,这里就进入了okhttp的五大拦截器流程中,也是okhttp的核心流程,这些源码会在后边的文章中单独进行分析。
less
override fun run() {
threadName("OkHttp ${redactedUrl()}") {
var signalledCallback = false
//超时逻辑
timeout.enter()
try {
//这是okhttp的主线,通过5大拦截器处理网络请求,最后返回结果
val response = getResponseWithInterceptorChain()
signalledCallback = true
//正常响应后回调成功
responseCallback.onResponse(this@RealCall, response)
} catch (e: IOException) {
responseCallback.onFailure(this@RealCall, e)
} catch (t: Throwable) {
responseCallback.onFailure(this@RealCall, canceledException)
} finally {
//本次请求不管成功还是失败,都执行一次finished,尝试启动等待中的请求
client.dispatcher.finished(this)
}
}
}
execute
异步请求看完了,顺便也看一眼同步请求,同步请求的逻辑很简单,和异步请求也有重合的地方,他们最终都是通过五大拦截器执行请求。
kotlin
override fun execute(): Response {
//超时逻辑
timeout.enter()
callStart()
try {
//看下执行逻辑,将当前请求添加到同步请求队列runningSyncCalls
client.dispatcher.executed(this)
//五大拦截器进行网络请求
return getResponseWithInterceptorChain()
} finally {
//执行完成,调用finished将当前请求从runningSyncCalls队列中移除(同步请求也会被加入队列)
//执行完成后移除,所以runningSyncCalls中始终都只有一条记录,另外你会发现,同步请求执行完成
//之后也调用到了promoteAndExecute方法,这个方法只会检查等待中的异步请求,如果有满足条件的
//就执行它们
client.dispatcher.finished(this)
}
}
@Synchronized internal fun executed(call: RealCall) {
runningSyncCalls.add(call)
}
总结
今天我们分析了okhttp的异步请求和同步请求的执行逻辑,异步请求有最大请求数的限制,最大64个线程同时执行,这是考虑了客户端硬件能力的发挥,我们知道无限的开启线程并不能提高系统的执行效率,这也算是对客户端系统资源的一种合理利用,另外针对同一个域名下,同时只能有5个请求,是对服务端的一种保护,控制同一时刻请求服务端的数量。后边我们会进行五大拦截器的分析,这才是okhttp的重中之重,也是难点,包含的知识点很多,更有利于我们去学习网络编程,敬请期待。