iT邦幫忙

2023 iThome 鐵人賽

DAY 16
0

時間過了兩週,我們也看了不少 Ktor 可以提供的各種功能。

今天我們來看看 Ktor 框架所提供的另一大塊功能:針對 HTTP Client 開發上所提供的協助。

首先我們先安裝好 client 相關套件

implementation("io.ktor:ktor-client-core:$ktor_version")
implementation("io.ktor:ktor-client-cio:$ktor_version")

然後就可以在程式內撰寫存取第三方 API 的 Client,寫法如下

val client = HttpClient(CIO)
val response: HttpResponse = client.get("https://ktor.io/")
println(response.status)
client.close()

我們來看看 Ktor 是怎麼讓存取第三方 API 如此簡單的

首先,我們來看看 CIO

public object CIO : HttpClientEngineFactory<CIOEngineConfig> {
    init {
        addToLoader()
    }

    override fun create(block: CIOEngineConfig.() -> Unit): HttpClientEngine =
        CIOEngine(CIOEngineConfig().apply(block))

    override fun toString(): String = "CIO"
}

這是一個 Kotlin Object,類似其他語言的 Singleton Pattern

並且這個實作了 HttpClientEngineFactory

在這個物件內的 create() 裡面,則是做了一個 CIOEngine 回傳

HttpClient 則是

@KtorDsl
public fun <T : HttpClientEngineConfig> HttpClient(
    engineFactory: HttpClientEngineFactory<T>,
    block: HttpClientConfig<T>.() -> Unit = {}
): HttpClient {
    val config: HttpClientConfig<T> = HttpClientConfig<T>().apply(block)
    val engine = engineFactory.create(config.engineConfig)
    val client = HttpClient(engine, config, manageEngine = true)

    // If the engine was created using factory Ktor is responsible for its lifecycle management. Otherwise user has to
    // close engine by themself.
    client.coroutineContext[Job]!!.invokeOnCompletion {
        engine.close()
    }

    return client
}

看到這邊我們可以知道,這裡又是一個利用 Factory Pattern 的方式

我們在建立 HttpClient 時,如果想要抽換裡面的 Engine,只要把 CIO 這個 HttpClientEngineFactory 換掉即可。

經過一番設置之後,我們最後會拿到一個 HttpClient 物件

/**
 * A multiplatform asynchronous HTTP client, which allows you to make requests and handle responses,
 * extend its functionality with plugins, such as authentication, JSON serialization, and so on.
 *
 * You can learn how to create a configure [HttpClient] from
 * [Creating and configuring a client](https://ktor.io/docs/create-client.html).
 * @property engine [HttpClientEngine] used to execute network requests.
 */
@OptIn(InternalAPI::class)
public class HttpClient(
    public val engine: HttpClientEngine,
    private val userConfig: HttpClientConfig<out HttpClientEngineConfig> = HttpClientConfig()
) : CoroutineScope, Closeable 

取得這個物件之後,我們就可以透過 get 來試圖取得第三方 API 的內容

/**
 * 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() }
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)
}

這邊往下看 request()

/**
 * 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()

這邊會取得一個 HttpStatement 物件

/**
 * Prepared statement for a HTTP client request.
 * This statement doesn't perform any network requests until [execute] method call.
 * [HttpStatement] is safe to execute multiple times.
 *
 * Example: [Streaming data](https://ktor.io/docs/response.html#streaming)
 */
public class HttpStatement(
    private val builder: HttpRequestBuilder,
    @PublishedApi
    internal val client: HttpClient
) 

HttpStatement().execute() 則是

/**
 * Executes this statement and download the response.
 * After the method execution finishes, the client downloads the response body in memory and release the connection.
 *
 * To receive exact type, consider using [body<T>()] method.
 */
public suspend fun execute(): HttpResponse = execute {
	val savedCall = it.call.save()

	savedCall.response
}

這邊的 it.callHttpClientCall 物件

 /**
 * The associated [HttpClientCall] containing both
 * the underlying [HttpClientCall.request] and [HttpClientCall.response].
 */
public abstract val call: HttpClientCall

HttpClientCall.save 則是

/**
 * Fetch data for [HttpClientCall] and close the origin.
 */
@OptIn(InternalAPI::class)
public suspend fun HttpClientCall.save(): HttpClientCall {
    val responseBody = response.content.readRemaining().readBytes()

    return SavedHttpCall(client, request, response, responseBody)
}

到這邊,我們可以粗淺地看出這段邏輯的流向

框架先建立了一個 CIO 的 Singleton,然後建立出 HttpClient 物件

這個物件搭配 HttpRequestBuilder 協助建立 HttpRequest

就可以送出並取得回傳內容了

今天我們先掌握概略的邏輯,明天我們來細看中間的邏輯細節。


上一篇
Day 15:call.respond 如何轉換回傳內容
下一篇
Day 17:HttpResponse 的結構,以及 HttpResponse.body()
系列文
深入解析 Kotlin 專案 Ktor 的程式碼,探索 Ktor 的強大功能30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言