generated from Kotlin/multiplatform-library-template
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds ktor sample from ktor-samples repo
- Loading branch information
1 parent
d479cf7
commit c22bd95
Showing
16 changed files
with
3,640 additions
and
48 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,8 @@ | ||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile | ||
|
||
plugins { | ||
id("root.publication") | ||
// trick: for the same plugin versions in all sub-modules | ||
alias(libs.plugins.androidLibrary) apply false | ||
alias(libs.plugins.kotlinMultiplatform) apply false | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,4 +4,8 @@ plugins { | |
|
||
dependencies { | ||
implementation(libs.nexus.publish) | ||
} | ||
|
||
kotlin { | ||
jvmToolchain(17) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,24 +1,87 @@ | ||
import org.jetbrains.kotlin.gradle.DeprecatedTargetPresetApi | ||
import org.jetbrains.kotlin.gradle.InternalKotlinGradlePluginApi | ||
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalDistributionDsl | ||
|
||
buildscript { | ||
repositories { | ||
mavenCentral() | ||
} | ||
dependencies { | ||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.21") | ||
} | ||
} | ||
|
||
repositories { | ||
mavenCentral() | ||
} | ||
|
||
plugins { | ||
id("io.ktor.plugin") version "2.3.8" | ||
kotlin("jvm") | ||
id("kotlin-multiplatform") | ||
} | ||
|
||
application { | ||
mainClass.set("com.example.ApplicationKt") | ||
kotlin { | ||
@OptIn(DeprecatedTargetPresetApi::class, InternalKotlinGradlePluginApi::class) | ||
targets { | ||
js("frontend", IR) { | ||
browser { | ||
testTask { enabled = false } | ||
|
||
val isDevelopment: Boolean = project.ext.has("development") | ||
applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment") | ||
@OptIn(ExperimentalDistributionDsl::class) | ||
distribution { | ||
directory = file("$projectDir/src/backendMain/resources/web") | ||
} | ||
binaries.executable() | ||
} | ||
} | ||
jvm("backend") | ||
} | ||
|
||
sourceSets.forEach { | ||
it.dependencies { | ||
implementation(project.dependencies.enforcedPlatform("io.ktor:ktor-bom:2.3.9")) | ||
} | ||
} | ||
|
||
sourceSets { | ||
val backendMain by getting { | ||
dependencies { | ||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.21") | ||
implementation("io.ktor:ktor-server-netty") | ||
implementation("io.ktor:ktor-server-websockets") | ||
implementation("io.ktor:ktor-server-call-logging") | ||
implementation("io.ktor:ktor-server-default-headers") | ||
implementation("io.ktor:ktor-server-sessions") | ||
implementation("ch.qos.logback:logback-classic:1.4.6") | ||
} | ||
} | ||
|
||
val backendTest by getting { | ||
dependencies { | ||
implementation("io.ktor:ktor-server-test-host") | ||
implementation("io.ktor:ktor-client-websockets") | ||
implementation("org.jetbrains.kotlin:kotlin-test") | ||
} | ||
} | ||
|
||
val frontendMain by getting { | ||
dependencies { | ||
implementation("org.jetbrains.kotlin:kotlin-stdlib-js") | ||
implementation("io.ktor:ktor-client-websockets") | ||
implementation("io.ktor:ktor-client-js") | ||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.6.4") | ||
} | ||
} | ||
} | ||
} | ||
|
||
repositories { | ||
mavenCentral() | ||
tasks.register<JavaExec>("run") { | ||
dependsOn("frontendBrowserDistribution") | ||
dependsOn("backendMainClasses") | ||
mainClass.set("backendMain.ChatApplicationKt") | ||
// classpath(configurations.getByName("backendRuntimeClasspath").plus("./build/libs/ktor-backend-0.0.1.jar")) | ||
args = emptyList() | ||
} | ||
|
||
dependencies { | ||
implementation("io.ktor:ktor-server-core-jvm") | ||
implementation("io.ktor:ktor-server-netty-jvm") | ||
implementation("ch.qos.logback:logback-classic:1.5.3") | ||
testImplementation("io.ktor:ktor-server-tests-jvm") | ||
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:1.9.23") | ||
implementation(kotlin("stdlib-jdk8")) | ||
} | ||
tasks.named("frontendBrowserProductionWebpack") { | ||
mustRunAfter(":ktor:backendProcessResources") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
resources/web/ktor.js | ||
resources/web/ktor.js.map |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
import io.ktor.server.application.* | ||
import io.ktor.server.engine.* | ||
import io.ktor.server.http.content.* | ||
import io.ktor.server.netty.* | ||
import io.ktor.server.plugins.callloging.* | ||
import io.ktor.server.plugins.defaultheaders.* | ||
import io.ktor.server.routing.* | ||
import io.ktor.server.sessions.* | ||
import io.ktor.server.websocket.* | ||
import io.ktor.util.* | ||
import io.ktor.websocket.* | ||
import kotlinx.coroutines.channels.* | ||
import java.time.* | ||
|
||
/** | ||
* An entry point of the application. | ||
* | ||
* Notice that the fully qualified name of this function is `io.ktor.samples.chat.backend.ChatApplicationKt.main`. | ||
* For top level functions, the class name containing the method in the JVM is FileNameKt. | ||
* | ||
* The `Application.main` part is Kotlin idiomatic that specifies that the main method is | ||
* an extension of the [Application] class, and thus can be accessed like a normal member `myapplication.main()`. | ||
*/ | ||
fun main() { | ||
embeddedServer(Netty, port = 8080) { | ||
ChatApplication().apply { main() } | ||
}.start(wait = true) | ||
} | ||
|
||
fun Application.main() { | ||
ChatApplication().apply { main() } | ||
} | ||
|
||
/** | ||
* In this case, we have a class holding our application state so it is not global and can be tested easier. | ||
*/ | ||
class ChatApplication { | ||
/** | ||
* This class handles the logic of a [ChatServer]. | ||
* With the standard handlers [ChatServer.memberJoin] or [ChatServer.memberLeft] and operations like | ||
* sending messages to everyone or to specific people connected to the server. | ||
*/ | ||
private val server = ChatServer() | ||
// TODO(shouldn't server be injected?) | ||
|
||
/** | ||
* This is the main method of application in this class. | ||
*/ | ||
fun Application.main() { | ||
/** | ||
* First, we install the plugins we need. | ||
* They are bound to the whole application | ||
* since this method has an implicit [Application] receiver that supports the [install] method. | ||
*/ | ||
// This adds Date and Server headers to each response, and would allow you to configure | ||
// additional headers served to each response. | ||
install(DefaultHeaders) | ||
// This uses the logger to log every call (request/response) | ||
install(CallLogging) | ||
// This installs the WebSockets plugin to be able to establish a bidirectional configuration | ||
// between the server and the client | ||
install(WebSockets) { | ||
pingPeriod = Duration.ofMinutes(1) | ||
} | ||
// This enables the use of sessions to keep information between requests/refreshes of the browser. | ||
install(Sessions) { | ||
cookie<ChatSession>("SESSION") | ||
} | ||
|
||
// This adds an interceptor that will create a specific session in each request if no session is available already. | ||
intercept(ApplicationCallPipeline.Plugins) { | ||
if (call.sessions.get<ChatSession>() == null) { | ||
call.sessions.set(ChatSession(generateNonce())) | ||
} | ||
} | ||
|
||
/** | ||
* Now we are going to define routes to handle specific methods + URLs for this application. | ||
*/ | ||
routing { | ||
|
||
// Defines a websocket `/ws` route that allows a protocol upgrade to convert a HTTP request/response request | ||
// into a bidirectional packetized connection. | ||
webSocket("/ws") { // this: WebSocketSession -> | ||
|
||
// First of all we get the session. | ||
val session = call.sessions.get<ChatSession>() | ||
|
||
// We check that we actually have a session. We should always have one, | ||
// since we have defined an interceptor before to set one. | ||
if (session == null) { | ||
close(CloseReason(CloseReason.Codes.VIOLATED_POLICY, "No session")) | ||
return@webSocket | ||
} | ||
|
||
// We notify that a member joined by calling the server handler [memberJoin]. | ||
// This allows associating the session ID to a specific WebSocket connection. | ||
server.memberJoin(session.id, this) | ||
|
||
try { | ||
// We start receiving messages (frames). | ||
// Since this is a coroutine, it is suspended until receiving frames. | ||
// Once the connection is closed, this consumeEach will finish and the code will continue. | ||
incoming.consumeEach { frame -> | ||
// Frames can be [Text], [Binary], [Ping], [Pong], [Close]. | ||
// We are only interested in textual messages, so we filter it. | ||
if (frame is Frame.Text) { | ||
// Now it is time to process the text sent from the user. | ||
// At this point, we have context about this connection, | ||
// the session, the text and the server. | ||
// So we have everything we need. | ||
receivedMessage(session.id, frame.readText()) | ||
} | ||
} | ||
} finally { | ||
// Either if there was an error, or if the connection was closed gracefully, | ||
// we notified the server that the member had left. | ||
server.memberLeft(session.id, this) | ||
} | ||
} | ||
|
||
// This defines a block of static resources for the '/' path (since no path is specified and we start at '/') | ||
static { | ||
// This marks index.html from the 'web' folder in resources as the default file to serve. | ||
defaultResource("index.html", "web") | ||
// This serves files from the 'web' folder in the application resources. | ||
resources("web") | ||
} | ||
|
||
} | ||
} | ||
|
||
/** | ||
* A chat session is identified by a unique nonce ID. This nonce comes from a secure random source. | ||
*/ | ||
data class ChatSession(val id: String) | ||
|
||
/** | ||
* We received a message. Let's process it. | ||
*/ | ||
private suspend fun receivedMessage(id: String, command: String) { | ||
// We are going to handle commands (text starting with '/') and normal messages | ||
when { | ||
// The command `who` responds the user about all the member names connected to the user. | ||
command.startsWith("/who") -> server.who(id) | ||
// The command `user` allows the user to set its name. | ||
command.startsWith("/user") -> { | ||
// We strip the command part to get the rest of the parameters. | ||
// In this case the only parameter is the user's newName. | ||
val newName = command.removePrefix("/user").trim() | ||
// We verify that it is a valid name (in terms of length) to prevent abusing | ||
when { | ||
newName.isEmpty() -> server.sendTo(id, "server::help", "/user [newName]") | ||
newName.length > 50 -> server.sendTo( | ||
id, | ||
"server::help", | ||
"new name is too long: 50 characters limit" | ||
) | ||
else -> server.memberRenamed(id, newName) | ||
} | ||
} | ||
// The command 'help' allows users to get a list of available commands. | ||
command.startsWith("/help") -> server.help(id) | ||
// If no commands are matched at this point, we notify about it. | ||
command.startsWith("/") -> server.sendTo( | ||
id, | ||
"server::help", | ||
"Unknown command ${command.takeWhile { !it.isWhitespace() }}" | ||
) | ||
// Handle a normal message. | ||
else -> server.message(id, command) | ||
} | ||
} | ||
} |
Oops, something went wrong.