iT邦幫忙

2023 iThome 鐵人賽

DAY 7
0

今天我們來看 call.respondText() 後段,也就是 respond(message) 怎麼將訊息傳遞出去。

respond(message) 實作如下

/**
 * Sends a [message] as a response.
 * @see [io.ktor.server.response.ApplicationResponse]
 */
@OptIn(InternalAPI::class)
@JvmName("respondWithType")
public suspend inline fun <reified T : Any> ApplicationCall.respond(message: T) {
    if (message !is OutgoingContent && message !is ByteArray) {
        response.responseType = typeInfo<T>()
    }
    response.pipeline.execute(this, message as Any)
}

這段的重點是 response.pipeline.execute(this, message as Any),我們來看看其實作

/**
 * Executes this pipeline in the given [context] and with the given [subject]
 */
public suspend fun execute(context: TContext, subject: TSubject): TSubject =
	createContext(context, subject, coroutineContext).execute(subject)

接著追 createContext

@Suppress("DEPRECATION")
private fun createContext(
	context: TContext,
	subject: TSubject,
	coroutineContext: CoroutineContext
): PipelineContext<TSubject, TContext> =
	pipelineContextFor(context, sharedInterceptorsList(), subject, coroutineContext, developmentMode)

到這邊,首先讀者們可能會注意到:已經不只一次的出現了僅用單一一行,就定義完一個函數的程式架構。

個人認為這件事情在 Kotlin 程式語言內,是被鼓勵進行的。Kotlin 甚至專門為這種形式的函數設計了用等號就可以不使用大括號定義的語法。這種寫法的好處是,鼓勵一個函數能做的事更加簡潔單純,而不是在一個函數內塞入大量的邏輯。

我們來看看 pipelineContextFor() 的實作

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

我們這邊走的是 SuspendFunctionGun,來看看 SuspendFunctionGun.execute() 的實作

override suspend fun execute(initial: TSubject): TSubject {
	index = 0
	if (index == blocks.size) return initial
	subject = initial

	if (lastSuspensionIndex >= 0) throw IllegalStateException("Already started")

	return proceed()
}

proceed()

override suspend fun proceed(): TSubject = suspendCoroutineUninterceptedOrReturn { continuation ->
	if (index == blocks.size) return@suspendCoroutineUninterceptedOrReturn subject

	addContinuation(continuation.intercepted())

	if (loop(true)) {
		discardLastRootContinuation()
		return@suspendCoroutineUninterceptedOrReturn subject
	}

	COROUTINE_SUSPENDED
}

suspendCoroutineUninterceptedOrReturn() 是一個 Kotlin 內建的函數

@SinceKotlin("1.3")
@InlineOnly
@Suppress("UNUSED_PARAMETER", "RedundantSuspendModifier")
public suspend inline fun <T> suspendCoroutineUninterceptedOrReturn(crossinline block: (Continuation<T>) -> Any?): T {
    contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
    throw NotImplementedError("Implementation of suspendCoroutineUninterceptedOrReturn is intrinsic")
}

這邊我們暫時先知道他會運作輸入的匿名函數就好。

接著我們看匿名函數,匿名函數內的實作則是

if (index == blocks.size) return@suspendCoroutineUninterceptedOrReturn subject

addContinuation(continuation.intercepted())

if (loop(true)) {
	discardLastRootContinuation()
	return@suspendCoroutineUninterceptedOrReturn subject
}

COROUTINE_SUSPENDED

這邊有一個寫法很值得看看,就是 return@suspendCoroutineUninterceptedOrReturn subject 這個標記法。看起來很直觀,但是不太容易想到可以這樣寫。

private fun addContinuation(continuation: Continuation<TSubject>) {
	suspensions[++lastSuspensionIndex] = continuation
}
private fun discardLastRootContinuation() {
	if (lastSuspensionIndex < 0) throw IllegalStateException("No more continuations to resume")
	suspensions[lastSuspensionIndex--] = null
}

幾段函數參考起來看,我們可以發現,這其實只是很常見的分段處理。

到這邊,我們終於將整件事情串起來了:

response.pipeline.execute(this, message as Any) 這邊的功用,其實就將我們的回傳內容,一段一段的處理好之後,最後往外回傳。並且回傳時,我們會特地標記為 COROUTINE_SUSPENDED

/**
 * This value is used as a return value of [suspendCoroutineUninterceptedOrReturn] `block` argument to state that
 * the execution was suspended and will not return any result immediately.
 *
 * **Note: this value should not be used in general code.** Using it outside of the context of
 * `suspendCoroutineUninterceptedOrReturn` function return value  (including, but not limited to,
 * storing this value in other properties, returning it from other functions, etc)
 * can lead to unspecified behavior of the code.
 */
// It is implemented as property with getter to avoid ProGuard <clinit> problem with multifile IntrinsicsKt class
@SinceKotlin("1.3")
public val COROUTINE_SUSPENDED: Any get() = CoroutineSingletons.COROUTINE_SUSPENDED

也就是說,proceed() 這段函數,並不會立刻處理完畢後回傳,而是會暫時被掛起(suspend)。

想想,這樣的設計也很合理,畢竟發送回傳這件事情的完成與否,取決於網路的 I/O 速度。如果程式一定要等網路傳輸完畢,才能繼續處理其他的事物,這確實有一點浪費效能。

到今天,我們總算看過了

routing {
	get("/") {
		call.respondText("Hello World!")
	}
}

這幾行程式背後的所有實作。不知道各位有沒有一點收穫?

明天我們來看 Ktor 框架還提供了什麼有趣的工具,來讓大家更快速的撰寫後端服務,以及這些工具是怎麼實作的。


上一篇
Day 06:處理回傳的內容,call.respondText() 前段
下一篇
Day 08:用 staticFiles() 處理靜態檔案
系列文
深入解析 Kotlin 專案 Ktor 的程式碼,探索 Ktor 的強大功能30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言