第一天開始,首先我們來一起看看 Ktor 是怎麼開始一個 server 服務的。
要開始研究這個事情,首先我們要先看看 Ktor 框架內的程式碼怎麼撰寫。
我們透過 https://start.ktor.io/ 下載一個 Ktor 專案之後,可以看到預設的程式碼如下
fun main() {
embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module)
.start(wait = true)
}
今天我們先釐清一個問題:Ktor 這段程式碼,是怎麼開啟一個 server 服務的?
我們往下追 embeddedServer
的實作如下
@OptIn(DelicateCoroutinesApi::class)
public fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configuration>
embeddedServer(
factory: ApplicationEngineFactory<TEngine, TConfiguration>,
port: Int = 80,
host: String = "0.0.0.0",
watchPaths: List<String> = listOf(WORKING_DIRECTORY_PATH),
configure: TConfiguration.() -> Unit = {},
module: Application.() -> Unit
): TEngine = GlobalScope.embeddedServer(factory, port, host, watchPaths, EmptyCoroutineContext, configure, module)
這邊直接在 GlobalScope
這個 Kotlin 的 CoroutineScope
下面,呼叫了 embeddedServer()
,我們繼續往下追:
public fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configuration>
CoroutineScope.embeddedServer(
factory: ApplicationEngineFactory<TEngine, TConfiguration>,
vararg connectors: EngineConnectorConfig = arrayOf(EngineConnectorBuilder()),
watchPaths: List<String> = listOf(WORKING_DIRECTORY_PATH),
parentCoroutineContext: CoroutineContext = EmptyCoroutineContext,
configure: TConfiguration.() -> Unit = {},
module: Application.() -> Unit
): TEngine {
val environment = applicationEngineEnvironment {
this.parentCoroutineContext = coroutineContext + parentCoroutineContext
this.log = KtorSimpleLogger("ktor.application")
this.watchPaths = watchPaths
this.module(module)
this.connectors.addAll(connectors)
}
return embeddedServer(factory, environment, configure)
}
這邊可以看到,這個對 CoroutineScope 所定義的延伸函數(extension function)只做了一件事情:設置 server 所需要的環境,包含 logger、connectors 等等。剩下的部分直接交給了對應的 factory
進行處理。
再往下追,我們會看到
public fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configuration> embeddedServer(
factory: ApplicationEngineFactory<TEngine, TConfiguration>,
environment: ApplicationEngineEnvironment,
configure: TConfiguration.() -> Unit = {}
): TEngine {
return factory.create(environment, configure)
}
也就是說,如果要找到實際產生 server 的程式碼,我們就要看 embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module)
這段裡面的 Netty
是怎麼實作 create()
這個函數的。
我們開始看 Netty
的實作,這是一個 Kotlin 的 object:
public object Netty : ApplicationEngineFactory<NettyApplicationEngine, NettyApplicationEngine.Configuration> {
override fun create(
environment: ApplicationEngineEnvironment,
configure: NettyApplicationEngine.Configuration.() -> Unit
): NettyApplicationEngine {
return NettyApplicationEngine(environment, configure)
}
}
到這邊,我們終於抽絲剝繭,挖出了最終會拿到的物件類別:NettyApplicationEngine
。
另外我們在這邊也看到了一個設計模式:工廠模式。
透過定義 ApplicationEngineFactory
這個介面,我們就可以簡單地切換送入 embeddedServer 函式的參數,只要我們傳送進去的 Factory
,最終能 create()
出 ApplicationEngine
,我們就可以透過切換 Factory,來取得不同的 ApplicationEngine
。
以這裡來說,我們拿到的會是一個 NettyApplicationEngine
,
/**
* [ApplicationEngine] implementation for running in a standalone Netty
*/
public class NettyApplicationEngine(
environment: ApplicationEngineEnvironment,
configure: Configuration.() -> Unit = {}
) : BaseApplicationEngine(environment)
到這邊,我們終於看到了運作一個獨立 Netty
的部分了!
我們來一起看看他對 start()
的實作,這段比較多,我們分段落來看:
override fun start(wait: Boolean): NettyApplicationEngine {
addShutdownHook {
stop(configuration.shutdownGracePeriod, configuration.shutdownTimeout)
}
environment.start()
前面加上了一個 addShutdownHook
,從命名和註解,我們可以得知是加上了 JVM shutdown 時該做的事情:
/**
* Adds automatic JVM shutdown hooks management. Should be used **before** starting the engine.
* Once JVM termination noticed, [stop] block will be executed.
* Please note that a shutdown hook only registered when the application is running. If the application
* is already stopped then there will be no hook and no [stop] function invocation possible.
* So [stop] block will be called once or never.
*/
之後呼叫了 environment.start()
,我們看看 environment.start()
上面的註解
/**
* Starts [ApplicationEngineEnvironment] and creates an application.
*/
public fun start()
這個函數的註解,就很有意思了!原來 environment.start()
除了啟動 ApplicationEngineEnvironment,還會在這邊建立 application。
(p.s. 所以不要再說,不需要註解了!!)
建立 application 之後,看到下面這段
try {
channels = bootstraps.zip(environment.connectors)
.map { it.first.bind(it.second.host, it.second.port) }
.map { it.sync().channel() }
val connectors = channels!!.zip(environment.connectors)
.map { it.second.withPort(it.first.localAddress().port) }
resolvedConnectors.complete(connectors)
} catch (cause: BindException) {
terminate()
throw cause
}
這段的寫法是利用了 Kotlin Collection 的寫法,很簡短地將 bootstraps
和 environment.connectors
進行綁定,如果綁定失敗就拋出 BindException
。
最後,我們加上一些監控和錯誤處理的部分,start()
就結束了
environment.monitor.raiseCatching(ServerReady, environment, environment.log)
cancellationDeferred =
stopServerOnCancellation(configuration.shutdownGracePeriod, configuration.shutdownTimeout)
if (wait) {
channels?.map { it.closeFuture() }?.forEach { it.sync() }
stop(configuration.shutdownGracePeriod, configuration.shutdownTimeout)
}
return this
}
到這邊,我們就看完了 Ktor 針對 embeddedServer()
的實作、知道了 Netty
的產出,也看了 NettyApplicationEngine
下 start()
的實作。
今天我們就先到這邊為止,各位明天見!