docs: 补充全栈 KMP 中的 kotlinx-rpc 实战与方案对比

This commit is contained in:
SOR2171
2026-10-14 16:24:32 +08:00
parent a25d65dc4d
commit 0f6d7759a4
+257 -3
View File
@@ -20,18 +20,272 @@
除了 iOS 部分必须由 Xcode 做额外设置后编译以外,其他的应用均可在 JDK 下完成编译(Android 需要相应 SDK,其他需要编译为字节码的需要其他编译器)。
Server 本身也是运行在 JVM 上面的,它和普通的 Java / Kotlin 后端项目基本没有区别,除非使用特殊的 Ktor 后端编译为字节码。不过,在 KMP 中,将它的部分代码写在 commonMain 中,可以在一定程度上减少代码的出错可能,还可以进行代码复用。
Server 本身也是运行在 JVM 上面的,它和普通的 Java / Kotlin 后端项目基本没有区别,除非使用特殊的 Ktor 后端编译为字节码。不过,在 KMP 中,将它的部分代码写在 `core/commonMain` 中,可以在一定程度上减少代码的出错可能,还可以进行代码复用。
我们在编写全栈 KMP 时,应当优先考虑 Web(WASM) 和 Server 端能否完美运行。
`core` 的依赖关系在 `shared` 之上;`server` 与整个 `app` 模块平级,在 `core` 之下;`app` 内部模块在 `shared` 之下。
在默认状况下,`core` 的依赖关系在 `shared` 之上;`server` 与整个 `app` 模块平级,在 `core` 之下;`app` 内部模块在 `shared` 之下。不建议自行改动。
## 整体的设计思路
既然 KMP 最大的特点是多平台代码共享,那么这里就要考虑到把 后端的 entity(vo, dto),网络接口(如果后端框架支持拆开写的话)等一并写在共享代码的部分,即 `core/commonMain` 内部。
## RPC 网络交互
在全栈开发中,前端与后端的通信是不可避免的一环。传统的开发流程中,前后端往往通过 RESTful API(HTTP + JSON)进行通信,但全栈 KMP 提供了一种更优雅、类型更安全的通信模式——**RPC(Remote Procedure Call,远程过程调用)**。
一般来讲,在域名后面,传统网络 api 以 `/api` 开头,websocket 服务以 `/ws` 开头,PRC 服务以 `/rpc` 开头。
### 为什么在全栈 KMP 中推荐 RPC?
在传统的双端分离架构(如前端 TS/Vue/React + 后端 Java/Go)中:
- 后端编写 Controller 暴露 HTTP 路由与参数。
- 前端需要查阅 Swagger / API 文档,手动拼写 URL、路径参数与请求体。
- **痛点**:字段名称变更、类型变动无法在编译期间发现,一旦接口不一致只能在运行时报错。
而在**全栈 KMP**体系下,前端(Android、iOS、Desktop、Web)和后端(Ktor Server)全部使用 Kotlin 编写:
1. **如同调用本地函数**:前端通过网络调用后端服务,就像调用本地对象的 `suspend` 函数一样简单自然,免去手动处理 URL 路径、请求方法和 HTTP 状态码的烦恼。
2. **端到端类型安全(End-to-End Type Safety)**:RPC 接口定义在共享的 `core/commonMain` 中。一旦接口方法或传输对象(DTO)发生改变,前后端在编译期便会直接检查并报错,彻底杜绝接口联调中的低级错误。
3. **重构极度友好**:在 IDE(IDEA / Android Studio / Fleet)中对数据字段或接口函数执行重命名或修改签名,全栈代码会自动联动更新。
4. **原生支持双向流式传输(Streaming)**:借助于 Kotlin 协程与 `Flow<T>`,RPC 可以轻松实现实时消息流、双向数据流推送。
### JetBrains 官方方案:kotlinx-rpc
JetBrains 推出了官方库 **`kotlinx-rpc`**,它是专为 Kotlin Multiplatform 打造的轻量级、跨平台 RPC 框架,天然与 Ktor 和 Kotlinx Serialization 深度集成。
SpringBoot 的 RPC 与 Ktor 不完全一致,推荐使用 gRPC,Apache Dubbo 常用于服务器间的通信。
#### 项目依赖配置
RPC 的通信契约定义在 `core` 模块,服务端与客户端分别引入对应的运行时插件与依赖:
**各模块 `build.gradle.kts` 配置示例:**
```kotlin
// 1. 在根工程或者对应模块中引入 kotlinx-rpc 插件
plugins {
id("org.jetbrains.kotlinx.rpc.plugin") version "0.5.1" // 请参考官方最新版本
kotlin("plugin.serialization") version "2.0.0"
}
// 2. core/build.gradle.kts (共享接口与数据模型)
kotlin {
sourceSets {
commonMain.dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-rpc-core:0.5.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1")
}
}
}
// 3. server/build.gradle.kts (服务端)
dependencies {
implementation(project(":core"))
implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-server:0.5.1")
implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-serialization-json:0.5.1")
implementation("io.ktor:ktor-server-websockets:2.3.12")
}
// 4. shared/build.gradle.kts (客户端共享层)
kotlin {
sourceSets {
commonMain.dependencies {
implementation(project(":core"))
implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-client:0.5.1")
implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-serialization-json:0.5.1")
implementation("io.ktor:ktor-client-websockets:2.3.12")
}
}
}
```
#### 在 `core/commonMain` 中定义接口与 DTO
在共享层中,我们只需要使用普通接口继承 `RPC`,并用 `@Serializable` 标记传输对象:
**最好一个文件只写一个 class**
```kotlin
package com.example.core.rpc
import kotlinx.rpc.RPC
import kotlinx.serialization.Serializable
import kotlinx.coroutines.flow.Flow
// 共享的数据传输对象 (DTO)
@Serializable
data class UserDto(
val id: Long,
val name: String,
val role: String
)
@Serializable
data class NoticeMessage(
val id: String,
val content: String,
val timestamp: Long
)
// 共享的 RPC 接口定义
interface UserService : RPC {
// 普通 RPC 异步方法调用
suspend fun getUserProfile(userId: Long): UserDto
// 接收参数并更新
suspend fun updateUserName(userId: Long, newName: String): Boolean
// 使用 Flow 实现服务器向客户端的实时流式推送
fun subscribeNotices(): Flow<NoticeMessage>
}
```
#### 在 `server` 模块实现业务并挂载
在服务端,编写接口的具体业务实现,并将其挂载到 Ktor 路由中(通常通过 WebSocket 通信):
```kotlin
package com.example.server.services
import com.example.core.rpc.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.delay
class UserServiceImpl : UserService {
override suspend fun getUserProfile(userId: Long): UserDto {
// 查数据库或执行业务计算
return UserDto(id = userId, name = "KMP 开发者", role = "Admin")
}
override suspend fun updateUserName(userId: Long, newName: String): Boolean {
println("更新用户 $userId 的姓名为: $newName")
return true
}
override fun subscribeNotices(): Flow<NoticeMessage> = flow {
var count = 0
while (true) {
emit(NoticeMessage("id_$count", "系统广播消息 #$count", System.currentTimeMillis()))
count++
delay(5000) // 每 5 秒推送一条
}
}
}
```
在 Ktor 的 `Application.kt` 路由中挂载 RPC 引擎:
```kotlin
package com.example.server
import io.ktor.server.application.*
import io.ktor.server.routing.*
import io.ktor.server.websocket.*
import kotlinx.rpc.krpc.ktor.server.rpc
import kotlinx.rpc.krpc.serialization.json.json
import com.example.core.rpc.UserService
import com.example.server.services.UserServiceImpl
fun Application.configureRpc() {
install(WebSockets) // 安装 WebSocket 插件
routing {
// 挂载 RPC 端点
rpc("/rpc") {
rpcConfig {
serialization {
json()
}
}
// 注册服务实现类
registerService<UserService> { UserServiceImpl() }
}
}
}
```
#### 在客户端(`shared/commonMain`)调用服务
在跨平台前端共享代码中,初始化 RPC Client 并获取服务代理:
```kotlin
package com.example.shared.network
import io.ktor.client.*
import io.ktor.client.plugins.websocket.*
import kotlinx.rpc.krpc.ktor.client.rpc
import kotlinx.rpc.krpc.ktor.client.rpcConfig
import kotlinx.rpc.krpc.serialization.json.json
import kotlinx.rpc.withService
import com.example.core.rpc.UserService
import com.example.core.rpc.UserDto
class RpcClientManager(
private val host: String = "127.0.0.1",
private val port: Int = 8080
) {
private val httpClient = HttpClient {
install(WebSockets)
}
suspend fun createUserService(): UserService {
val rpcClient = httpClient.rpc {
url {
this.host = this@RpcClientManager.host
this.port = this@RpcClientManager.port
encodedPath = "/rpc"
}
rpcConfig {
serialization {
json()
}
}
}
// 获取由 kotlinx-rpc 动态生成的 UserService 代理实现
return rpcClient.withService<UserService>()
}
}
```
在 UI 层或业务层直接调用:
```kotlin
// 先开一个协程
coroutineScope.launch {
try {
val userService = rpcClientManager.createUserService()
// 1. 调用远程挂起函数
val user: UserDto = userService.getUserProfile(1001)
println("成功获取用户: ${user.name}")
// 2. 收集服务端的实时 Flow 流
userService.subscribeNotices().collect { notice ->
println("收到广播: ${notice.content}")
}
} catch (e: Exception) {
println("RPC 通信异常: ${e.message}")
}
}
```
### 其他 RPC 方案的对比与选型
根据项目场景的不同,全栈 KMP 还支持以下常见方案:
| 方案 | 协议与传输层 | 优势 | 适用场景 |
| :--- | :--- | :--- | :--- |
| **kotlinx-rpc** | WebSocket / HTTP 等 (kRPC) | Kotlin 官方出品,全平台开箱即用,纯 Kotlin 接口定义,开发效率最高 | 纯 Kotlin / KMP 全栈应用、前后端统一开发 |
| **gRPC / Wire** | HTTP/2 + Protobuf | 跨语言支持最强(Java/Go/Python/C++),传输体积小、二进制序列化速度快 | 混合语言微服务架构,前端为 KMP、后端为既有微服务集群 |
| **传统 RESTful (Ktor Client)** | HTTP/1.1 或 HTTP/2 + JSON | 标准通用,对外开放友好,与第三方平台对接便利 | 接口需对外公开(Open API)、或者对接既有老系统 |
> [!TIP]
> 即使在某些必须使用传统 RESTful 架构的项目中,全栈 KMP 依然可以通过在 `core/commonMain` 中统一管理 `@Serializable` 数据类(Request/Response Model),让前后端共用同一套数据协议,从而显著减少联调错误。
## 接下来可以看
[目录](../README.md)
[下一篇:复杂逻辑与多平台不同逻辑](./复杂逻辑与多平台不同逻辑.md)
[下一篇:复杂逻辑与 expect / actual](./复杂逻辑与多平台不同逻辑.md)