KMM 入门处理 HTTP 网络请求
Posted 袁国正_yy
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了KMM 入门处理 HTTP 网络请求相关的知识,希望对你有一定的参考价值。
背景
与 Server 的数据交互已经成为 App 必不可少的一个重要部分,常用的方式即 HTTP(S),当然也有 WebSocket、TCP、UDP 等等
在 KMM 模块中,为保证双端逻辑一致,且对 JVM、Native 进行统一兼容,可以使用官方推荐的 Ktor 进行网络通信,Kotlinx.Serialization 来进行数据解析
这篇文章就来介绍在 KMM 中如何发起并处理网络请求,后面的文章再详细介绍 kotlinx.serialization 的使用
Ktor 是什么?
Ktor 是由 JetBrains 开发的一套用于解决各类应用中网络连接的框架,不仅可以用在发起请求的各类客户端(不是所谓的 App),还可以构建微服务
针对客户端能力,通过一系列插件,可以支持 HTTP 的各类特性,如:Cookies、重定向、代理、UA、WebSocket 等,在一定程度上,还可以支持一些简单的 TCP 或 UDP 通信
另外,Ktor 还支持为不同的平台配置不同的 HTTP 引擎,如:为 android 配置 OkHttp 或 HttpURLConnection,为 ios 配置 NSURLSession,或者为 JVM 配置 Apache HttpClient、为 javascript (Node.js) 配置 node-fetch,以便使用同一套代码逻辑处理网络请求
由于现在的 RESTful API 通常会以 JSON 作为通信数据格式,在 JVM 平台上,Ktor 还支持与 Gson、Jackson 协同工作,而对于 Kotlin Multiplatform(当然包括 KMM)可以与 kotlinx.serialization 进行协作
由于 Ktor 适用的平台广泛,本文只对 KMM 平台上的使用进行说明
为 KMM 模块配置 Ktor
如果你使用的 IDE 是 IntelliJ IDEA Ultimate 版本,可以考虑安装 Ktor 插件,但基于 Community 版本的 Android Studio 等 IDE 并不支持该插件,当然它对实际使用影响不大
对于 KMM 模块,首先需要在 Common 的依赖中加入 Ktor 的核心依赖
由于 Ktor 底层依赖协程一些核心功能,同时 Ktor 需要使用基于 Kotlin Native 且实现多线程版本的协程库,所以还需要加入对协程的依赖
// build.gradle.kts
// 2022 年 4 月,Ktor 正式发布了 2.0.0 版本
val ktor_version = "2.0.2"
// ...
val commonMain by getting
dependencies
implementation("io.ktor:ktor-client-core:$ktor_version")
Android 模块中加入 Ktor Android 端默认引擎(使用 HttpURLConnection)的依赖
// build.gradle.kts
androidMain
dependencies
implementation("io.ktor:ktor-client-android:$ktor_version")
如果需要使用 OkHttp 来作为 HTTP 能力的引擎,可以使用如下的依赖
// build.gradle.kts
androidMain
dependencies
implementation("io.ktor:ktor-client-okhttp:$ktor_version")
另外,Android 端也可以使用 CIO(Coroutine(协程) based I/O 实现)引擎,但 CIO 目前还不支持 HTTP/2
对于 iOS,则加入 iOS 的引擎依赖,由于 iOS 的 HTTP 网络请求都是使用 NSURLSession(包括著名的 AFNetworking,NSURLConnection 早已经不用了),所以也就不像 Android 上有多种选择
// build.gradle.kts
iosMain
dependencies
implementation("io.ktor:ktor-client-darwin:$ktor_version")
由于 Ktor 是 Kotlin 团队主要负责开发和维护,所以对 Kotlin 相关技术栈支持的比较友好,且部分技术应用的也比较激进,比如 Kotlin Native 的 New Memory Management,所以官方建议大家使用 Kotlin 协程,这就要求在宿主 App(Android 端)中添加协程相关的依赖
dependencies
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1")
Ktor 已经适配了 New Memory 技术,如果还需要开启 New Memory,则需要根据 New Memory 官方的文档要求,在 gradle.properties 文件中,添加以下的配置项
kotlin.native.binary.memoryModel=experimental
创建 Ktor 的 HttpClient
Ktor 中的 HttpClient 与其他 HTTP 框架类似,都是对发送和接收网络请求的一系列资源、配置的封装,请求与响应的操作方法,以 Extension 的形式表现,调用也非常简洁
在 Common 代码中,首先需要创建一个 HttpClient 的实例
val httpClient by lazy HttpClient()
如果不需要对 HttpClient 默认的引擎(根据 Gradle 中的依赖自动设置)进行特殊配置,以上代码足矣
为保障多平台的一致,在 Common 中的 HttpClient,对 engine 的可配置项非常有限,只有下面的 Proxy 和线程数量可配,同时可以支持一些公共的请求配置,写在 defaultRequest
闭包中即可,具体内容见下面一节
HttpClient
engine
proxy = ProxyBuilder.http("http://127.0.0.1:8888")
threadsCount = 4
defaultRequest
// 可配置公共的 Cookies、Headers、Params
如果需要针对不同的平台和不同的引擎的特性,进行一些自定义配置,则需要用到 expect/actual 的方式来实现 HttpClient
比如在 Android 代码中,针对 OkHttp 进行一些定制
actual val httpClient by lazy
HttpClient(OkHttp)
engine
config
// 禁止重定向
followRedirects(false)
// 加入 Stetho 方便 Debug
addNetworkInterceptor(StethoInterceptor())
或者对 iOS 的 NSURLSession 进行一些配置
actual val httpClient by lazy
HttpClient(Ios)
engine
configureRequest
// 如果 HttpClient 需要在后台进行上传、下载
NSURLSessionConfiguration.backgroundSessionConfiguration("xxx").apply
// 添加统一的 Headers
HTTPAdditionalHeaders = mapOf("a" to "b")
完成 HttpClient 的创建和配置以后,我们就可以在 Common 目录中的 Kotlin 代码中发起网络请求了
发送一个简单的 HTTP 请求
代码非常简单,只需要一行,但因为 Ktor 中大量使用了协程的开发理念,所以需要符合 Kotlin 协程的基本思想和写法,可以参考:https://kotlinlang.org/docs/coroutines-basics.html
// 写法1:
fun sendGet()
GlobalScope.launch(Dispatchers.Default)
val res: HttpResponse = httpClient.get("https://www.baidu.com")
// 写法2:
suspend fun sendGetAsync()
val res: HttpResponse = httpClient.get("https://www.baidu.com")
这里需要注意的是,由于 iOS 并不支持协程,所以在 iOS 代码中,如果不使用默认的 CoroutineContext
,则需要使用 GCD
单独实现一个 CoroutineDispatcher
实例并作为 launch
方法的参数传入,如下面代码所示:
internal actual val ApplicationDispatcher: CoroutineDispatcher = NsQueueDispatcher(dispatch_get_main_queue())
internal class NsQueueDispatcher(
private val dispatchQueue: dispatch_queue_t
) : CoroutineDispatcher()
override fun dispatch(context: CoroutineContext, block: Runnable)
dispatch_async(dispatchQueue)
block.run()
最后在实际的 Android 和 iOS 工程当中,调用 sendGet()
即可发送网络请求,完成请求之后,别忘记调用 close()
来关闭和释放 HttpClient 实例,以免造成内存泄露
如果 HttpClient 的实例只做一次网络请求,也可以使用 use
语法,在结束时自动进行 close 操作
val status = HttpClient().use client ->
// ...
由于我们还没有处理网络请求的响应,所以需要使用 Charles 或 Fiddler 抓包才能看到发送的网络请求
自定义请求
众所周知,一条 HTTP 请求报文,包含几个重要部分:Method、Host、Path 及 Query、HTTP 版本、Headers、Body(主要是 POST、PUT)
这些内容,Ktor 也都支持定义,封装在 HttpRequestBuilder
当中,并在 HttpClient
的初始化闭包中的 defaultResult
子闭包,以及 HttpClient
的各个扩展方法中,作为最后一个参数的 Block 参数返回,即:可在 HttpClient.request 或 get、post 等扩展方法调用的后的闭包中操作
如果需要添加统一的公共参数,或者 Headers(包括 Cookies、User-Agent),可以在 HttpClient
初始化时,添加 defaultRequest
闭包,并利用其 HttpRequestBuilder
类型的参数进行配置,这样就是可以使所有使用当前 HttpClient 实例的发送的网络请求,保持统一配置
HttpClient
defaultRequest
header("CommonHeader", "KMM")
parameter("CommonParam", "666")
cookie("USER_ID", "123456")
// ...
如果只是给某一个请求添加自定义的配置,只需要在 request
方法调用后的闭包中处理即可
fun sendGet()
GlobalScope.launch(Dispatchers.Default)
val res: HttpResponse = httpClient.request ("https://www.baidu.com")
method = HttpMethod.Get
header("TestHeader", "1")
header("MyHeader", "2")
userAgent("KMM Http Client")
cookie("USER_ID", "123456")
formData
// 示例写法,实际需要处理字节流
append("image", ByteArray(256))
通过 Charles 抓包,就可以看到经过自定义配置后,通过 Ktor 发出的 HTTP 请求
处理响应
和常见的 HTTP 请求框架(如:OkHttp、AFNetworking)类似,Ktor 也支持获取多种类型的返回数据,具体为以下三种:
-
原始响应 Body:
获取原始的 HTTP 响应体内容,比如 HTML、纯文本字符串、二进制数据等
-
JSON 对象:
如果响应内容为纯 JSON 字符串,Ktor 可以在返回响应之前直接解析成你需要的对象,但是需要配置 JSON 插件,并结合 kotlinx.serialization 进行使用
-
流式数据:
如文件下载这种数据量比较大,或是异步、非阻塞式返回形式的数据,可能会用到流式的 HTTP 响应接收模式
下面使用几段示例代码,来实现以上几种响应类型的处理
获取原始类型
- 获取 String 类型(纯文本)的 Body
val httpResponse: HttpResponse = client.get("https://ktor.io/")
val stringBody: String = httpResponse.body()
- 获取 ByteArray 类型(二进制)的 Body
val httpResponse: HttpResponse = client.get("https://ktor.io/")
val byteArrayBody: ByteArray = httpResponse.body()
进行类型自动转换
如果你配置了 Kotlinx.serialization 插件,并且声明了对应数据结构的实体类,则 Ktor 可以自动进行 JSON 解析
首先需要添加 Ktor 用于进行类型转换的依赖,也被称为 ContentNegotiation
implementation("io.ktor:ktor-client-content-negotiation:$ktor_version")
其次需要添加 Kotlinx.serialization 依赖
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version")
注意:这里添加以后,会将 Kotlinx.Serialization 相关的依赖传递进来,建议显式指定版本
然后在 HttpClient 初始化的时候,install 这个类型插件 ContentNegotiation
,并把 JSON 插件配置在里面
val client = HttpClient()
install(ContentNegotiation)
json()
熟悉 Kotlinx.Serialization 的同学可以使用 Json
语法来和直接使用 Kotlin.Serialization 一样进行全局解析配置
这里定义一个和请求结果 JSON 结构一致的 data class,并配置好解析规则
@Serializable
data class Student(
@SerialName("user_id")
val id: String,
@SerialName("user_name")
val name: String,
@SerialName("age")
val age: Int,
)
val httpResponse: HttpResponse = client.get("https://api.xxx.com/student?id=xxx")
val xxx: Student = httpResponse.body()
println(xxx.name) // 张三
流式数据
如果需要下载文件,择需要用到流式数据的形式,来处理 HTTP 响应
val client = HttpClient(CIO)
val file = File.createTempFile("files", "index")
runBlocking
client.prepareGet("https://ktor.io/").execute httpResponse ->
val channel: ByteReadChannel = httpResponse.body()
while (!channel.isClosedForRead)
val packet = channel.readRemaining(DEFAULT_BUFFER_SIZE.toLong())
while (!packet.isEmpty)
val bytes = packet.readBytes()
file.appendBytes(bytes)
println("Received $file.length() bytes from $httpResponse.contentLength()")
println("A file saved to $file.path")
Ktor 的其他功能
Server 能力
Ktor 是个很强大的网络库,不但提供了 HTTP 客户端所需要的各种常见功能,也提供了 HTTP Server 的能力,虽不能与 nginx 这种专业的 HTTP Server 相提并论,但用作测试还是不错的
文档:https://ktor.io/docs/intellij-idea.html
WebSocket
除了常见的 HTTP API,Ktor 对 WebSocket 的支持相当友好,Chat Server,Chat Client
以上关于 Ktor 的介绍就不再详细展开了,有需要的话,可以参考 Ktor 官网的文档:https://ktor.io/docs/welcome.html,内容也十分详细!
KMM 网络能力建设
直接使用 Ktor 建设网络能力,所带来的影响
- 主要优点
- 整体性好,API 统一
- 可借助 Ktor 的所有新增能力
- 友好支持协程、Kotlinx.Serialization 等 Kotlin 工具链
- 没有历史包袱
- 部分缺点
- 无法再利用 App 已有网络组件的能力
- 公共参数、Headers 等需要从 0 开始重新建设
- 在一定程度上导致包体积增大(尤其是 iOS)
- 存在一些不稳定因素(New Memory、协程等)
综合 Ktor 在 KMM 项目中集成的一些优点和缺点,个人认为如果你需要使用 KMM 从零开始开发一个 App,且不太过分在意 iOS 平台的包体积影响,可以优先考虑使用 Ktor,这样 API 和各种网络请求流程会更加统一,也能够结合 Ktor 的各类插件,在一定程度上提升开发效率。
但是如果你需要在原有已经非常成熟的 App 中应用 KMM 技术,重构或新开发某些功能,使用 Ktor 往往不会带来更多的收益。这些 App 大多已经拥有非常完善的网络库了,无论是业务上的公参、统计、异常处理、免流量,还是 HTTP/3、IP 直通、HTTP DNS、SSL 等技术迭代,可谓是遍地开花。所以在这种情况下,个人认为应当尽可能充分地利用现有网络库的能力,在 KMM 层进行 API 和流程的抹平!
推荐的网络能力建设方式
结合实际开发过程中的情况,个人更推荐使用 expect/actual 模式来桥接双端真正的 API。
且由于 HTTP 请求这种业务逻辑,各平台都比较接近,也不存在直接操作 UI 的需求,所以也非常适合使用 KMM 去做逻辑统一。
/**
* HTTP 请求公共接口
*
* @param method 请求方法 [HttpMethod]
* @param url URL
* @param headers 请求 Header,Key-Value
* @param params 参数,Key-Value
* @param bodyType POST 请求的 Body 类型,可能为 JSON 或 URLParams
* @param succeedCallback 成功回调,在状态码为 200 时回调 Header 和 Body
* @param failedCallback 失败回调,回调错误码和信息
*/
expect fun commonHttpRequest(
method: HttpMethod,
url: String,
headers: Map<String, Any?>?,
params: Map<String, Any>?,
bodyType: HttpPostBodyTypes = HttpPostBodyTypes.URL_PARAMS,
succeedCallback: (resHeaders: Map<String, Any>?, body: String?) -> Unit,
failedCallback: (errCode: Int, errMsg: String?) -> Unit
)
如上面的代码片段所示,可以在 KMM 的 commonMain 目录中定义类似的 HTTP 请求接口,后续在 KMM 代码中即可使用该方法发送并处理 HTTP 请求。
但其 actual 的实现应当考虑的相对周全一些,例如:Android 端可以桥接 [OkHttp](square/okhttp: Square’s meticulous HTTP client for the JVM, Android, and GraalVM. (github.com)),iOS 端可以桥接 [AFNetworking](AFNetworking/AFNetworking: A delightful networking framework for iOS, macOS, watchOS, and tvOS. (github.com))。当然,如果项目中有基于系统或第三方库 API 进行二次开发的网络能力,应当桥接二次开发后的 API。
例如,淘宝客户端内部的 ANetwork 网络框架等等……
以 OkHttp(4.0 以上版本)的基本使用为例,Android 端的 actual 实现可以参考下面的代码:
actual fun commonHttpRequest(
method: HttpMethod,
url: String,
headers: Map<String, Any?>?,
params: Map<String, Any>?,
bodyType: HttpPostBodyTypes = HttpPostBodyTypes.URL_PARAMS,
succeedCallback: (resHeaders: Map<String, Any>?, body: String?) -> Unit,
failedCallback: (errCode: Int, errMsg: String?) -> Unit
)
val request = Request.Builder().apply
val httpUrl = url.toHttpUrlOrNull() ?: return@apply
headers?.keys?.forEach key ->
val value = headers[key] ?: return@forEach
header(key, value.toString())
if (method == HttpMethod.POST)
val reqBodyBuilder = FormBody.Builder()
params?.keys?.forEach key ->
val value = params[key] ?: return@forEach
reqBodyBuilder.addEncoded(key, value)
method("POST", reqBodyBuilder.build())
url(httpUrl)
else
val urlBuilder = httpUrl.newBuilder().apply
params?.keys?.forEach key ->
val value = params[key] ?: return@forEach
addEncodedQueryParameter(key, value)
url(urlBuilder.build())
.build()
val call = okHttpClient.newCall(request)
call.enqueue(object : Callback
override fun onFailure(call: Call, e: IOException)
failedCallback(e.message)
override fun onResponse(call: Call, response: Response)
try
if (response.code == 200)
succeedCallback(response.headers.toMap(), response.body?.string() ?: "")
response.body?.closeQuietly()
else
failedCallback.invoke(ERR_CODE_FAILED, "Response is not 200!")
catch (e: Exception)
e.printStackTrace()
failedCallback.invoke(ERR_CODE_FAILED, "Response is not 200!")
)
使用 URLSession 的示例代码:
actual fun commonHttpRequest(
method: HttpMethod,
url: String,
headers: Map<String, Any?>?,
params: Map<String, Any>?,
bodyType: HttpPostBodyTypes,
succeedCallback: (resHeaders: Map<String, Any>?, body: String?) -> Unit,
failedCallback: (errCode: Int, errMsg: String?) -> Unit
)
// 伪代码,不保证能运行
val req = NSMutableURLRequest.requestWithURL(NSURL.URLWithString(url)!!)
req.setHTTPMethod(if (method == HttpMethod.GET) "GET" else "POST")
req.setAllHTTPHeaderFields(headers as Map<Any?, *>)
val session = NSURLSession.sharedSession
session.dataTaskWithRequest(req) data, res, err ->
// handle response
总结
由于网络请求是业务逻辑代码中使用非常频繁的功能,所以在 KMM 中,建设一套适合项目使用的网络能力尤为重要,需要根据项目实际情况选择合理的实现方案,以便实现网络请求开发的效率最大化。
以上是关于KMM 入门处理 HTTP 网络请求的主要内容,如果未能解决你的问题,请参考以下文章