From 0f6d7759a4d55fea9c7f51930b91941caddd7037 Mon Sep 17 00:00:00 2001 From: SOR2171 Date: Wed, 14 Oct 2026 16:24:32 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E8=A1=A5=E5=85=85=E5=85=A8=E6=A0=88=20?= =?UTF-8?q?KMP=20=E4=B8=AD=E7=9A=84=20kotlinx-rpc=20=E5=AE=9E=E6=88=98?= =?UTF-8?q?=E4=B8=8E=E6=96=B9=E6=A1=88=E5=AF=B9=E6=AF=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Learning/全栈KMP.md | 260 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 257 insertions(+), 3 deletions(-) diff --git a/Learning/全栈KMP.md b/Learning/全栈KMP.md index 50dfe82..caa28c4 100644 --- a/Learning/全栈KMP.md +++ b/Learning/全栈KMP.md @@ -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`,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 +} +``` + +#### 在 `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 = 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 { 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() + } +} +``` + +在 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) \ No newline at end of file +[下一篇:复杂逻辑与 expect / actual](./复杂逻辑与多平台不同逻辑.md) \ No newline at end of file