Ktor网络框架源码分析 - 设计篇

1,406 阅读10分钟

背景

公司最近在封装 KMP 能力的基础库,并计划使用 Ktor 取代传统的 RetrofitOkhttp 进行网络请求,所以提前对 Ktor了解了一下,特此梳理一下。

本篇文章暂不说明网络引擎过程(点火阶段),我们先了解一下它的请求前准备阶段

什么是 Ktor

Ktor 全称是「Kotlin HTTP Client and Web Framework Library」, 由 JetBrains 开发的一个用 Kotlin 编写的异步 Web 框架。它旨在简化构建连接应用程序的过程,支持创建服务器端和客户端应用程序。Ktor 提供了一种灵活且可扩展的方法来处理 HTTP 请求和响应,适用于现代 Web 应用程序的需求。

KMP的概念类似,JetBrains希望采用Kotlin的高易用性能力实现多平台快速搭建 C/S能力,提高通用性,高灵活性,并减少开发学习成本。

网络引擎对照表

image.png

Ktor项目结构

image.png
模块名角色
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 请求的类。它提供了配置请求的方法和属性,如 URLHTTP 方法、请求头、请求体等。

  • 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}]"
}

代码量不多,核心是将HttpRequestBuilderHttpClient关联起来,打通请求流程。 当执行 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
}

这么做的目的是:

  1. 正确处理协程取消:

• 在协程中,取消操作通常会抛出 CancellationException。这种异常表示协程被取消,但并不总是表示错误情况。

• 有时,CancellationException 可能包含一个实际的异常原因,这个原因是更有意义的错误信息。

• 使用 unwrapCancellationException 可以提取出这个实际的异常原因,使得异常处理更加准确和有意义。

  1. 保留有用的异常信息:

• 直接抛出 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 种所谓的管道分别是:

  1. HttpRequestPipeline:

    • 含义与作用: 处理客户端发送的 HTTP 请求。在请求发送前进行各种准备和修改,例如添加公共头部、身份验证、修改请求 URL 等。
    • 阶段:
      • Before: 请求处理之前的阶段。
      • State: 使用此阶段可以修改具有共享状态的请求。
      • Transform: 将请求正文转换为支持的呈现格式。
      • Render: 将请求正文编码为 OutgoingContent。
      • After: 请求处理之后的阶段。
  2. HttpSendPipeline:

    • 含义与作用: 处理实际发送请求的过程,负责将已经准备好的请求数据发送到目标服务器。
    • 阶段:
      • Before: 发送请求之前的阶段。
      • State: 发送请求的状态阶段。
      • Monitoring: 发送请求的监控阶段。
      • Engine: 使用引擎发送请求的阶段。
      • Receive: 接收管道执行阶段。
  3. HttpResponsePipeline:

    • 含义与作用: 处理从服务器接收到的原始 HTTP 响应数据,包括检查响应状态码、解析响应头部和初步处理响应体数据。
    • 阶段:
      • Receive: 接收响应的阶段。
      • Parse: 解析响应数据的阶段。
      • Transform: 将响应body转换为预期格式。
      • State: 使用此阶段来存储请求共享状态。
      • After: 最新响应管道阶段
  4. 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 模块下。

image.png

首先我们看一下 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,而入参泛型 TContextTSubject的理解为:

  • 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 将拦截器执行按照一定的顺序执行,因此针对这个流程我画了一个流程图供大家理解。

whiteboard_exported_image.png

串联梳理

结合上述流程图,我们梳理一下整体的请求链路。

@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 网络引擎分析