Skip to content

Commit ab616b2

Browse files
committed
添加一个基于Ktor server的简单服务器
1 parent df97920 commit ab616b2

File tree

16 files changed

+538
-35
lines changed

16 files changed

+538
-35
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ build/
33
out/
44
*.iml
55
/.gradle/
6-
logs/
6+
logs/
7+
.kotlin/

demo/build.gradle.kts

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ dependencies {
5454
implementation(libs.kotlinx.dataframe) {
5555
exclude(group = "org.slf4j", module = "slf4j-simple")
5656
}
57-
implementation(libs.kotlinx.datetime.jvm)
57+
implementation(libs.kotlinx.datetime)
5858
implementation(project(":bittersweet"))
5959

6060
implementation(libs.materialfx)

demo/src/main/kotlin/com/icuxika/bittersweet/demo/api/SuspendApi.kt

+6-6
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,10 @@ suspend inline fun <reified T> suspendPost(
4646
}.execute()
4747
}
4848

49-
sealed class ProgressFlowState {
50-
data class Progress(val progress: Double) : ProgressFlowState()
51-
data class Success(val result: Any? = null) : ProgressFlowState()
52-
data class Error(val throwable: Throwable) : ProgressFlowState()
49+
sealed class ProgressFlowState<T> {
50+
data class Progress<T>(val progress: Double) : ProgressFlowState<T>()
51+
data class Success<T>(val result: T? = null) : ProgressFlowState<T>()
52+
data class Error<T>(val throwable: Throwable) : ProgressFlowState<T>()
5353
}
5454

5555
/**
@@ -59,7 +59,7 @@ suspend fun suspendGetFileFlow(
5959
url: String,
6060
filePath: Path,
6161
data: Any? = null,
62-
) = callbackFlow {
62+
) = callbackFlow<ProgressFlowState<Int>> {
6363
val result = runCatching {
6464
suspendCancellableCoroutine { cancellableContinuation ->
6565
val type = object : TypeToken<Pair<InputStream, Double>>() {}.type
@@ -109,7 +109,7 @@ suspend fun suspendGetFileFlow(
109109
suspend inline fun <reified T> suspendPostFileFlow(
110110
url: String,
111111
data: Any? = null,
112-
) = callbackFlow<ProgressFlowState> {
112+
) = callbackFlow<ProgressFlowState<T>> {
113113
val result = runCatching {
114114
suspendCancellableCoroutine { cancellableContinuation ->
115115
val type = object : TypeToken<T>() {}.type

demo/src/main/kotlin/com/icuxika/bittersweet/demo/util/FileDownloader.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ import java.nio.file.Path
1818
* 使用Flow来实现文件下载的逻辑
1919
*/
2020
object FileDownloader {
21-
suspend fun downloadFile(fileURL: String, filePath: Path): Flow<ProgressFlowState> {
22-
return flow {
21+
suspend fun downloadFile(fileURL: String, filePath: Path): Flow<ProgressFlowState<Double>> {
22+
return flow<ProgressFlowState<Double>> {
2323
val byteBuffer = ByteBuffer.allocate(1024)
2424
runCatching {
2525
URI(fileURL).toURL()
@@ -50,7 +50,7 @@ object FileDownloader {
5050
byteBuffer.clear()
5151
}
5252
fileOutputStream.flush()
53-
emit(ProgressFlowState.Success(0))
53+
emit(ProgressFlowState.Success(0.0))
5454
}
5555
}
5656
}

demo/src/test/kotlin/ApiTest.kt

+18-15
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import java.time.LocalTime
1010
import kotlin.test.Test
1111
import kotlin.test.assertEquals
1212

13+
/**
14+
* .\gradlew.bat :server:run -Pdevelopment
15+
*/
1316
class ApiTest {
1417

1518
data class User(
@@ -24,7 +27,7 @@ class ApiTest {
2427
fun getTest() = runTest {
2528
// ApiData<User> getTest(@RequestParam("id") Long id, @RequestParam("username") String username, @RequestParam("localDate") LocalDate localDate, @RequestParam("localTime") LocalTime localTime, @RequestParam("localDateTime") LocalDateTime localDateTime)
2629
val apiData =
27-
suspendGet<ApiData<User>>("http://127.0.0.1:8080/test/getTest?id=1&username=icuxika&localDate=2024-05-01&localTime=16:58:00&localDateTime=2024-05-01 16:58:00")
30+
suspendGet<ApiData<User>>("http://127.0.0.1:8080/users?id=1&username=icuxika&localDate=2024-05-01&localTime=16:58:00&localDateTime=2024-05-01 16:58:00")
2831
println(apiData)
2932
assertEquals(10000, apiData.code, "请求失败")
3033
}
@@ -33,7 +36,7 @@ class ApiTest {
3336
fun postJsonTest() = runTest {
3437
// ApiData<User> postJsonTest(@RequestBody User user)
3538
val apiData = suspendPost<ApiData<User>>(
36-
"http://127.0.0.1:8080/test/postJsonTest",
39+
"http://127.0.0.1:8080/users",
3740
User(1, "icuxika", LocalDate.of(2024, 5, 20), LocalTime.of(17, 12), LocalDateTime.of(2024, 5, 20, 17, 12))
3841
)
3942
println(apiData)
@@ -44,35 +47,37 @@ class ApiTest {
4447
fun postFileTest() = runTest {
4548
// ApiData<User> postFileTest(@RequestPart("file") MultipartFile file, @RequestParam("id") Long id)
4649
val apiData = suspendPost<ApiData<Unit>>(
47-
"http://127.0.0.1:8080/test/postFileTest", mapOf(
48-
Api.REQUEST_KEY_FILE to File("/Users/icuxika/IdeaProjects/bittersweet/demo/src/main/kotlin/com/icuxika/bittersweet/demo/api/ApiData.kt"),
50+
"http://127.0.0.1:8080/users/upload", mapOf(
51+
Api.REQUEST_KEY_FILE to File("build.gradle.kts"),
4952
"id" to "11"
5053
)
5154
)
55+
println(apiData)
5256
assertEquals(10000, apiData.code, "请求失败")
5357
}
5458

5559
@Test
5660
fun postFileListTest() = runTest {
5761
// ApiData<User> postFileListTest(@RequestPart("fileList") List<MultipartFile> fileList, @RequestParam("id") Long id, @RequestParam("time") LocalDateTime time)
5862
val apiData = suspendPost<ApiData<Unit>>(
59-
"http://127.0.0.1:8080/test/postFileListTest", mapOf(
63+
"http://127.0.0.1:8080/users/upload", mapOf(
6064
Api.REQUEST_KEY_FILE_LIST to listOf(
61-
File("/Users/icuxika/IdeaProjects/bittersweet/demo/src/main/kotlin/com/icuxika/bittersweet/demo/api/ApiData.kt"),
62-
File("/Users/icuxika/IdeaProjects/bittersweet/demo/Dataset/AliYunDataV/aliyun_datav_100000_cn/aliyun_datav_100000_cn.shp")
65+
File("build.gradle.kts"),
66+
File("src/main/resources/application.properties")
6367
),
6468
"id" to "11",
6569
"time" to "2024-05-01 16:58:00"
6670
)
6771
)
72+
println(apiData)
6873
assertEquals(10000, apiData.code, "请求失败")
6974
}
7075

7176
@Test
7277
fun uploadFileTest() = runTest {
73-
suspendPostFileFlow<ApiData<User>>(
74-
"http://127.0.0.1:8080/test/postFileTest", mapOf(
75-
Api.REQUEST_KEY_FILE to File("/Users/icuxika/IdeaProjects/bittersweet/demo/src/main/resources/com/icuxika/bittersweet/demo/fonts/HarmonyOS-Sans/HarmonyOS_Sans_Black.ttf"),
78+
suspendPostFileFlow<ApiData<Unit>>(
79+
"http://127.0.0.1:8080/users/upload", mapOf(
80+
Api.REQUEST_KEY_FILE to File("build.gradle.kts"),
7681
"id" to "11",
7782
)
7883
).flowOn(Dispatchers.IO).collect {
@@ -86,9 +91,7 @@ class ApiTest {
8691
}
8792

8893
is ProgressFlowState.Success -> {
89-
(it.result as? ApiData<*>)?.let { apiData ->
90-
println(apiData)
91-
}
94+
println(it.result)
9295
}
9396
}
9497
}
@@ -97,8 +100,8 @@ class ApiTest {
97100
@Test
98101
fun downloadFileTest() = runTest {
99102
suspendGetFileFlow(
100-
"http://127.0.0.1:8080/test/downloadFileTest",
101-
Path.of("/Users/icuxika/Downloads/test.mp4")
103+
"http://127.0.0.1:8080/users/download",
104+
Path.of(System.getProperty("user.home")).resolve("Downloads").resolve("temp").resolve("build.gradle.kts")
102105
).flowOn(Dispatchers.IO).collect {
103106
when (it) {
104107
is ProgressFlowState.Progress -> {

gradle/libs.versions.toml

+24-9
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
[versions]
22
project-version = "0.0.2-SNAPSHOT"
3+
kotlin-version = "2.1.0"
34
jvm-target = "21"
45
javafx-version = "22"
56

67
kotlinx-coroutines-core-version = "1.9.0"
78
kotlinx-dataframe-version = "0.14.1"
8-
kotlinx-datetime-jvm-version = "0.6.1"
9+
kotlinx-datetime-version = "0.6.1"
910
kotlinx-serialization-json-version = "1.7.3"
11+
ktor-version = "3.0.2"
1012

1113
slf4j2-version = "2.0.12"
1214
log4j-version = "2.23.0"
@@ -20,26 +22,37 @@ okhttp-version = "4.12.0"
2022
gson-version = "2.10.1"
2123
fxgl-version = "21.1"
2224

23-
plugin-kotlin-jvm-version = "2.1.0"
2425
plugin-beryx-jlink-version = "3.1.0"
2526
plugin-beryx-runtime-version = "1.13.1"
2627

2728
[libraries]
2829
# kotlin
29-
kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "plugin-kotlin-jvm-version" }
30-
kotlin-reflect = { group = "org.jetbrains.kotlin", name = "kotlin-reflect", version.ref = "plugin-kotlin-jvm-version" }
31-
kotlin-test-junit5 = { group = "org.jetbrains.kotlin", name = "kotlin-test-junit5", version.ref = "plugin-kotlin-jvm-version" }
30+
kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin-version" }
31+
kotlin-reflect = { group = "org.jetbrains.kotlin", name = "kotlin-reflect", version.ref = "kotlin-version" }
32+
kotlin-test-junit5 = { group = "org.jetbrains.kotlin", name = "kotlin-test-junit5", version.ref = "kotlin-version" }
3233

34+
# kotlinx
3335
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinx-coroutines-core-version" }
3436
kotlinx-coroutines-javafx = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-javafx", version.ref = "kotlinx-coroutines-core-version" }
3537
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines-core-version" }
3638
kotlinx-dataframe = { group = "org.jetbrains.kotlinx", name = "dataframe", version.ref = "kotlinx-dataframe-version" }
37-
kotlinx-datetime-jvm = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime-jvm", version.ref = "kotlinx-datetime-jvm-version" }
39+
kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinx-datetime-version" }
3840
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization-json-version" }
3941

42+
# ktor
43+
ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negotiation-jvm", version.ref = "ktor-version" }
44+
ktor-server-core = { module = "io.ktor:ktor-server-core-jvm", version.ref = "ktor-version" }
45+
ktor-server-resources = { module = "io.ktor:ktor-server-resources", version.ref = "ktor-version" }
46+
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json-jvm", version.ref = "ktor-version" }
47+
ktor-server-netty = { module = "io.ktor:ktor-server-netty-jvm", version.ref = "ktor-version" }
48+
ktor-server-config-yaml = { module = "io.ktor:ktor-server-config-yaml-jvm", version.ref = "ktor-version" }
49+
ktor-server-test-host = { module = "io.ktor:ktor-server-test-host-jvm", version.ref = "ktor-version" }
50+
51+
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation-jvm", version.ref = "ktor-version" }
52+
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor-version" }
53+
4054
# javafx ui
4155
materialfx = { group = "io.github.palexdev", name = "materialfx", version.ref = "materialfx-version" }
42-
4356
fxgl = { group = "com.github.almasb", name = "fxgl", version.ref = "fxgl-version" }
4457

4558
# Lets-Plot
@@ -69,9 +82,11 @@ log4j = ["log4j-api", "log4j-core", "log4j-slf4j2-impl"]
6982
logback = ["logback-classic", "logback-core"]
7083
lets-plot = ["lets-plot-jfx", "lets-plot-kotlin-jvm", "lets-plot-kotlin-geotools", "gt-shapefile", "gt-geojson", "gt-cql"]
7184
okhttp = ["okhttp", "gson"]
85+
ktor-server = ["ktor-server-content-negotiation", "ktor-server-core", "ktor-server-resources", "ktor-serialization-kotlinx-json", "ktor-server-netty", "ktor-server-config-yaml", "ktor-server-test-host"]
7286

7387
[plugins]
74-
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "plugin-kotlin-jvm-version" }
75-
kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "plugin-kotlin-jvm-version" }
88+
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin-version" }
89+
kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin-version" }
90+
ktor = { id = "io.ktor.plugin", version.ref = "ktor-version" }
7691
beryx-jlink = { id = "org.beryx.jlink", version.ref = "plugin-beryx-jlink-version" }
7792
beryx-runtime = { id = "org.beryx.runtime", version.ref = "plugin-beryx-runtime-version" }

server/build.gradle.kts

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
2+
import org.gradle.api.tasks.testing.logging.TestLogEvent.*
3+
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
4+
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
5+
6+
plugins {
7+
alias(libs.plugins.kotlin.jvm)
8+
application
9+
alias(libs.plugins.ktor)
10+
alias(libs.plugins.kotlinx.serialization)
11+
}
12+
13+
repositories {
14+
mavenCentral()
15+
}
16+
17+
application {
18+
applicationName = "BitterSweetDemo"
19+
mainClass.set("com.icuxika.bittersweet.server.MainAppKt")
20+
21+
// .\gradlew.bat :server:run -Pdevelopment
22+
val isDevelopment: Boolean = project.ext.has("development")
23+
println("isDevelopment: $isDevelopment")
24+
val enableKotlinxCoroutinesDebug = if (isDevelopment) "on" else "off"
25+
applicationDefaultJvmArgs =
26+
listOf("-Dkotlinx.coroutines.debug=$enableKotlinxCoroutinesDebug", "-Dio.ktor.development=$isDevelopment")
27+
}
28+
29+
dependencies {
30+
implementation(libs.kotlin.stdlib)
31+
implementation(libs.kotlin.reflect)
32+
implementation(libs.kotlinx.coroutines.core)
33+
implementation(libs.kotlinx.datetime)
34+
implementation(libs.bundles.ktor.server)
35+
implementation(libs.bundles.logback)
36+
testImplementation(libs.kotlin.test.junit5)
37+
testImplementation(libs.kotlinx.coroutines.test)
38+
testImplementation(libs.ktor.server.test.host)
39+
testImplementation(libs.ktor.client.content.negotiation)
40+
testImplementation(libs.ktor.client.logging)
41+
}
42+
43+
group = "com.icuxika.bittersweet"
44+
version = libs.versions.project.version.get()
45+
46+
tasks.compileJava {
47+
options.release.set(libs.versions.jvm.target.get().toInt())
48+
}
49+
50+
kotlin {
51+
compilerOptions {
52+
jvmTarget.set(JvmTarget.fromTarget(libs.versions.jvm.target.get()))
53+
languageVersion.set(KotlinVersion.KOTLIN_2_1)
54+
}
55+
}
56+
57+
tasks.test {
58+
useJUnitPlatform()
59+
testLogging {
60+
exceptionFormat = FULL
61+
showExceptions = true
62+
showStandardStreams = true
63+
events(PASSED, SKIPPED, FAILED, STANDARD_OUT, STANDARD_ERROR)
64+
}
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package com.icuxika.bittersweet.server
2+
3+
import kotlinx.coroutines.*
4+
import kotlinx.coroutines.channels.Channel
5+
import kotlin.coroutines.CoroutineContext
6+
7+
@OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class)
8+
fun main(args: Array<String>) = runBlocking {
9+
CoroutineScope(newSingleThreadContext("miniByteTurbo")).launch {
10+
val miniByteTurbo = MiniByteTurbo()
11+
miniByteTurbo.start()
12+
println("[${currentCoroutineInfo()}] hello")
13+
doSendData(miniByteTurbo)
14+
println("[${currentCoroutineInfo()}] world")
15+
}
16+
io.ktor.server.netty.EngineMain.main(args)
17+
}
18+
19+
suspend fun doSendData(miniByteTurbo: MiniByteTurbo) = coroutineScope {
20+
launch {
21+
delay(3000)
22+
println("[${currentCoroutineInfo()}] sendData: 1")
23+
miniByteTurbo.sendData(1)
24+
}
25+
launch {
26+
delay(2000)
27+
println("[${currentCoroutineInfo()}] sendData: 2")
28+
miniByteTurbo.sendData(2)
29+
}
30+
launch {
31+
delay(1000)
32+
println("[${currentCoroutineInfo()}] sendData: 3")
33+
miniByteTurbo.sendData(3)
34+
}
35+
}
36+
37+
@OptIn(ExperimentalStdlibApi::class)
38+
suspend fun currentCoroutineInfo() =
39+
"${currentCoroutineContext()[CoroutineDispatcher]}###${Thread.currentThread().name}"
40+
41+
@OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class)
42+
class MiniByteTurbo {
43+
private val readDispatcher = newSingleThreadContext("readDispatcher")
44+
private val writeDispatcher = newSingleThreadContext("writeDispatcher")
45+
private val readScope = object : CoroutineScope {
46+
override val coroutineContext: CoroutineContext
47+
get() = readDispatcher + CoroutineName("readScope")
48+
}
49+
private val writeScope = object : CoroutineScope {
50+
override val coroutineContext: CoroutineContext
51+
get() = writeDispatcher + CoroutineName("writeScope")
52+
}
53+
private val readJob = readScope.launch(start = CoroutineStart.LAZY) {
54+
doRead()
55+
}
56+
private val writeJob = writeScope.launch(start = CoroutineStart.LAZY) {
57+
doWrite()
58+
}
59+
private val channel = Channel<Int>()
60+
61+
private fun doRead() {
62+
63+
}
64+
65+
private suspend fun doWrite() {
66+
while (writeJob.isActive) {
67+
val data = channel.receive()
68+
println("[${currentCoroutineInfo()}] data: $data")
69+
}
70+
}
71+
72+
fun start() {
73+
writeJob.start()
74+
}
75+
76+
suspend fun sendData(data: Int) {
77+
channel.send(data)
78+
}
79+
}

0 commit comments

Comments
 (0)