This commit is contained in:
Sergey Chernov 2024-08-05 17:51:33 +02:00
parent 439e229294
commit 8a21a836e5
5 changed files with 127 additions and 20 deletions

3
.gitignore vendored
View File

@ -23,8 +23,7 @@ out/
.project
.settings
.springBeans
.sts4-cache
bin/
.sts4-caches
!**/src/main/**/bin/
!**/src/test/**/bin/

View File

@ -146,6 +146,11 @@ val ns: NettyApplicationEngine = embeddedServer(Netty, port = 8080, host = "0.0.
~~~
## See also:
- [Source documentation](https://code.sergeych.net/docs/kiloparsec/)
- [Project's WIKI](https://gitea.sergeych.net/sergeych/kiloparsec/wiki)
# Details
It is not compatible with parsec family and no more based on an Universa crypto library. To better fit

View File

@ -2,6 +2,7 @@ plugins {
kotlin("multiplatform") version "2.0.0"
id("org.jetbrains.kotlin.plugin.serialization") version "2.0.0"
`maven-publish`
id("org.jetbrains.dokka") version "1.9.20"
}
group = "net.sergeych"
@ -135,3 +136,13 @@ kotlin {
}
}
}
tasks.dokkaHtml.configure {
outputDirectory.set(buildDir.resolve("dokka"))
dokkaSourceSets {
configureEach {
// includes.from("docs/bipack.md")
}
}
}

View File

@ -13,13 +13,97 @@ import net.sergeych.mp_tools.globalLaunch
import net.sergeych.tools.AtomicCounter
private val instances = AtomicCounter()
@Suppress("unused")
/**
* The Kiloparsec server.
* Server accepts incoming connections and serves them using the same [clientInterface].
*
* ## Incoming connections
*
* Server collecting incoming connections provided by [connections] `Flow`. For each incoming connection
* the Kiloparsec handshake is performed, then the session object is created, see below, and connection is
* served with [clientInterface] until closed.
*
* ## Session param [S]
*
* After the successful handshake server creates new session for each connection calling the [sessionBuilder].
* Then it creates a [KiloScope] so [KiloScope.session] holds this connection-specific instance. Then
* [KiloInterface.onConnected] is called with this scope. Session can be used to hold connection state. Session
* objects are not persistent, but could be initialized in [KiloInterface.onConnected] where the remote side
* [KiloScope.remoteIdentity] is already verified and set.
*
* ## Usage:
*
* Create a shared library between you server and clients, to specify the interface (otherwise you can
* share sources).
*
* Suppose we have session with a state:
* ```kotlin
* data class Session(
* var data: String,
* )
*```
*
* And some commands to access and change it, in the shared library too:
*
* ```kotlin
* val cmdSave by command<String, Unit>()
* val cmdLoad by command<Unit, String>()
* val cmdDrop by command<Unit, Unit>()
* val cmdException by command<Unit, Unit>()
* ```
* Then the server code (TCP/IP variant) could look like:
*
* ```kotlin
* // The server implementation (could be shared between server instances connected
* // to different protocol adapters):
*
* val cli = KiloInterface<Session>().apply {
* // Suppose we want to throw this exception at the caller site, so we need to register it:
* registerError { SomeException() }
*
* // Session initialization. If you need a sessino to depend initially on the client's identity.
* // you can do it here:
* onConnected {
* // check the remoteIdentity
* session.data = if( remoteIdentity == somePublicVerifyingKey )
* "known"
* else
* "unknown"
* }
* on(cmdSave) { session.data = it }
* on(cmdLoad) {
* session.data
* }
* on(cmdException) {
* throw TestException()
* }
* on(cmdDrop) {
* throw LocalInterface.BreakConnectionException()
* }
* }
*
* // Create the server instance that accepts incoming TCP/IP connections on all local interfaces using the
* // specified port:
*
* val server = KiloServer(cli, acceptTcpDevice(port)) {
* // This creates a new session
* Session("unknown")
* }
* ```
* @param S the type of the server session object, returned by [sessionBuilder]. See above
* @param clientInterface the interface available for remote calls
* @param connections flow of incoming connections. Server stops when the flow is fully collected (normally
* it shouldn't)
* @param serverSecretKey the [SigningKey] used to identify this server during Kiloparsec handshake.
* @param sessionBuilder callback that creates session objects for successful incoming connections
*/
class KiloServer<S>(
private val clientInterface: KiloInterface<S>,
private val connections: Flow<InetTransportDevice>,
private val serverSecretKey: SigningKey? = null,
private val sessionBuilder: ()->S,
): LogTag("KS:${instances.incrementAndGet()}") {
private val sessionBuilder: () -> S,
) : LogTag("KS:${instances.incrementAndGet()}") {
private val job = globalLaunch {
connections.collect { device ->
@ -27,22 +111,30 @@ class KiloServer<S>(
try {
info { "connected ${device}" }
KiloServerConnection(clientInterface, device, sessionBuilder(), serverSecretKey)
.apply { debug { "server connection is ready" }}
.apply { debug { "server connection is ready" } }
.run()
}
catch(_: CancellationException) {
}
catch(cce: LocalInterface.BreakConnectionException) {
} catch (_: CancellationException) {
} catch (cce: LocalInterface.BreakConnectionException) {
info { "Closed exception caught, closing (${cce.flushSendQueue}" }
}
catch (t: Throwable) {
} catch (t: Throwable) {
exception { "unexpected while creating kiloclient" to t }
}
}
}
}
/**
* Stop the server and cancel all pending sessions. Unlike finishing the flow passed
* for [KiloServer.connections], it will cancel all currently active sessions.
*/
fun close() {
job.cancel()
}
/**
* Server is closed either if [close] was called or all [KiloServer.connections] were collected and flow was
* closed.
*/
@Suppress("unused")
val isClosed: Boolean get() = job.isCompleted
}

View File

@ -15,17 +15,17 @@ class TcpTest {
@Test
fun tcpTest() = runTest {
initCrypto()
// Log.connectConsole(Log.Level.DEBUG)
data class Session(
var data: String
)
// Log.connectConsole(Log.Level.DEBUG)
data class Session(
var data: String,
)
val port = 27170 + Random.nextInt(1, 200)
val cmdSave by command<String, Unit>()
val cmdLoad by command<Unit, String>()
val cmdDrop by command<Unit, Unit>()
val cmdException by command<Unit, Unit>()
val cmdSave by command<String, Unit>()
val cmdLoad by command<Unit, String>()
val cmdDrop by command<Unit, Unit>()
val cmdException by command<Unit, Unit>()
val cli = KiloInterface<Session>().apply {
registerError { TestException() }