背景
公司最近在封装 KMP
能力的基础库,并计划使用 Ktor
取代传统的 Retrofit
和 Okhttp
进行网络请求,所以提前对 Ktor
了解了一下,特此梳理一下。
本篇文章暂不说明网络引擎过程(点火阶段),我们先了解一下它的请求前准备阶段。
什么是 Ktor
Ktor 全称是「Kotlin HTTP Client and Web Framework Library」, 由 JetBrains
开发的一个用 Kotlin
编写的异步 Web
框架。它旨在简化构建连接应用程序的过程,支持创建服务器端和客户端应用程序。Ktor
提供了一种灵活且可扩展的方法来处理 HTTP
请求和响应,适用于现代 Web
应用程序的需求。
像 KMP
的概念类似,JetBrains
希望采用Kotlin
的高易用性能力实现多平台快速搭建 C/S
能力,提高通用性,高灵活性,并减少开发学习成本。
网络引擎对照表
Ktor项目结构
模块名 | 角色 |
---|---|
ktor-bom | 管理项目中所有模块的版本,确保所有 Ktor 依赖项使用相同的版本,简化依赖管理。 |
ktor-client | 包含 Ktor 客户端相关功能,用于构建 HTTP 客户端应用程序,支持异步和同步请求。 |
ktor-http | 处理 HTTP 协议的核心功能,包括请求和响应的格式化、HTTP 方法、状态码、头部等。 |
ktor-io | 提供输入/输出操作的功能,处理网络数据传输,高效的 I/O 操作支持。 |
ktor-network | 提供底层的网络通信功能,包括连接管理、数据传输、协议处理等,是网络功能的核心模块。 |
ktor-server | 包含 Ktor 服务器相关功能,用于构建 HTTP 服务器应用程序,支持异步处理和多种服务器配置。 |
ktor-shared | 包含共享代码和通用功能,被多个模块使用,提供通用工具和辅助功能,促进代码重用。 |
ktor-utils | 提供各种实用工具和辅助功能,简化 Ktor 应用程序开发,包括日志记录、配置管理、数据处理等。 |
在阅读 Ktor
的代码时,由于内部有很多平台的实现,阅读起来比较麻烦,我们可以单独在一个项目中依赖某一个平台的 maven
,然后看到底哪些 moudle
被一并依赖进来,此时对包依赖关系就比较清晰了。
Android platform
使用方式
implementation("io.ktor:ktor-client-android:3.0.0-beta-1")
Get
请求
class MainActivity : ComponentActivity() {
private val client: HttpClient by lazy {
HttpClient(Android) {
engine {
connectTimeout = 100_000
socketTimeout = 100_000
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
client.sendPipeline.intercept(HttpSendPipeline.Before) { context ->
println("Edisonli: intercept")
// proceedWith(context)
}
GlobalScope.launch(Dispatchers.IO) {
kotlin.runCatching {
val response = client.get("https://www.juejin.cn")
Log.d("MainActivity", "Response: ${response.body<String>()}")
}.onFailure {
Log.e("MainActivity", "Error: ${it.message}", it)
}
}
简单几行的Kotlin协程
代码即可完成一次GET
请求,简洁舒服,同样也可以对请求的各个阶段进行拦截转发操作。
HttpClient
类在Ktor
框架中扮演着至关重要的角色,它负责在发起请求前整合各种必要的信息和配置。这个类不仅仅是一个简单的请求发起工具,而是包含了大量的Kotlin
协程高级用法,使得异步网络操作更加高效和优雅。我们暂时不深入探讨请求引擎的具体实现(例如在上述代码示例中使用的 Android
引擎), 先分析一下在真正执行请求前,Ktor
是怎么设计的。
如果是让我们设计一个网络框架,我们一般步骤是
graph TD
构建请求报文体 --> 配置连接池 --> 处理请求和响应 --> 解析响应数据 --> 错误处理
builders.kt
分析一次请求的流程,我们直接从请求入口出发,先梳理出大致轮廓,之后再完善细节。
/**
* Executes an [HttpClient]'s GET request with the specified [url] and
* an optional [block] receiving an [HttpRequestBuilder] for configuring the request.
*
* Learn more from [Making requests](https://ktor.io/docs/request.html).
*/
public suspend inline fun HttpClient.get(
urlString: String,
block: HttpRequestBuilder.() -> Unit = {}
): HttpResponse = get { url(urlString); block() }
/**
* Executes an [HttpClient]'s GET request with the parameters configured in [block].
*
* Learn more from [Making requests](https://ktor.io/docs/request.html).
*/
public suspend inline fun HttpClient.get(block: HttpRequestBuilder.() -> Unit): HttpResponse =
get(HttpRequestBuilder().apply(block))
/**
* Executes an [HttpClient]'s GET request with the parameters configured in [builder].
*
* Learn more from [Making requests](https://ktor.io/docs/request.html).
*/
public suspend inline fun HttpClient.get(builder: HttpRequestBuilder): HttpResponse {
builder.method = HttpMethod.Get
return request(builder)
}
/**
* Executes an [HttpClient]'s request with the parameters specified using [builder].
*
* Learn more from [Making requests](https://ktor.io/docs/request.html).
*/
public suspend inline fun HttpClient.request(
builder: HttpRequestBuilder = HttpRequestBuilder()
): HttpResponse = HttpStatement(builder, this).execute()
上述代码非常整洁与清爽,
使用内联函数配合建造者模式,首先,将 url
设置给 HttpRequestBuilder
,通过扩展方法
作为参数串联调用,最终将HttpClient
& HttpRequestBuilder
对象通过构造方法一步一步的传递给HttpStatement
对象,然后执行 execute()
方法,启动了一次网络请求。
这种设计让每个方法单一职责,并且结合 Kotlin 高级语法特性,使得函数阅读起来非常的清爽,并且每一个函数都采用了 inline
内联提高性能。
经过上述的一系列操作,我们可以知道在构建请求时,有三大核心类:
-
HttpClient
负责管理和执行
HTTP
请求。它提供了一个统一的接口来发送HTTP
请求和接收HTTP
响应。 -
HttpRequestBuilder
构建
HTTP
请求的类。它提供了配置请求的方法和属性,如URL
、HTTP
方法、请求头、请求体等。 -
HttpStatement
准备好的
HTTP
请求,可以多次执行以获取响应。它封装了请求的构建和执行逻辑,但不会立即执行请求(execute()
被调用前),允许在实际发送请求之前进行更多的配置或拦截。
HttpStatement
/**
* 准备好的 HTTP 客户端请求的声明。
* 该声明不会执行任何网络请求,直到 [execute] 方法被调用。
* [HttpStatement] 可以安全地多次执行。
*
* 示例:[Streaming data](https://ktor.io/docs/response.html#streaming)
*/
public class HttpStatement(
private val builder: HttpRequestBuilder,
internal val client: HttpClient
) {
/**
* 执行这个声明并下载响应。
* 在方法执行完成后,客户端会下载响应体到内存中,并释放连接。
*
* 要接收确切类型,请考虑使用 [body<T>()] 方法。
*/
public suspend fun execute(): HttpResponse = fetchResponse()
@OptIn(InternalAPI::class)
public suspend inline fun <reified T, R> body(crossinline block: suspend (response: T) -> R): R = unwrapRequestTimeoutException {
val response = fetchStreamingResponse()
try {
val result = response.body<T>()
// 获取响应体的类型为 T 的结果
return block(result)
// 执行传入的块并返回其结果
} finally {
response.cleanup()
// 最终清理响应
}
}
/**
* 返回带有打开流体的 [HttpResponse]。
*/
@PublishedApi
internal suspend fun fetchStreamingResponse(): HttpResponse = unwrapRequestTimeoutException {
val builder = HttpRequestBuilder().takeFromWithExecutionContext(builder)
// 跳过保存请求体
builder.skipSavingBody()
// 使用 client 执行请求
val call = client.execute(builder)
// 返回响应
return call.response
}
/**
* 返回带有保存体的 [HttpResponse]。
*/
@PublishedApi
internal suspend fun fetchResponse(): HttpResponse = unwrapRequestTimeoutException {
// 从已有的 builder 创建一个新的 HttpRequestBuilder
val builder = HttpRequestBuilder().takeFromWithExecutionContext(builder)
// 使用 client 执行请求
val call = client.execute(builder)
// 保存请求结果
val result = call.save()
// 清理原始响应
call.response.cleanup()
// 返回保存后的响应
return result.response
}
override fun toString(): String = "HttpStatement[${builder.url}]"
}
代码量不多,核心是将HttpRequestBuilder
与 HttpClient
关联起来,打通请求流程。
当执行 execute()
方法时,这里包装了一份请求超时异常检测的逻辑——unwrapRequestTimeoutException
。
unwrapRequestTimeoutException
@PublishedApi
internal inline fun <T> unwrapRequestTimeoutException(block: () -> T): T {
try {
return block()
} catch (cause: CancellationException) {
throw cause.unwrapCancellationException()
}
}
/**
* If the exception contains cause that differs from [CancellationException] returns it otherwise returns itself.
*/
public expect fun Throwable.unwrapCancellationException(): Throwable
/**
* If the exception contains cause that differs from [CancellationException] returns it otherwise returns itself.
*/
public actual fun Throwable.unwrapCancellationException(): Throwable {
var exception: Throwable? = this
while (exception is CancellationException) {
// If there is a cycle, we return the initial exception.
if (exception == exception.cause) {
return this
}
exception = exception.cause
}
return exception ?: this
}
这么做的目的是:
- 正确处理协程取消:
• 在协程中,取消操作通常会抛出 CancellationException
。这种异常表示协程被取消,但并不总是表示错误情况。
• 有时,CancellationException
可能包含一个实际的异常原因,这个原因是更有意义的错误信息。
• 使用 unwrapCancellationException
可以提取出这个实际的异常原因,使得异常处理更加准确和有意义。
- 保留有用的异常信息:
• 直接抛出 CancellationException
可能会掩盖实际的错误原因。
• 通过提取并抛出实际的异常原因,可以更好地调试和处理错误。
expect 关键字代表此方法存在多平台的实现。
HttpClient
从 HttpClient
类中我们知道,发起请求是通过 HttpClient
调用 execute()
时触发的。
我们进入这个类看下。
构造与初始化代码块
先看一下它的构造方法与初始化代码块。
public class HttpClient(
public val engine: HttpClientEngine,
private val userConfig: HttpClientConfig<out HttpClientEngineConfig> = HttpClientConfig()
) : CoroutineScope, Closeable {
private var manageEngine: Boolean = false
internal constructor(
engine: HttpClientEngine,
userConfig: HttpClientConfig<out HttpClientEngineConfig>,
manageEngine: Boolean
) : this(engine, userConfig) {
this.manageEngine = manageEngine
}
// 原子boolean型变量。
private val closed = atomic(false)
// 使用 Job 作业去管控网络引擎的执行状态,可以通过 Job 去取消请求。
private val clientJob: CompletableJob = Job(engine.coroutineContext[Job])
// 引擎的协程上下文与Job关联。
public override val coroutineContext: CoroutineContext = engine.coroutineContext + clientJob
// 用于处理客户端发送的所有请求的管道
public val requestPipeline: HttpRequestPipeline = HttpRequestPipeline()
// 用于处理服务器发送的所有响应的管道
public val responsePipeline: HttpResponsePipeline = HttpResponsePipeline()
// 用于发送请求的管道
public val sendPipeline: HttpSendPipeline = HttpSendPipeline()
// 用于接收请求的管道
public val receivePipeline: HttpReceivePipeline = HttpReceivePipeline()
// 作为该客户端的轻量级容器的类型化属性
public val attributes: Attributes = Attributes(concurrent = true)
// 提供对客户端引擎配置的访问
public val engineConfig: HttpClientEngineConfig = engine.config
// 提供对客户端生命周期事件的访问,我们可以订阅这个事件去监控请求阶段。
// 内部逻辑是一个很简单的观察这模式的封装。
public val monitor: Events = Events()
// HttpClientConfig 内部包含了对网络引擎的配置组合。
// 例如 Ktor 提供了 Android HttpUrlConnection & OkHttp 的网络引擎,针对这两种引擎有各自的配置属性,可以让网络引擎自定义配置。
internal val config = HttpClientConfig<HttpClientEngineConfig>()
init {
if (manageEngine) {
clientJob.invokeOnCompletion {
if (it != null) {
// 如果 ClientJob事务结束,那么把网络引擎取消。
engine.cancel()
}
}
}
// 挂载网络引擎, 并将HttpClient对象传入。
engine.install(this)
// 对发送请求的管道中的接收步骤添加一个拦截器。
// 可以理解为当网络请求成功返回响应报文后时会回调。
sendPipeline.intercept(HttpSendPipeline.Receive) { call ->
check(call is HttpClientCall) { "Error: HttpClientCall expected, but found $call(${call::class})." }
// 主要是将流程转换给接收管道并执行拦截器。
val response = receivePipeline.execute(Unit, call.response)
call.setResponse(response)
proceedWith(call)
}
with(userConfig) {
// 安装插件
// 客户端的 HTTP 插件,在管道完全处理完毕时设置 HttpRequestBuilder. executionContext 并完成它。
config.install(HttpRequestLifecycle)
// 为上传和下载提供可观察进度的插件
config.install(BodyProgress)
// 保存响应体插件
config.install(SaveBodyPlugin)
if (useDefaultTransformers) {
// 默认转换器
config.install("DefaultTransformers") { defaultTransformers() }
}
// 暂且不表
config.install(HttpSend)
// 暂且不表
config.install(HttpCallValidator)
if (followRedirects) {
// 暂且不表
config.install(HttpRedirect)
}
config += this
if (useDefaultTransformers) {
config.install(HttpPlainText)
}
config.addDefaultResponseValidation()
// 将插件与 HttpClient 关联起来
config.install(this@HttpClient)
}
// 在响应管道的 Receive 阶段 注册一个拦截器
responsePipeline.intercept(HttpResponsePipeline.Receive) {
try {
proceed()
} catch (cause: Throwable) {
monitor.raise(HttpResponseReceiveFailed, HttpResponseReceiveFail(context.response, cause))
throw cause
}
}
}
}
如上代码涉及了对 HttpClient
的初始化进行了解释,对插件与自定义插件的实现原理我们暂且不表,先梳理网络请求执行流程。
在初始化中从成员变量中我们可以发现,在 Ktor
的网络请求世界中,存在 4 种所谓的管道分别是:
-
HttpRequestPipeline
:- 含义与作用: 处理客户端发送的 HTTP 请求。在请求发送前进行各种准备和修改,例如添加公共头部、身份验证、修改请求 URL 等。
- 阶段:
- Before: 请求处理之前的阶段。
- State: 使用此阶段可以修改具有共享状态的请求。
- Transform: 将请求正文转换为支持的呈现格式。
- Render: 将请求正文编码为 OutgoingContent。
- After: 请求处理之后的阶段。
-
HttpSendPipeline
:- 含义与作用: 处理实际发送请求的过程,负责将已经准备好的请求数据发送到目标服务器。
- 阶段:
- Before: 发送请求之前的阶段。
- State: 发送请求的状态阶段。
- Monitoring: 发送请求的监控阶段。
- Engine: 使用引擎发送请求的阶段。
- Receive: 接收管道执行阶段。
-
HttpResponsePipeline
:- 含义与作用: 处理从服务器接收到的原始 HTTP 响应数据,包括检查响应状态码、解析响应头部和初步处理响应体数据。
- 阶段:
- Receive: 接收响应的阶段。
- Parse: 解析响应数据的阶段。
- Transform: 将响应body转换为预期格式。
- State: 使用此阶段来存储请求共享状态。
- After: 最新响应管道阶段
-
HttpReceivePipeline
:- 含义与作用: 处理解析后的响应数据,将其转换为应用程序实际使用的格式,例如将 JSON 数据转换为对象,进一步处理响应体数据。
- 阶段:
- Before: 在任何其他阶段之前发生的最早阶。
- State: 使用此阶段来存储请求共享状态。
- After: 最新响应管道阶段。
如上,相比于在开发 android 时,使用的 Okhttp 不同在于,Okhttp 拦截器的事件流只有一种,而 Ktor 对网络请求步骤进行细致的拆解,将这个步骤用管道的概念切割,每一个管道有自己的「阶段」,每一个「阶段」都有自己的拦截器(或者自定义拦截器)。与 Okhttp 相同的是,所有的事件都是以责任链设计模式设计的,并且执行到最后一步的时候会把 Response 依次返回,形成一个环。
HttpClient#execute()
/**
* Creates a new [HttpClientCall] from a request [builder].
*/
internal suspend fun execute(builder: HttpRequestBuilder): HttpClientCall {
monitor.raise(HttpRequestCreated, builder)
return requestPipeline.execute(builder, builder.body) as HttpClientCall
}
这个 execute()
方法就是由之前 HttpStatement
类调用的,执行请求管道,说明这个是网络请求触发的入口。当然你也可以看到 monitor
触发了一个事件。
Pipeline
从现在开始我们可以进入Ktor
的“管道”的世界,我们先来看一下pipeline
的设计,pipeline
的实现被放到了 Ktor
中的 utils
模块下。
首先我们看一下 pipeline
的派生类如上图,其中我们作为Android
客户端,主要关心HttpReceivePipeline
HttpRequestPipeline
HttpResponsePipeline
HttpSendPipeline
EnginePipeline
即可。其他作为服务端的管道,都是相同的原理,我们暂且不表。
@Suppress("DEPRECATION")
// 1、泛型类 两个泛型在不同种类的管道中表示不同。
public open class Pipeline<TSubject : Any, TContext : Any>(
// 设置管道中的多阶段
vararg phases: PipelinePhase
) {
// 2、存储管道属性。本质上就是一个K-V数据结构,ConcurrentHashMap 或者 Hashmap.
public val attributes: Attributes = Attributes(concurrent = true)
public open val developmentMode: Boolean = false
// 将变长参数阶段展开依次保存。
private val phasesRaw: MutableList<Any> = mutableListOf(*phases)
// 阶段中拦截器数量 默认空
private var interceptorsQuantity = 0
// 遍历拿到管道中的所有的阶段
public val items: List<PipelinePhase>
get() = phasesRaw.map {
it as? PipelinePhase ?: (it as? PhaseContent<*, *>)?.phase!!
}
// 当前拦截器执行到某个阶段时 拦截器是否存在
public val isEmpty: Boolean
get() = interceptorsQuantity == 0
// 当前拦截器执行到某个阶段时 所有的拦截器
private val _interceptors: AtomicRef<List<PipelineInterceptorFunction<TSubject, TContext>>?> =
atomic(null)
// 当前拦截器执行到某个阶段时 所有的拦截器的set()/get()方法。
private var interceptors: List<PipelineInterceptorFunction<TSubject, TContext>>?
get() = _interceptors.value
set(value) {
_interceptors.value = value
}
private var interceptorsListShared: Boolean = false
private var interceptorsListSharedPhase: PipelinePhase? = null
// 构造方法传入某个阶段与这个阶段要执行的拦截器。
public constructor(
phase: PipelinePhase,
interceptors: List<PipelineInterceptor<TSubject, TContext>>
) : this(phase) {
interceptors.forEach { intercept(phase, it) }
}
/**
* 3.
* 当管道开始执行各个阶段前,会先执行这个方法。
* 也就是说这行这个方法时,开始执行当前管道的串行阶段队列。
*/
public suspend fun execute(context: TContext, subject: TSubject): TSubject {
return createContext(context, subject, coroutineContext).execute(subject)
}
// 4.
public fun intercept(phase: PipelinePhase, block: PipelineInterceptor<TSubject, TContext>) {
// 根据阶段找到对应的阶段包装类
val phaseContent = findPhase(phase)
?: throw InvalidPhaseException("Phase $phase was not registered for this pipeline")
// 将挂起方法转换成普通方法
val suspendBlock = block.toFunction()
if (tryAddToPhaseFastPath(phase, suspendBlock)) {
interceptorsQuantity++
return
}
phaseContent.addInterceptor(suspendBlock)
// 将计数器加 1
interceptorsQuantity++
// 重置
resetInterceptorsList()
// 空方法,用于 hook。
afterIntercepted()
}
// 省略一些管道与管道,拦截器与拦截器数据结构合并的算法逻辑,有兴趣可以自行解读一下。
// Tips:
// 很简单的合并算法,两个集合遍历过程,最坏的情况是 O(n^2)。
// 但这种情况只会在所有阶段都在第二个管道中相互插入之前发生(参见测试 testDependantPhasesLastCommon)。实际上,对于大多数情况,它将是线性时间。
}
/**
* Represents an interceptor type which is a suspend extension function for a context
*/
public typealias PipelineInterceptor<TSubject, TContext> =
suspend PipelineContext<TSubject, TContext>.(TSubject) -> Unit
关于管道的基类(准确点说应该是原始类),核心逻辑就是上述代码,主要是对管道与阶段,阶段与拦截器的一对多关系的映射。
上述代码我们可以看到execute()
函数是真正的开始执行管道中的各个阶段中要做的逻辑,需要注入管道的主题TSubject
与上下文TContext
,而入参泛型 TContext
与 TSubject
的理解为:
-
TContext
表示管道执行的上下文的对象。
-
TSubject
代表沿着管道前进的主题的对象,举例在触发真正的网络请求拦截器之前,这个
TSubject
代表RequestBuilder
对象,内部包装了请求报文。而请求之后就是ResponseBuilder
对象,也就是说管道执行过程中的主题是什么。
CreateContext
在执行 Pipeline.execute()
时,先创建了一个 PipelineContext
对象。
@Suppress("DEPRECATION")
private fun createContext(
context: TContext,
subject: TSubject,
coroutineContext: CoroutineContext
): PipelineContext<TSubject, TContext> =
pipelineContextFor(context, sharedInterceptorsList(), subject, coroutineContext, developmentMode)
/**
* Build a pipeline of the specified [interceptors] and create executor.
*/
internal fun <TSubject : Any, TContext : Any> pipelineContextFor(
context: TContext,
interceptors: List<PipelineInterceptorFunction<TSubject, TContext>>,
subject: TSubject,
coroutineContext: CoroutineContext,
debugMode: Boolean = false
): PipelineContext<TSubject, TContext> = if (DISABLE_SFG || debugMode) {
DebugPipelineContext(context, interceptors, subject, coroutineContext)
} else {
SuspendFunctionGun(subject, context, interceptors)
}
当处于 Debug 模式下会构造出DebugPipelineContext
类,这个类我们可以看到把「主题」「上下文」「拦截器集合」传入,可以猜到要实现对拦截器的逻辑。
PipelineContext
public abstract class PipelineContext<TSubject : Any, TContext : Any>(
public val context: TContext
) : CoroutineScope {
/**
* 这个管道执行的主体内容,沿着管道传递
*/
public abstract var subject: TSubject
/**
* 结束当前的管道执行
*/
public abstract fun finish()
/**
* 继续执行管道,使用给定的主体内容
*/
public abstract suspend fun proceedWith(subject: TSubject): TSubject
/**
* 继续执行管道,使用相同的主体内容
*/
public abstract suspend fun proceed(): TSubject
/**
* 内部方法,用于执行管道,使用初始的主体内容
*/
internal abstract suspend fun execute(initial: TSubject): TSubject
}
有没有特别像 OkHttp
内部拦截器的实现?其实本质上这个逻辑就是管道中单一phase
的拦截器实现。
DebugPipelineContext
package io.ktor.util.pipeline
import io.ktor.utils.io.*
import kotlin.coroutines.*
/**
* 代表管道的运行执行
* @param context 代表管道执行上下文的对象
* @param interceptors 要执行的拦截器列表
* @param subject 在管道中传递的主体对象
*/
@KtorDsl
internal class DebugPipelineContext<TSubject : Any, TContext : Any> constructor(
context: TContext,
private val interceptors: List<PipelineInterceptorFunction<TSubject, TContext>>,
subject: TSubject,
override val coroutineContext: CoroutineContext
) : PipelineContext<TSubject, TContext>(context) {
/**
* 当前管道执行的主体内容
*/
override var subject: TSubject = subject
private var index = 0
/**
* 结束当前的管道执行
*/
override fun finish() {
index = -1
}
/**
* 使用给定的主体继续执行管道
*/
override suspend fun proceedWith(subject: TSubject): TSubject {
this.subject = subject
return proceed()
}
/**
* 使用相同的主体继续执行管道
*/
override suspend fun proceed(): TSubject {
val index = index
if (index < 0) return subject
if (index >= interceptors.size) {
finish()
return subject
}
return proceedLoop()
}
/**
* 执行管道,使用初始的主体内容
*/
override suspend fun execute(initial: TSubject): TSubject {
index = 0
subject = initial
return proceed()
}
/**
* 内部方法,循环执行拦截器
*/
private suspend fun proceedLoop(): TSubject {
do {
// 使用循环遍历拦截器
val index = index
if (index == -1) {
break
}
val interceptors = interceptors
if (index >= interceptors.size) {
// 执行到最后一个拦截器就结束,会走到 上面的 -1 的逻辑。
finish()
break
}
// 根据当前的 index 拿到需要执行的拦截器
val executeInterceptor = interceptors[index]
this.index = index + 1
// 1. 直接调用拦截器中的逻辑。
executeInterceptor.toInterceptor().invoke(this, subject)
} while (true)
return subject
}
}
其中阅读这里时,executeInterceptor.toInterceptor().invoke(this, subject)
这里的逻辑有点不是很清晰。
详细说明一下这里,还记得我们在使用示例中自己添加了一个拦截器。
// 定义一个拦截器
val myInterceptor: PipelineInterceptor<Any, HttpRequestBuilder> = { subject ->
println("Processing subject: $subject")
proceed()
}
@Test
fun test() {
val client = HttpClient(Android) {
engine {
socketTimeout = 100_000
connectTimeout = 100_000
}
}
runBlocking {
client.sendPipeline.intercept(HttpSendPipeline.Before) { context ->
println("Edisonliang: intercept")
// proceedWith(context)
}
client.sendPipeline.intercept(HttpSendPipeline.Before, myInterceptor)
val response = client.get("https://www.baidu.com/") {
headers {
}
}
println("response: ${response.body<String>()}")
}
}
我们直接使用函数类型的尾随 Lambda 表达式
的形式有点看不清晰参数,上述的 demo 代码我把它拆解了一下。
executeInterceptor.toInterceptor().invoke(this, subject)
这个invoke
的执行就是上述代码的myInterceptor
函数的调用。
Phase 的作用
在分析 Pipeline
类的时候,我们讲述了一下 Phase
的作用就是对拦截器进行分组,并且按照一定组别的顺序进行执行组内的拦截器。
/**
* Represents a phase in a pipeline
*
* @param name a name for this phase
*/
public class PipelinePhase(public val name: String) {
override fun toString(): String = "Phase('$name')"
}
所以在定义拦截器的时候,也需要设定是在哪个 Phase
执行,相当于使用 Phase
将拦截器执行按照一定的顺序执行,因此针对这个流程我画了一个流程图供大家理解。
串联梳理
结合上述流程图,我们梳理一下整体的请求链路。
@Test
fun test() {
val client = HttpClient(Android) {
engine {
socketTimeout = 100_000
connectTimeout = 100_000
}
}
runBlocking {
client.requestPipeline.intercept(HttpRequestPipeline.Before){
proceed()
}
client.sendPipeline.intercept(HttpRequestPipeline.Before){
proceed()
}
client.receivePipeline.intercept(HttpRequestPipeline.Before){
proceed()
}
client.responsePipeline.intercept(HttpRequestPipeline.Before){
proceed()
}
val response = client.get("https://www.baidu.com/") {
headers {}
}
// ignore more phase...
println("response: ${response.body<String>()}")
}
}
我们使用Ktor
将网络请求阶段进行拆解,在不同阶段去做不同的事情,可以理解为 Ktor
就是 Retrofit
角色,使用这个方式,充分做到了事务逻辑的单一职责。
总结
Ktor 采用高度模块化的设计,开发者可以根据需求选择和组合不同的模块。这种设计提高了代码的可维护性,并且高度灵活性。
本篇文章对框架的整体请求流程进行了浅析,内部细节我们在下篇文章梳理,涉及到 Kotlin DSL异步方式
, Ktor插件执行流程
,CIO & OkHttp 网络引擎分析
。