美文网首页
【Ktor入坑指南】全Kotlin编写的多平台异步网络框架 ——

【Ktor入坑指南】全Kotlin编写的多平台异步网络框架 ——

作者: _Jun | 来源:发表于2022-09-21 10:25 被阅读0次

    Ktor是一个在国外挺火的开源网络框架,它既是一个服务端框架、也能用作客户端。它背靠JetBrains,即出品IntelliJ IDEA的公司,同时Kotlin也是Jetbrains开发的,Ktor也算是背景非常雄厚了 。截止到写文章的时候,Ktor在github拥有10k的star。

    github.com/ktorio/ktor

    Ktor的特性可以先看看我的第一篇入坑文章。简而言之,它是全Kotlin的网络框架,使用协程在底层实现异步,具有可插拔插件的特性,扩展性非常强,本文就简单带大家入坑Ktor的客户端(Desktop、Android)。

    多平台客户端

    为了展现这个网络框架的多平台特性,我决定还是用一个例子来讲解,此处创建一个多平台项目。当然如果大家的项目是单平台的(Destop or Android),Android平台引入依赖方式见第一篇入坑文章,也可以在单平台上使用。

    进入common/build.gradle.kts加入Ktor依赖

    kotlin {
        ....
        sourceSets {
            val commonMain by getting {
                dependencies {
                    ...
                    // Coroutine
                    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
                    // Ktor-core
                    implementation("io.ktor:ktor-client-core:${extra["ktor.version"]}")
                    // Logging
                    implementation("io.ktor:ktor-client-logging:${extra["ktor.version"]}")
                    implementation("ch.qos.logback:logback-classic:1.2.11")
                    // Negotiation
                    implementation("io.ktor:ktor-client-content-negotiation:${extra["ktor.version"]}")
                    implementation("io.ktor:ktor-serialization-kotlinx-json:${extra["ktor.version"]}")
                }
            }
            val androidMain by getting {
                dependencies {
                    ...
                    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
                    implementation("io.ktor:ktor-client-okhttp:${extra["ktor.version"]}")
                }
            }
            val desktopMain by getting {
                dependencies {
                    ...
                    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.4")
                    implementation("io.ktor:ktor-client-cio:${extra["ktor.version"]}")
                }
            }
        }
    }
    

    在commonMain中加入四个依赖,分别是:

    • Kotlin协程核心组件

    • Ktor客户端核心库

    • Ktor打印插件

    • Ktor数据序列化插件 —— 本文引入Json可将Json转换成对应数据类

    在androidMain中加入两个依赖,分别是:

    • coroutines-android协程组件

    • Ktor的Okhttp客户端插件,好用的Okhttp!

      当然也可以使用android特有的。

      implementation("io.ktor:ktor-client-android:${extra["ktor.version"]}")
      

    在desktopMain加入两个依赖,分别是

    • coroutines-core-jvm协程组件

    • Ktor的CIO客户端插件,这里也可以引入Okhttp。

    为什么像Okhttp、CIO这些客户端插件需要在不同的平台上分别引用呢?

    由于核心库是通用的,而其他客户端插件是有平台特性的,例如ktor-android只能在Android平台上使用不能在Desktop平台上使用,所以需要在不同平台依赖。

    协程组件同上。

    创建客户端实例

    打开common/commonMain/kotlin/包名/platform.kt加入以下一行代码声明一个httpClient expect函数。该函数的作用简单来说类似于函数的接口,在通用代码库中声明,在不同的平台有不同的实现。

    expect fun getPlatformName(): String
    expect fun httpClient(config: HttpClientConfig<*>.() -> Unit = {}): HttpClient
    

    打开common/androidMain/kotlin/包名/platform.kt实现上面声明的httpClient函数。由于打开的Android平台的文件,因此使用上面依赖的OkHttp插件。(此处可以看到也实现了一个getPlatfromName函数,返回"Android")

    actual fun getPlatformName(): String {
        return "Android"
    }
    
    actual fun httpClient(config: HttpClientConfig<*>.() -> Unit) = HttpClient(OkHttp) {
        config(this)
    }
    

    打开common/desktopMain/kotlin/包名/platform.kt实现httpClient函数,使用CIO

    actual fun httpClient(config: HttpClientConfig<*>.() -> Unit) = HttpClient(CIO) {
        config(this)
    }
    

    到这里,多平台的代码适配就告一段落了!

    common/commonMain/kotlin/包名/创建一个network文件夹,并在network中创建一个HttpClient文件。声明一个httpClient

    val httpClient = httpClient {
        install(DefaultRequest) {
             url {
                 protocol = URLProtocol.HTTP  // HTTP协议
                 host = "192.168.31.229"      // 本地IP
                 port = 8088                  // 本地端口
             }
        }
        install(Logging) {
            level = LogLevel.ALL
        }
        install(HttpCookies) {
            storage = AcceptAllCookiesStorage()
        }
        install(ContentNegotiation) {
            json()
        }
    }
    

    设置插件的时候遵循DSL语法,并且每个插件都是可插拔的,介绍一下使用的插件:

    DefaultRequest

    是一个自动补充默认请求base URL的插件,对应Retrofit就是baseUrl方法。

    Retrofit.Builder().baseUrl(BASE_URL)
    

    它可以单独设置协议、host和port,可读性比较强。

    Logging

    是一个自动打印请求和响应的插件,非常好用。有5个等级可以选择:ALL、HEADER、BODY、INFO、NONE,根据名字可以知道对应的打印内容,为了方便展示,此处设置ALL登记。打印结果可以看下文。

    HttpCookies

    是一个存放Cookies的插件,默认使用AcceptAllCookiesStorage在内存存放服务端下发的Cookies,点进AcceptAllCookieStorage可以看到它默认接收所有Cookies,并且对发放了Cookies的网站使用同样的Cookies。

    不过建议自己实现CookiesStorage接口来存放Cookies,并使用自己的方式来持久化Cookies。

    ContentNegotiation

    数据序列化插件,类似于Retrofit的addConverterFactory,可以将数据序列化成对应数据类。

    Retrofit.Builder()
        .addConverterFactory(GsonConverterFactory.create()) //使用GSON转换库解析JSON数据
    

    发送Http请求

    创建一个CatSource object,先看一下最常用的Get、Post请求,HttpClient发起网络请求一般是挂起函数,在内部切换成子线程发起网络请求,因此它是异步的,在实际使用中直接调用函数即可,是非常方便的。

    Get

    @Serializable
    data class Cat(
        val name: String,
        val description: String,
        val imageUrl: String
    )
    
    object CatSource {
        suspend fun getRandomCat(): Result<Cat> {
            return runCatching {
                httpClient.get("random-cat").body()
            }
        }
    }
    

    使用HttpClient实例的get方法传入一个url字符串获取到一个HttpResponse实例,而通过该HttpResponse可以获取到服务端发回来的响应,包括call、HTTP状态码、HTTP版本、请求时间和响应时间等等。

    由于前面引入了默认请求的IP地址和端口,因此在请求中会自动拼接成一个完整的URL

    public abstract class HttpResponse : HttpMessage, CoroutineScope {
        // call中包含请求、响应报文,可以从中获取到参数信息、内容
        public abstract val call: HttpClientCall
        // HTTP状态码
        public abstract val status: HttpStatusCode
        // HTTP版本,通常为1.1和2.0
        public abstract val version: HttpProtocolVersion
        // 请求时间
        public abstract val requestTime: GMTDate
        // 响应时间
        public abstract val responseTime: GMTDate
    }
    

    而一般使用body()方法获取服务器响应内容,看一下body方法。

    public suspend inline fun <reified T> HttpResponse.body(): T = call.bodyNullable(typeInfo<T>()) as T
    

    其实只是通过从HttpResponse的call成员变量中获取响应内容,点进发现还有反序列化的逻辑,而这个反序列化则需要引入上方依赖的反序列化的插件。由于是inline函数并且通过<reified T>将泛型类型传递进去,因此不会丢失类型信息。

    因此在使用的时候只需要一行函数即可发起网络请求并转换成想要的数据类。

    网络请求和反序列化过程中,可能会抛出异常,因此在调用的时候需要try-catch

    Post

    @Serializable
    data class SpecialCatRequest(
        val number: Int
    )
    
    object CatSource {
        suspend fun postSpecialCat(number: Int): Result<Cat> {
            return runCatching {
                httpClient.post {
                    url("special-cat")
                    setBody(SpecialCatRequest(number = number))
                    contentType(ContentType.Application.Json)
                }.body()
            }
        }
    }
    
    

    通过HttpClient实例的post函数获取一个HttpResponse,这次并没有直接url参数进去,采用另外一个重载方法。而这个重载方法传入一个HttpRequestBuilder.() -> Unit参数,也就是说这个参数在HttpRequestBuilder作用域内,能够调用HttpRequestBuilder中的方法构建一个请求。在实际使用中则可以通过DSL范式来构建报文。

    • 调用url方法传入urlString,传入的值会拼接之前设置的IP地址和端口,组合成一个完整的URL

    • 调用setBody设置请求body内容,可以直接传入数据类,setBody方法是一个内联泛型实体化的函数,因此可以获取该数据类型信息。

    • 调用contentType为该body设置序列化类型,由于前方引用的Json依赖,因此此处设置Json类型,点进ContentType.Application会发现其支持非常多种类型,Json、Cbor、Protobuf、ZIP等等,如需使用则分别引用依赖即可。

    界面代码

    打开common/src/commomMain/kotlin/包名/App.kt,写下如下Compose代码。

    @Composable
    fun App() {
        val scope = rememberCoroutineScope()
        Column(
            modifier = Modifier.fillMaxSize().padding(24.dp),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center
        ) {
            Text("Get请求", style = TextStyle(fontSize = 24.sp))
            var catForGet by remember { mutableStateOf(Cat("没有猫咪", "快Get获取猫咪", "没有URL")) }
            Button(onClick = {
                scope.launch {
                    CatSource.getRandomCat().onSuccess { catForGet = it }
                }
            }) {
                Text("Get随机猫咪")
            }
            Text(catForGet.toString())
    
            Spacer(Modifier.height(32.dp))
    
            Text("Post请求", style = TextStyle(fontSize = 24.sp))
            var catForPost by remember { mutableStateOf(Cat("没有猫咪", "快Post获取猫咪", "没有URL")) }
            var catNumber by remember { mutableStateOf("1") }
            OutlinedTextField(
                value = catNumber,
                onValueChange = {
                    catNumber = it
                },
                keyboardOptions = KeyboardOptions(
                    keyboardType = KeyboardType.Number
                )
            )
            Button(onClick = {
                scope.launch {
                    CatSource.postSpecialCat(catNumber.toIntOrNull() ?: 0).onSuccess {
                        catForPost = it
                    }.onFailure {
                        println("获取猫咪失败!请检查参数。")
                    }
                }
            }) {
                Text("Post特定猫咪")
            }
            Text(catForPost.toString())
        }
    }
    
    

    运行一下吧!

    本次跑了桌面端和Android端,都能成功从本地搭建的服务器中正确获取到相应内容!

    本来想做图片展示猫咪的,由于本人比较懒就没做!

    可以看到左下角打印日志的地方有打印请求和响应日志。而这个也是一个插件,在初始化HttpClient的时候设置了install(Logging)并将打印级别设置成了ALL,因此打印信息非常全,有看到get请求返回的Json字符串,也能看到post请求发送出去的{"number":4}

    代码

    放一下代码仓库,想跑通文章的代码的同学可以clone下来跑。

    服务端:github.com/MReP1/ktor-…

    客户端:github.com/MReP1/Multi…

    总结

    不知道有没有人看到这里呢?其实我是不太会写总结的,对于全Kotlin的项目我会比较倾向于使用Ktor作为网络请求框架,对协程的支持也比较好,扩展性也强。本文章下一篇Ktor的文章我会讲讲如何使用Ktor发起WebSocket长连接,同样是使用协程和Flow来实现的!

    作者:米奇律师
    链接:https://juejin.cn/post/7142887007465766925

    相关文章

      网友评论

          本文标题:【Ktor入坑指南】全Kotlin编写的多平台异步网络框架 ——

          本文链接:https://www.haomeiwen.com/subject/gzubortx.html