0.2.5-snapshot native support for tcp client/server
This commit is contained in:
parent
26d1f3522f
commit
ffcdcf7350
1
.idea/misc.xml
generated
1
.idea/misc.xml
generated
@ -1,4 +1,3 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
<component name="FrameworkDetectionExcludesConfiguration">
|
<component name="FrameworkDetectionExcludesConfiguration">
|
||||||
|
62
README.md
62
README.md
@ -1,31 +1,36 @@
|
|||||||
# Kiloparsec
|
# Kiloparsec
|
||||||
|
|
||||||
The new generation of __PARanoid SECurity__ protocol, advanced, faster, more secure. It also allows connecting any "block device" transport to the same local interface. Out if the box it
|
The new generation of __PARanoid SECurity__ protocol, advanced, faster, more secure. It also allows connecting any "
|
||||||
|
block device" transport to the same local interface. Out if the box it
|
||||||
provides the following transports:
|
provides the following transports:
|
||||||
|
|
||||||
| name | JVM | JS | native |
|
| name | JVM | JS | native |
|
||||||
|----------------|-----|----|--------|
|
|----------------|-----|----|-------------------|
|
||||||
| TCP/IP server | * | | |
|
| TCP/IP server | ✓ | | β @0.2.5-SNAPSHOT |
|
||||||
| TCP/IP client | * | | |
|
| TCP/IP client | ✓ | | β @0.2.5-SNAPSHOT |
|
||||||
| Websock server | * | | |
|
| Websock server | ✓ | | |
|
||||||
| Websock client | * | * | * |
|
| Websock client | ✓ | ✓ | ✓ |
|
||||||
|
|
||||||
At the moment we're working on supporting TCP/IP on most native targets. This feature is planned to rach public beta in August and production in early september 2024.
|
|
||||||
|
|
||||||
|
At the moment we're working on supporting TCP/IP on most native targets. This feature is planned to rach public beta in
|
||||||
|
August and production in early september 2024.
|
||||||
|
|
||||||
## TCP/IP transport
|
## TCP/IP transport
|
||||||
|
|
||||||
It is the fastest. JVM implementation uses nio2 async sockets and optimizes TCP socket to play
|
It is the fastest. JVM implementation uses nio2 async sockets and optimizes TCP socket to play
|
||||||
well with blocks (smart NO_DELAY mode). It is multiplatform, nut lacks of async TCP/IP support
|
well with blocks (smart NO_DELAY mode). It is multiplatform, nut lacks of async TCP/IP support
|
||||||
on natvic targetm this is where I need help having little time. I'd prefer to use something asyn like UV on native targets.
|
on natvic targetm this is where I need help having little time. I'd prefer to use something asyn like UV on native
|
||||||
|
targets.
|
||||||
|
|
||||||
I know no existing way to implement it in KotlinJS for the modern browsers.
|
I know no existing way to implement it in KotlinJS for the modern browsers.
|
||||||
|
|
||||||
## Websock server
|
## Websock server
|
||||||
|
|
||||||
While it is much slower than pure TCP, it is still faster than any http-based transport. It uses binary frames based on the Ktor server framework to easily integrate with web services. We recommend using it instead of a classic HTTP API as it beats it in terms of speed and server load even with HTTP/2.
|
While it is much slower than pure TCP, it is still faster than any http-based transport. It uses binary frames based on
|
||||||
|
the Ktor server framework to easily integrate with web services. We recommend using it instead of a classic HTTP API as
|
||||||
|
it beats it in terms of speed and server load even with HTTP/2.
|
||||||
|
|
||||||
We recommend to create the `KiloInterface<S>` instance and connect it to the websock and tcp servers in real applications to get easy access from anywhere.
|
We recommend to create the `KiloInterface<S>` instance and connect it to the websock and tcp servers in real
|
||||||
|
applications to get easy access from anywhere.
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
|
|
||||||
@ -64,16 +69,16 @@ and functions available, like:
|
|||||||
// Api.kt
|
// Api.kt
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class FooArgs(val text: String,val number: Int = 42)
|
class FooArgs(val text: String, val number: Int = 42)
|
||||||
|
|
||||||
// Server-side interface
|
// Server-side interface
|
||||||
val cmdSetFoo by command<FooArgs,Unit>()
|
val cmdSetFoo by command<FooArgs, Unit>()
|
||||||
val cmdGetFoo by command<Unit,FooArgs>()
|
val cmdGetFoo by command<Unit, FooArgs>()
|
||||||
val cmdPing by command<String,String>()
|
val cmdPing by command<String, String>()
|
||||||
val cmdCheckConnected by command<Unit,Boolean>()
|
val cmdCheckConnected by command<Unit, Boolean>()
|
||||||
|
|
||||||
// client-side interface (called from the server)
|
// client-side interface (called from the server)
|
||||||
val cmdPushClient by command<String,Unit>()
|
val cmdPushClient by command<String, Unit>()
|
||||||
```
|
```
|
||||||
|
|
||||||
## Call it from the client:
|
## Call it from the client:
|
||||||
@ -93,7 +98,7 @@ val client = websocketClient<Unit>("wss://your.host.com/kp") {
|
|||||||
// If we want to collect connected state changes (this is optional)
|
// If we want to collect connected state changes (this is optional)
|
||||||
launch {
|
launch {
|
||||||
client.connectedStateFlow.collect {
|
client.connectedStateFlow.collect {
|
||||||
if( it )
|
if (it)
|
||||||
println("I am connected")
|
println("I am connected")
|
||||||
else
|
else
|
||||||
println("trying to connect...")
|
println("trying to connect...")
|
||||||
@ -113,13 +118,13 @@ the protocol. With KILOPARSEC it is rather basic operation:\
|
|||||||
|
|
||||||
~~~kotlin
|
~~~kotlin
|
||||||
// Our session just keeps Foo for cmd{Get|Set}Foo:
|
// Our session just keeps Foo for cmd{Get|Set}Foo:
|
||||||
data class Session(var fooState: FooArgs?=null)
|
data class Session(var fooState: FooArgs? = null)
|
||||||
|
|
||||||
// Let's now provide interface we export, it will be used on each connection automatically:
|
// Let's now provide interface we export, it will be used on each connection automatically:
|
||||||
|
|
||||||
// Note server interface uses Session:
|
// Note server interface uses Session:
|
||||||
val serverInterface = KiloInterface<Session>().apply {
|
val serverInterface = KiloInterface<Session>().apply {
|
||||||
onConnected {
|
onConnected {
|
||||||
// Do some initialization
|
// Do some initialization
|
||||||
session.fooState = null
|
session.fooState = null
|
||||||
}
|
}
|
||||||
@ -136,6 +141,7 @@ val ns: NettyApplicationEngine = embeddedServer(Netty, port = 8080, host = "0.0.
|
|||||||
|
|
||||||
|
|
||||||
~~~
|
~~~
|
||||||
|
|
||||||
# Details
|
# Details
|
||||||
|
|
||||||
It is not compatible with parsec family and no more based on an Universa crypto library. To better fit
|
It is not compatible with parsec family and no more based on an Universa crypto library. To better fit
|
||||||
@ -144,12 +150,14 @@ and every connection (while parsec caches session keys to avoid time-consuming k
|
|||||||
keys cryptography for session is shifted to use ed25519 curves which are supposed to provide agreeable strength with
|
keys cryptography for session is shifted to use ed25519 curves which are supposed to provide agreeable strength with
|
||||||
enough speed to protect every connection with a unique new keys. Also, we completely get rid of SHA2.
|
enough speed to protect every connection with a unique new keys. Also, we completely get rid of SHA2.
|
||||||
|
|
||||||
Kiloparsec also uses a denser binary format [bipack](https://gitea.sergeych.net/SergeychWorks/mp_bintools), no more key-values,
|
Kiloparsec also uses a denser binary format [bipack](https://gitea.sergeych.net/SergeychWorks/mp_bintools), no more
|
||||||
which reveals much less on the inner data structure, providing advanced
|
key-values,
|
||||||
typed RPC interfaces with kotlinx.serialization. There is also Rust implementation [bipack_ru](https://gitea.sergeych.net/DiWAN/bipack_ru).
|
which reveals much less on the inner data structure, providing advanced
|
||||||
|
typed RPC interfaces with kotlinx.serialization. There is also Rust
|
||||||
|
implementation [bipack_ru](https://gitea.sergeych.net/DiWAN/bipack_ru).
|
||||||
The architecture allows connecting same functional interfaces to several various type channels at once.
|
The architecture allows connecting same functional interfaces to several various type channels at once.
|
||||||
|
|
||||||
Also, the difference from parsecs is that there are no more unencrypted layer commands available to users.
|
Also, the difference from parsecs is that there are no more unencrypted layer commands available to users.
|
||||||
All RPC is performed over the encrypted connection.
|
All RPC is performed over the encrypted connection.
|
||||||
|
|
||||||
# Technical description
|
# Technical description
|
||||||
@ -163,7 +171,9 @@ Integrated tools to prevent MITM attacks include also non-transferred independen
|
|||||||
independently on the ends and is never transferred with the network. Comparing it somehow (visually, with QR code, etc)
|
independently on the ends and is never transferred with the network. Comparing it somehow (visually, with QR code, etc)
|
||||||
could add a very robust guarantee of the connection safety and ingenuity.
|
could add a very robust guarantee of the connection safety and ingenuity.
|
||||||
|
|
||||||
Kiloparsec has built-in completely asynchronous (coroutine based top-down) transport layer based on TCP (JVM only as for now) and the same async Websocket-based transport based on KTOR. Websocket client is multiplatform, though the server is JVM only insofar.
|
Kiloparsec has built-in completely asynchronous (coroutine based top-down) transport layer based on TCP (JVM only as for
|
||||||
|
now) and the same async Websocket-based transport based on KTOR. Websocket client is multiplatform, though the server is
|
||||||
|
JVM only insofar.
|
||||||
|
|
||||||
# Licensing
|
# Licensing
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
kotlin("multiplatform") version "2.0.0"
|
kotlin("multiplatform") version "2.0.0"
|
||||||
id("org.jetbrains.kotlin.plugin.serialization") version "2.0.0"
|
id("org.jetbrains.kotlin.plugin.serialization") version "2.0.0"
|
||||||
@ -59,6 +58,9 @@ kotlin {
|
|||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val ktorSocketTest by creating {
|
||||||
|
dependsOn(commonTest)
|
||||||
|
}
|
||||||
val jvmMain by getting {
|
val jvmMain by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("io.ktor:ktor-server-core:$ktor_version")
|
implementation("io.ktor:ktor-server-core:$ktor_version")
|
||||||
@ -69,16 +71,52 @@ kotlin {
|
|||||||
}
|
}
|
||||||
dependsOn(ktorSocketMain)
|
dependsOn(ktorSocketMain)
|
||||||
}
|
}
|
||||||
val jvmTest by getting
|
val jvmTest by getting {
|
||||||
|
dependsOn(ktorSocketTest)
|
||||||
|
}
|
||||||
|
|
||||||
val jsMain by getting {
|
val jsMain by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("io.ktor:ktor-client-js:$ktor_version")
|
implementation("io.ktor:ktor-client-js:$ktor_version")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val jsTest by getting
|
val jsTest by getting
|
||||||
|
val macosArm64Main by getting {
|
||||||
|
dependsOn(ktorSocketMain)
|
||||||
|
}
|
||||||
|
val macosArm64Test by getting {
|
||||||
|
dependsOn(ktorSocketTest)
|
||||||
|
}
|
||||||
|
val macosX64Main by getting {
|
||||||
|
dependsOn(ktorSocketMain)
|
||||||
|
}
|
||||||
|
val iosX64Main by getting {
|
||||||
|
dependsOn(ktorSocketMain)
|
||||||
|
}
|
||||||
|
val iosX64Test by getting {
|
||||||
|
dependsOn(ktorSocketTest)
|
||||||
|
}
|
||||||
|
val iosArm64Main by getting {
|
||||||
|
dependsOn(ktorSocketMain)
|
||||||
|
}
|
||||||
|
val iosArm64Test by getting {
|
||||||
|
dependsOn(ktorSocketTest)
|
||||||
|
}
|
||||||
|
val linuxArm64Main by getting {
|
||||||
|
dependsOn(ktorSocketMain)
|
||||||
|
}
|
||||||
|
val linuxArm64Test by getting {
|
||||||
|
dependsOn(ktorSocketTest)
|
||||||
|
}
|
||||||
|
val linuxX64Main by getting {
|
||||||
|
dependsOn(ktorSocketMain)
|
||||||
|
}
|
||||||
|
val linuxX64Test by getting {
|
||||||
|
dependsOn(ktorSocketTest)
|
||||||
|
}
|
||||||
|
|
||||||
for (pm in listOf(linuxMain, macosMain, iosMain, mingwMain))
|
// for (pm: NamedDomainObjectProvider<KotlinSourceSet> in listOf(macosMain,linuxMain, iosMain, mingwMain))
|
||||||
pm { dependsOn(ktorSocketMain) }
|
// pm.get().dependsOn(ktorSocketMain)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1 +1,2 @@
|
|||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
|
kotlin.mpp.applyDefaultHierarchyTemplate=false
|
@ -43,6 +43,8 @@ class KiloServer<S>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun close() {
|
fun close() {
|
||||||
|
println("PRREEEC")
|
||||||
job.cancel()
|
job.cancel()
|
||||||
|
println("POOOSTC")
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,56 +1,56 @@
|
|||||||
package net.sergeych.kiloparsec.adapter
|
package net.sergeych.kiloparsec.adapter
|
||||||
|
|
||||||
import kotlinx.coroutines.channels.ReceiveChannel
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Multiplatform implementation of an internet address.
|
* Multiplatform internet address.
|
||||||
* Notice to implementors. It must provide correct and effective [equals] and [hashCode].
|
|
||||||
*/
|
*/
|
||||||
interface NetworkAddress {
|
data class NetworkAddress(
|
||||||
val host: String
|
val host: String,
|
||||||
val port: Int
|
val port: Int
|
||||||
}
|
) {
|
||||||
|
override fun toString(): String {
|
||||||
/**
|
return "$host:$port"
|
||||||
* Multiplatform datagram abstraction
|
|
||||||
*/
|
|
||||||
interface Datagram {
|
|
||||||
/**
|
|
||||||
* Received message
|
|
||||||
*/
|
|
||||||
val message: UByteArray
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Address from where the message was sent
|
|
||||||
*/
|
|
||||||
val address: NetworkAddress
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalStdlibApi::class)
|
|
||||||
interface DatagramConnector: AutoCloseable {
|
|
||||||
|
|
||||||
val incoming: ReceiveChannel<Datagram>
|
|
||||||
suspend fun send(message: UByteArray, networkAddress: NetworkAddress)
|
|
||||||
@Suppress("unused")
|
|
||||||
suspend fun send(message: UByteArray, datagramAddress: String) {
|
|
||||||
send(message, datagramAddress.toNetworkAddress())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun send(message: UByteArray,host: String,port: Int) =
|
|
||||||
send(message, NetworkAddress(host,port))
|
|
||||||
override fun close()
|
|
||||||
}
|
}
|
||||||
|
//
|
||||||
expect fun NetworkAddress(host: String,port: Int): NetworkAddress
|
///**
|
||||||
|
// * Multiplatform datagram abstraction
|
||||||
fun String.toNetworkAddress() : NetworkAddress {
|
// */
|
||||||
val (host, port) = this.split(":").map { it.trim()}
|
//interface Datagram {
|
||||||
return NetworkAddress(host, port.toInt())
|
// /**
|
||||||
}
|
// * Received message
|
||||||
|
// */
|
||||||
expect fun acceptTcpDevice(port: Int): Flow<InetTransportDevice>
|
// val message: UByteArray
|
||||||
|
//
|
||||||
expect suspend fun connectTcpDevice(address: NetworkAddress): InetTransportDevice
|
// /**
|
||||||
|
// * Address from where the message was sent
|
||||||
suspend fun connectTcpDevice(address: String) = connectTcpDevice(address.toNetworkAddress())
|
// */
|
||||||
|
// val address: NetworkAddress
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//@OptIn(ExperimentalStdlibApi::class)
|
||||||
|
//interface DatagramConnector: AutoCloseable {
|
||||||
|
//
|
||||||
|
// val incoming: ReceiveChannel<Datagram>
|
||||||
|
// suspend fun send(message: UByteArray, networkAddress: NetworkAddress)
|
||||||
|
// @Suppress("unused")
|
||||||
|
// suspend fun send(message: UByteArray, datagramAddress: String) {
|
||||||
|
// send(message, datagramAddress.toNetworkAddress())
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// suspend fun send(message: UByteArray,host: String,port: Int) =
|
||||||
|
// send(message, NetworkAddress(host,port))
|
||||||
|
// override fun close()
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//expect fun NetworkAddress(host: String,port: Int): NetworkAddress
|
||||||
|
//
|
||||||
|
//fun String.toNetworkAddress() : NetworkAddress {
|
||||||
|
// val (host, port) = this.split(":").map { it.trim()}
|
||||||
|
// return NetworkAddress(host, port.toInt())
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//expect fun acceptTcpDevice(port: Int): Flow<InetTransportDevice>
|
||||||
|
//
|
||||||
|
//expect suspend fun connectTcpDevice(address: NetworkAddress): InetTransportDevice
|
||||||
|
//
|
||||||
|
//suspend fun connectTcpDevice(address: String) = connectTcpDevice(address.toNetworkAddress())
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
package net.sergeych.kiloparsec.adapter
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
|||||||
package net.sergeych.kiloparsec.adapter
|
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
actual fun NetworkAddress(host: String, port: Int): NetworkAddress {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
actual fun acceptTcpDevice(port: Int): Flow<InetTransportDevice> {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
actual suspend fun connectTcpDevice(address: NetworkAddress): InetTransportDevice {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
@ -1,118 +1,118 @@
|
|||||||
package net.sergeych.kiloparsec.adapter
|
//package net.sergeych.kiloparsec.adapter
|
||||||
|
//
|
||||||
import kotlinx.coroutines.*
|
//import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
//import kotlinx.coroutines.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.channels.Channel
|
//import kotlinx.coroutines.channels.Channel
|
||||||
import net.sergeych.mp_logger.LogTag
|
//import net.sergeych.mp_logger.LogTag
|
||||||
import net.sergeych.mp_logger.exception
|
//import net.sergeych.mp_logger.exception
|
||||||
import net.sergeych.mp_logger.info
|
//import net.sergeych.mp_logger.info
|
||||||
import net.sergeych.mp_logger.warning
|
//import net.sergeych.mp_logger.warning
|
||||||
import java.net.*
|
//import java.net.*
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
//import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
//
|
||||||
private val counter = AtomicInteger(0)
|
//private val counter = AtomicInteger(0)
|
||||||
|
//
|
||||||
class JvmNetworkAddress(val inetAddress: InetAddress, override val port: Int) : NetworkAddress {
|
//class JvmNetworkAddress(val inetAddress: InetAddress, override val port: Int) : NetworkAddress {
|
||||||
override val host: String by lazy { inetAddress.canonicalHostName }
|
// override val host: String by lazy { inetAddress.canonicalHostName }
|
||||||
override fun equals(other: Any?): Boolean {
|
// override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
// if (this === other) return true
|
||||||
if (other !is JvmNetworkAddress) return false
|
// if (other !is JvmNetworkAddress) return false
|
||||||
|
//
|
||||||
if (inetAddress != other.inetAddress) return false
|
// if (inetAddress != other.inetAddress) return false
|
||||||
if (port != other.port) return false
|
// if (port != other.port) return false
|
||||||
|
//
|
||||||
return true
|
// return true
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
val socketAddress: SocketAddress by lazy { InetSocketAddress(inetAddress,port) }
|
// val socketAddress: SocketAddress by lazy { InetSocketAddress(inetAddress,port) }
|
||||||
|
//
|
||||||
override fun hashCode(): Int {
|
// override fun hashCode(): Int {
|
||||||
var result = inetAddress.hashCode()
|
// var result = inetAddress.hashCode()
|
||||||
result = 31 * result + port
|
// result = 31 * result + port
|
||||||
return result
|
// return result
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
override fun toString(): String = "$host:$port"
|
// override fun toString(): String = "$host:$port"
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
class UdpDatagram(override val message: UByteArray, val inetAddress: InetAddress, val port: Int) : Datagram {
|
//class UdpDatagram(override val message: UByteArray, val inetAddress: InetAddress, val port: Int) : Datagram {
|
||||||
|
//
|
||||||
override val address: NetworkAddress by lazy {
|
// override val address: NetworkAddress by lazy {
|
||||||
JvmNetworkAddress(inetAddress, port)
|
// JvmNetworkAddress(inetAddress, port)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
|
//
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
//@OptIn(DelicateCoroutinesApi::class)
|
||||||
class UdpServer(val port: Int) :
|
//class UdpServer(val port: Int) :
|
||||||
DatagramConnector, LogTag("UDPS:${counter.incrementAndGet()}") {
|
// DatagramConnector, LogTag("UDPS:${counter.incrementAndGet()}") {
|
||||||
private var isClosed = false
|
// private var isClosed = false
|
||||||
|
//
|
||||||
|
//
|
||||||
private val deferredSocket = CompletableDeferred<DatagramSocket>()
|
// private val deferredSocket = CompletableDeferred<DatagramSocket>()
|
||||||
private var job: Job? = null
|
// private var job: Job? = null
|
||||||
|
//
|
||||||
private suspend fun start() = try {
|
// private suspend fun start() = try {
|
||||||
coroutineScope {
|
// coroutineScope {
|
||||||
val socket = DatagramSocket(port)
|
// val socket = DatagramSocket(port)
|
||||||
val buffer = ByteArray(16384)
|
// val buffer = ByteArray(16384)
|
||||||
val packet = DatagramPacket(buffer, buffer.size)
|
// val packet = DatagramPacket(buffer, buffer.size)
|
||||||
deferredSocket.complete(socket)
|
// deferredSocket.complete(socket)
|
||||||
while (isActive && !isClosed) {
|
// while (isActive && !isClosed) {
|
||||||
try {
|
// try {
|
||||||
socket.receive(packet)
|
// socket.receive(packet)
|
||||||
val data = packet.data.sliceArray(0..<packet.length)
|
// val data = packet.data.sliceArray(0..<packet.length)
|
||||||
val datagram = UdpDatagram(data.toUByteArray(), packet.address, packet.port)
|
// val datagram = UdpDatagram(data.toUByteArray(), packet.address, packet.port)
|
||||||
if (!channel.trySend(datagram).isSuccess) {
|
// if (!channel.trySend(datagram).isSuccess) {
|
||||||
warning { "packet lost!" }
|
// warning { "packet lost!" }
|
||||||
// and we cause overflow that overwrites the oldest
|
// // and we cause overflow that overwrites the oldest
|
||||||
channel.send(datagram)
|
// channel.send(datagram)
|
||||||
}
|
// }
|
||||||
} catch (e: Exception) {
|
// } catch (e: Exception) {
|
||||||
if (!isClosed)
|
// if (!isClosed)
|
||||||
e.printStackTrace()
|
// e.printStackTrace()
|
||||||
throw e
|
// throw e
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
info { "closing socket and reception loop" }
|
// info { "closing socket and reception loop" }
|
||||||
|
//
|
||||||
}
|
// }
|
||||||
} catch (_: CancellationException) {
|
// } catch (_: CancellationException) {
|
||||||
info { "server is closed" }
|
// info { "server is closed" }
|
||||||
} catch (t: Throwable) {
|
// } catch (t: Throwable) {
|
||||||
exception { "unexpected end of server" to t }
|
// exception { "unexpected end of server" to t }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
init {
|
// init {
|
||||||
job = GlobalScope.launch(Dispatchers.IO) {
|
// job = GlobalScope.launch(Dispatchers.IO) {
|
||||||
start()
|
// start()
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
override fun close() {
|
// override fun close() {
|
||||||
if (!isClosed) {
|
// if (!isClosed) {
|
||||||
if (deferredSocket.isCompleted) {
|
// if (deferredSocket.isCompleted) {
|
||||||
runCatching {
|
// runCatching {
|
||||||
deferredSocket.getCompleted().close()
|
// deferredSocket.getCompleted().close()
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
isClosed = true
|
// isClosed = true
|
||||||
job?.cancel(); job = null
|
// job?.cancel(); job = null
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private val channel = Channel<Datagram>(2048, BufferOverflow.DROP_OLDEST)
|
// private val channel = Channel<Datagram>(2048, BufferOverflow.DROP_OLDEST)
|
||||||
override val incoming = channel
|
// override val incoming = channel
|
||||||
|
//
|
||||||
override suspend fun send(message: UByteArray, networkAddress: NetworkAddress) {
|
// override suspend fun send(message: UByteArray, networkAddress: NetworkAddress) {
|
||||||
networkAddress as JvmNetworkAddress
|
// networkAddress as JvmNetworkAddress
|
||||||
withContext(Dispatchers.IO) {
|
// withContext(Dispatchers.IO) {
|
||||||
val packet = DatagramPacket(
|
// val packet = DatagramPacket(
|
||||||
message.toByteArray(), message.size,
|
// message.toByteArray(), message.size,
|
||||||
networkAddress.inetAddress, networkAddress.port
|
// networkAddress.inetAddress, networkAddress.port
|
||||||
)
|
// )
|
||||||
deferredSocket.await().send(packet)
|
// deferredSocket.await().send(packet)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//}
|
@ -1,155 +1,155 @@
|
|||||||
package net.sergeych.kiloparsec.adapter
|
//package net.sergeych.kiloparsec.adapter
|
||||||
|
//
|
||||||
import kotlinx.coroutines.*
|
//import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.channels.Channel
|
//import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
//import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
//import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import net.sergeych.crypto2.Contrail
|
//import net.sergeych.crypto2.Contrail
|
||||||
import net.sergeych.crypto2.encodeVarUnsigned
|
//import net.sergeych.crypto2.encodeVarUnsigned
|
||||||
import net.sergeych.crypto2.readVarUnsigned
|
//import net.sergeych.crypto2.readVarUnsigned
|
||||||
import net.sergeych.kiloparsec.RemoteInterface
|
//import net.sergeych.kiloparsec.RemoteInterface
|
||||||
import net.sergeych.kiloparsec.Transport
|
//import net.sergeych.kiloparsec.Transport
|
||||||
import net.sergeych.mp_logger.LogTag
|
//import net.sergeych.mp_logger.LogTag
|
||||||
import net.sergeych.mp_logger.warning
|
//import net.sergeych.mp_logger.warning
|
||||||
import net.sergeych.mp_tools.globalLaunch
|
//import net.sergeych.mp_tools.globalLaunch
|
||||||
import net.sergeych.tools.waitFor
|
//import net.sergeych.tools.waitFor
|
||||||
import java.net.InetSocketAddress
|
//import java.net.InetSocketAddress
|
||||||
import java.net.StandardSocketOptions.TCP_NODELAY
|
//import java.net.StandardSocketOptions.TCP_NODELAY
|
||||||
import java.nio.ByteBuffer
|
//import java.nio.ByteBuffer
|
||||||
import java.nio.channels.AsynchronousSocketChannel
|
//import java.nio.channels.AsynchronousSocketChannel
|
||||||
import kotlin.coroutines.cancellation.CancellationException
|
//import kotlin.coroutines.cancellation.CancellationException
|
||||||
import kotlin.coroutines.suspendCoroutine
|
//import kotlin.coroutines.suspendCoroutine
|
||||||
|
//
|
||||||
private val log = LogTag("ASTD")
|
//private val log = LogTag("ASTD")
|
||||||
|
//
|
||||||
/**
|
///**
|
||||||
* Prepend block with its size, varint-encoded
|
// * Prepend block with its size, varint-encoded
|
||||||
*/
|
// */
|
||||||
private fun encode(block: UByteArray): ByteArray {
|
//private fun encode(block: UByteArray): ByteArray {
|
||||||
val c = Contrail.create(block)
|
// val c = Contrail.create(block)
|
||||||
return (encodeVarUnsigned(c.size.toUInt()) + c).toByteArray()
|
// return (encodeVarUnsigned(c.size.toUInt()) + c).toByteArray()
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
/**
|
///**
|
||||||
* Convert asynchronous socket to a [Transport.Device] using non-blocking nio,
|
// * Convert asynchronous socket to a [Transport.Device] using non-blocking nio,
|
||||||
* in a coroutine-effective manner. Note that it runs coroutines to read/write
|
// * in a coroutine-effective manner. Note that it runs coroutines to read/write
|
||||||
* to the socket in a global scope.These are closed when transport is closed
|
// * to the socket in a global scope.These are closed when transport is closed
|
||||||
* or the socket is closed, for example, by network failure.
|
// * or the socket is closed, for example, by network failure.
|
||||||
*/
|
// */
|
||||||
suspend fun asyncSocketToDevice(socket: AsynchronousSocketChannel): InetTransportDevice {
|
//suspend fun asyncSocketToDevice(socket: AsynchronousSocketChannel): InetTransportDevice {
|
||||||
val deferredDevice = CompletableDeferred<InetTransportDevice>()
|
// val deferredDevice = CompletableDeferred<InetTransportDevice>()
|
||||||
globalLaunch {
|
// globalLaunch {
|
||||||
coroutineScope {
|
// coroutineScope {
|
||||||
val sendQueueEmpty = MutableStateFlow(true)
|
// val sendQueueEmpty = MutableStateFlow(true)
|
||||||
val receiving = MutableStateFlow(false)
|
// val receiving = MutableStateFlow(false)
|
||||||
// We're in block mode, every block we send worth immediate sending, we do not
|
// // We're in block mode, every block we send worth immediate sending, we do not
|
||||||
// send partial blocks, so:
|
// // send partial blocks, so:
|
||||||
socket.setOption(TCP_NODELAY, true)
|
// socket.setOption(TCP_NODELAY, true)
|
||||||
|
//
|
||||||
// socket input is to be parsed for blocks, so we receive bytes
|
// // socket input is to be parsed for blocks, so we receive bytes
|
||||||
// and decode them to blocks
|
// // and decode them to blocks
|
||||||
val input = Channel<UByte>(1024)
|
// val input = Channel<UByte>(1024)
|
||||||
val inputBlocks = Channel<UByteArray>()
|
// val inputBlocks = Channel<UByteArray>()
|
||||||
// output is blocks, so we sent transformed, framed blocks:
|
// // output is blocks, so we sent transformed, framed blocks:
|
||||||
val outputBlocks = Channel<UByteArray>()
|
// val outputBlocks = Channel<UByteArray>()
|
||||||
|
//
|
||||||
fun stop() {
|
// fun stop() {
|
||||||
kotlin.runCatching { inputBlocks.close(RemoteInterface.ClosedException()) }
|
// kotlin.runCatching { inputBlocks.close(RemoteInterface.ClosedException()) }
|
||||||
kotlin.runCatching { outputBlocks.close() }
|
// kotlin.runCatching { outputBlocks.close() }
|
||||||
socket.close()
|
// socket.close()
|
||||||
cancel()
|
// cancel()
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
// copy incoming data from the socket to input channel:
|
// // copy incoming data from the socket to input channel:
|
||||||
launch {
|
// launch {
|
||||||
val data = ByteArray(1024)
|
// val data = ByteArray(1024)
|
||||||
val inb = ByteBuffer.wrap(data)
|
// val inb = ByteBuffer.wrap(data)
|
||||||
kotlin.runCatching {
|
// kotlin.runCatching {
|
||||||
while (isActive) {
|
// while (isActive) {
|
||||||
inb.position(0)
|
// inb.position(0)
|
||||||
val size: Int = suspendCoroutine { continuation ->
|
// val size: Int = suspendCoroutine { continuation ->
|
||||||
socket.read(inb, continuation, IntCompletionHandler)
|
// socket.read(inb, continuation, IntCompletionHandler)
|
||||||
}
|
// }
|
||||||
if (size < 0) stop()
|
// if (size < 0) stop()
|
||||||
else {
|
// else {
|
||||||
// println("recvd:\n${data.sliceArray(0..<size).toDump()}\n------------------")
|
//// println("recvd:\n${data.sliceArray(0..<size).toDump()}\n------------------")
|
||||||
for (i in 0..<size) input.send(data[i].toUByte())
|
// for (i in 0..<size) input.send(data[i].toUByte())
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// copy from output to socket:
|
// // copy from output to socket:
|
||||||
launch {
|
// launch {
|
||||||
try {
|
// try {
|
||||||
while (isActive) {
|
// while (isActive) {
|
||||||
// wait for the first block to send
|
// // wait for the first block to send
|
||||||
sendQueueEmpty.value = outputBlocks.isEmpty
|
// sendQueueEmpty.value = outputBlocks.isEmpty
|
||||||
var data = encode(outputBlocks.receive())
|
// var data = encode(outputBlocks.receive())
|
||||||
|
//
|
||||||
// now we're sending, so queue state is sending:
|
// // now we're sending, so queue state is sending:
|
||||||
sendQueueEmpty.value = false
|
// sendQueueEmpty.value = false
|
||||||
|
//
|
||||||
// if there are more, take them all (NO_DELAY optimization)
|
// // if there are more, take them all (NO_DELAY optimization)
|
||||||
while (!outputBlocks.isEmpty)
|
// while (!outputBlocks.isEmpty)
|
||||||
data += encode(outputBlocks.receive())
|
// data += encode(outputBlocks.receive())
|
||||||
|
//
|
||||||
// now send it all together:
|
// // now send it all together:
|
||||||
val outBuff = ByteBuffer.wrap(data)
|
// val outBuff = ByteBuffer.wrap(data)
|
||||||
val cnt = suspendCoroutine { continuation ->
|
// val cnt = suspendCoroutine { continuation ->
|
||||||
socket.write(outBuff, continuation, IntCompletionHandler)
|
// socket.write(outBuff, continuation, IntCompletionHandler)
|
||||||
}
|
// }
|
||||||
// be sure it was all sent
|
// // be sure it was all sent
|
||||||
if (outBuff.position() != data.size || cnt != data.size) {
|
// if (outBuff.position() != data.size || cnt != data.size) {
|
||||||
throw RuntimeException("unexpected partial write")
|
// throw RuntimeException("unexpected partial write")
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
// in the case of just breaking out of the loop:
|
// // in the case of just breaking out of the loop:
|
||||||
sendQueueEmpty.value = true
|
// sendQueueEmpty.value = true
|
||||||
} catch (_: ClosedReceiveChannelException) {
|
// } catch (_: ClosedReceiveChannelException) {
|
||||||
stop()
|
// stop()
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
// transport device copes with blocks:
|
// // transport device copes with blocks:
|
||||||
// decode blocks from a byte channel read from the socket:
|
// // decode blocks from a byte channel read from the socket:
|
||||||
launch {
|
// launch {
|
||||||
try {
|
// try {
|
||||||
while (isActive) {
|
// while (isActive) {
|
||||||
receiving.value = !input.isEmpty
|
// receiving.value = !input.isEmpty
|
||||||
val size = readVarUnsigned(input)
|
// val size = readVarUnsigned(input)
|
||||||
receiving.value = true
|
// receiving.value = true
|
||||||
if (size == 0u) log.warning { "zero size block is ignored!" }
|
// if (size == 0u) log.warning { "zero size block is ignored!" }
|
||||||
else {
|
// else {
|
||||||
val block = UByteArray(size.toInt())
|
// val block = UByteArray(size.toInt())
|
||||||
for (i in 0..<size.toInt()) {
|
// for (i in 0..<size.toInt()) {
|
||||||
block[i] = input.receive()
|
// block[i] = input.receive()
|
||||||
}
|
// }
|
||||||
Contrail.unpack(block)?.let { inputBlocks.send(it) }
|
// Contrail.unpack(block)?.let { inputBlocks.send(it) }
|
||||||
?: log.warning { "skipping bad block ${block.size} bytes" }
|
// ?: log.warning { "skipping bad block ${block.size} bytes" }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
} catch (_: CancellationException) {
|
// } catch (_: CancellationException) {
|
||||||
} catch (_: ClosedReceiveChannelException) {
|
// } catch (_: ClosedReceiveChannelException) {
|
||||||
stop()
|
// stop()
|
||||||
}
|
// }
|
||||||
receiving.value = false
|
// receiving.value = false
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
val addr = socket.remoteAddress as InetSocketAddress
|
// val addr = socket.remoteAddress as InetSocketAddress
|
||||||
deferredDevice.complete(
|
// deferredDevice.complete(
|
||||||
InetTransportDevice(inputBlocks, outputBlocks, JvmNetworkAddress(addr.address, addr.port), {
|
// InetTransportDevice(inputBlocks, outputBlocks, JvmNetworkAddress(addr.address, addr.port), {
|
||||||
yield()
|
// yield()
|
||||||
// wait until all received data are parsed, but not too long
|
// // wait until all received data are parsed, but not too long
|
||||||
withTimeoutOrNull(500) {
|
// withTimeoutOrNull(500) {
|
||||||
receiving.waitFor { !it }
|
// receiving.waitFor { !it }
|
||||||
}
|
// }
|
||||||
// then stop it
|
// // then stop it
|
||||||
stop()
|
// stop()
|
||||||
})
|
// })
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
globalLaunch { socket.close() }
|
// globalLaunch { socket.close() }
|
||||||
}
|
// }
|
||||||
return deferredDevice.await()
|
// return deferredDevice.await()
|
||||||
}
|
//}
|
||||||
|
@ -3,22 +3,20 @@ package net.sergeych.kiloparsec
|
|||||||
import assertThrows
|
import assertThrows
|
||||||
import io.ktor.server.engine.*
|
import io.ktor.server.engine.*
|
||||||
import io.ktor.server.netty.*
|
import io.ktor.server.netty.*
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import net.sergeych.crypto2.initCrypto
|
import net.sergeych.crypto2.initCrypto
|
||||||
import net.sergeych.kiloparsec.adapter.acceptTcpDevice
|
|
||||||
import net.sergeych.kiloparsec.adapter.connectTcpDevice
|
|
||||||
import net.sergeych.kiloparsec.adapter.setupWebsocketServer
|
import net.sergeych.kiloparsec.adapter.setupWebsocketServer
|
||||||
import net.sergeych.kiloparsec.adapter.websocketClient
|
import net.sergeych.kiloparsec.adapter.websocketClient
|
||||||
import net.sergeych.mp_logger.Log
|
import net.sergeych.mp_logger.Log
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import kotlin.test.*
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
class ClientTest {
|
class ClientTest {
|
||||||
|
|
||||||
class TestException : Exception("test1")
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testAddresses() {
|
fun testAddresses() {
|
||||||
println(InetAddress.getLocalHost())
|
println(InetAddress.getLocalHost())
|
||||||
@ -26,60 +24,7 @@ class ClientTest {
|
|||||||
println(InetAddress.getByName("127.0.0.1"))
|
println(InetAddress.getByName("127.0.0.1"))
|
||||||
println(InetAddress.getByName("mail.ru"))
|
println(InetAddress.getByName("mail.ru"))
|
||||||
}
|
}
|
||||||
@Test
|
|
||||||
fun testClient() = runTest {
|
|
||||||
initCrypto()
|
|
||||||
Log.connectConsole(Log.Level.DEBUG)
|
|
||||||
data class Session(
|
|
||||||
var data: String
|
|
||||||
)
|
|
||||||
|
|
||||||
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() }
|
|
||||||
onConnected { session.data = "start" }
|
|
||||||
on(cmdSave) { session.data = it }
|
|
||||||
on(cmdLoad) {
|
|
||||||
session.data
|
|
||||||
}
|
|
||||||
on(cmdException) {
|
|
||||||
throw TestException()
|
|
||||||
}
|
|
||||||
on(cmdDrop) {
|
|
||||||
throw LocalInterface.BreakConnectionException()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val server = KiloServer(cli, acceptTcpDevice(27101)) {
|
|
||||||
Session("unknown")
|
|
||||||
}
|
|
||||||
|
|
||||||
val client = KiloClient<Unit>() {
|
|
||||||
addErrors(cli)
|
|
||||||
connect { connectTcpDevice("localhost:27101") }
|
|
||||||
}
|
|
||||||
delay(500)
|
|
||||||
println(client.call(cmdLoad))
|
|
||||||
|
|
||||||
assertEquals("start", client.call(cmdLoad))
|
|
||||||
client.call(cmdSave, "foobar")
|
|
||||||
assertEquals("foobar", client.call(cmdLoad))
|
|
||||||
//
|
|
||||||
val res = kotlin.runCatching { client.call(cmdException) }
|
|
||||||
println(res.exceptionOrNull())
|
|
||||||
assertIs<TestException>(res.exceptionOrNull())
|
|
||||||
assertEquals("foobar", client.call(cmdLoad))
|
|
||||||
|
|
||||||
assertThrows<RemoteInterface.ClosedException> { client.call(cmdDrop) }
|
|
||||||
|
|
||||||
// reconnect?
|
|
||||||
assertEquals("start", client.call(cmdLoad))
|
|
||||||
|
|
||||||
server.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun webSocketTest() = runTest {
|
fun webSocketTest() = runTest {
|
||||||
|
@ -1,100 +0,0 @@
|
|||||||
package net.sergeych.kiloparsec.adapters
|
|
||||||
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import net.sergeych.crypto2.initCrypto
|
|
||||||
import net.sergeych.kiloparsec.adapter.UdpServer
|
|
||||||
import net.sergeych.kiloparsec.adapter.acceptTcpDevice
|
|
||||||
import net.sergeych.kiloparsec.adapter.connectTcpDevice
|
|
||||||
import net.sergeych.kiloparsec.adapter.toNetworkAddress
|
|
||||||
import net.sergeych.kiloparsec.decodeFromUByteArray
|
|
||||||
import net.sergeych.kiloparsec.encodeToUByteArray
|
|
||||||
import net.sergeych.mp_logger.Log
|
|
||||||
import net.sergeych.mp_logger.LogTag
|
|
||||||
import net.sergeych.synctools.ProtectedOp
|
|
||||||
import net.sergeych.synctools.invoke
|
|
||||||
import kotlin.test.Test
|
|
||||||
import kotlin.test.assertContains
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
|
|
||||||
class NetworkTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun udpProviderTest() = runTest {
|
|
||||||
Log.connectConsole(Log.Level.DEBUG)
|
|
||||||
val s1 = UdpServer(17120)
|
|
||||||
val s2 = UdpServer(17121)
|
|
||||||
s1.send("Hello".encodeToUByteArray(), "localhost", 17121)
|
|
||||||
val d1 = s2.incoming.receive()
|
|
||||||
assertEquals(d1.address.port, 17120)
|
|
||||||
assertEquals("Hello", d1.message.toByteArray().decodeToString())
|
|
||||||
s1.send("world".encodeToUByteArray(), d1.address)
|
|
||||||
assertEquals("world", s1.incoming.receive().message.toByteArray().decodeToString())
|
|
||||||
// println("s1: ${s1.bindAddress()}")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun tcpAsyncConnectionTest() = runTest {
|
|
||||||
initCrypto()
|
|
||||||
Log.connectConsole(Log.Level.DEBUG)
|
|
||||||
|
|
||||||
coroutineScope {
|
|
||||||
val serverFlow = acceptTcpDevice(17171)
|
|
||||||
val op = ProtectedOp()
|
|
||||||
var pills = setOf<String>()
|
|
||||||
val j = launch {
|
|
||||||
println("serf")
|
|
||||||
serverFlow.collect { device ->
|
|
||||||
println("serf 0")
|
|
||||||
launch {
|
|
||||||
println("serf 1")
|
|
||||||
device.output.send("Hello, world!".encodeToUByteArray())
|
|
||||||
device.output.send("Great".encodeToUByteArray())
|
|
||||||
while (true) {
|
|
||||||
val x = device.input.receive().decodeFromUByteArray()
|
|
||||||
if (x.startsWith("die")) {
|
|
||||||
op.invoke {
|
|
||||||
pills += x
|
|
||||||
}
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
else
|
|
||||||
println("ignoring unexpected input: $x")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
yield()
|
|
||||||
run {
|
|
||||||
try {
|
|
||||||
println("pre-con")
|
|
||||||
val s = connectTcpDevice("127.0.1.1:17171".toNetworkAddress())
|
|
||||||
println("2")
|
|
||||||
assertEquals("Hello, world!", s.input.receive().decodeFromUByteArray())
|
|
||||||
assertEquals("Great", s.input.receive().decodeFromUByteArray())
|
|
||||||
s.output.send("Goodbye".encodeToUByteArray())
|
|
||||||
s.output.send("die1".encodeToUByteArray())
|
|
||||||
s.close()
|
|
||||||
}
|
|
||||||
catch(t: Throwable) {
|
|
||||||
t.printStackTrace()
|
|
||||||
throw t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
println("pre-con2")
|
|
||||||
val s1 = connectTcpDevice("127.0.1.1:17171".toNetworkAddress())
|
|
||||||
assertEquals("Hello, world!", s1.input.receive().decodeFromUByteArray())
|
|
||||||
assertEquals("Great", s1.input.receive().decodeFromUByteArray())
|
|
||||||
s1.output.send("die2".encodeToUByteArray())
|
|
||||||
s1.close()
|
|
||||||
|
|
||||||
// check that channels were flushed prior to closed:
|
|
||||||
assertContains(pills, "die1")
|
|
||||||
assertContains(pills, "die2")
|
|
||||||
|
|
||||||
// Check that server jobs are closed
|
|
||||||
j.cancelAndJoin()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,27 +2,16 @@ package net.sergeych.kiloparsec.adapter
|
|||||||
|
|
||||||
import io.ktor.network.selector.*
|
import io.ktor.network.selector.*
|
||||||
import io.ktor.network.sockets.*
|
import io.ktor.network.sockets.*
|
||||||
import io.ktor.util.network.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.CancellationException
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.isActive
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import net.sergeych.kiloparsec.AsyncVarint
|
import net.sergeych.kiloparsec.AsyncVarint
|
||||||
import net.sergeych.kiloparsec.LocalInterface
|
import net.sergeych.kiloparsec.LocalInterface
|
||||||
import net.sergeych.mp_logger.*
|
import net.sergeych.mp_logger.*
|
||||||
import net.sergeych.tools.AtomicCounter
|
import net.sergeych.tools.AtomicCounter
|
||||||
|
import net.sergeych.tools.AtomicValue
|
||||||
class SocketNetworkAddress(override val host: String, override val port: Int) : NetworkAddress {
|
|
||||||
override fun toString(): String {
|
|
||||||
return "$host:$port"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
actual fun NetworkAddress(host: String, port: Int): NetworkAddress = SocketNetworkAddress(host, port)
|
|
||||||
|
|
||||||
private val logCounter = AtomicCounter(0)
|
private val logCounter = AtomicCounter(0)
|
||||||
|
|
||||||
@ -30,51 +19,66 @@ class ProtocolException(text: String, cause: Throwable? = null) : RuntimeExcepti
|
|||||||
|
|
||||||
const val MAX_TCP_BLOCK_SIZE = 16776216
|
const val MAX_TCP_BLOCK_SIZE = 16776216
|
||||||
|
|
||||||
actual fun acceptTcpDevice(port: Int): Flow<InetTransportDevice> {
|
fun acceptTcpDevice(port: Int): Flow<InetTransportDevice> {
|
||||||
val selectorManager = SelectorManager(Dispatchers.IO)
|
val selectorManager = SelectorManager(Dispatchers.IO)
|
||||||
val serverSocket = aSocket(selectorManager).tcp().bind("127.0.0.1", port)
|
val serverSocket = aSocket(selectorManager).tcp().bind("127.0.0.1", port)
|
||||||
val log = LogTag("TCPS${logCounter.incrementAndGet()}")
|
|
||||||
return flow {
|
return flow {
|
||||||
while(true) {
|
while (true) {
|
||||||
log.info { "Accepting incoming connections on $port" }
|
|
||||||
serverSocket.accept().let { sock ->
|
serverSocket.accept().let { sock ->
|
||||||
log.info { "Emitting transport device" }
|
|
||||||
emit(inetTransportDevice(sock, "srv"))
|
emit(inetTransportDevice(sock, "srv"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actual suspend fun connectTcpDevice(address: NetworkAddress): InetTransportDevice {
|
suspend fun connectTcpDevice(address: String) = connectTcpDevice(address.toNetworkAddress())
|
||||||
|
|
||||||
|
suspend fun connectTcpDevice(address: NetworkAddress): InetTransportDevice {
|
||||||
val selectorManager = SelectorManager(Dispatchers.IO)
|
val selectorManager = SelectorManager(Dispatchers.IO)
|
||||||
val socket = aSocket(selectorManager).tcp().connect(address.host, address.port)
|
val socket = aSocket(selectorManager).tcp().connect(address.host, address.port)
|
||||||
println("Connected to ${address.host}:${address.port}")
|
|
||||||
return inetTransportDevice(socket)
|
return inetTransportDevice(socket)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun String.toNetworkAddress(): NetworkAddress {
|
||||||
|
val (host, port) = this.split(":").map { it.trim() }
|
||||||
|
return NetworkAddress(host, port.toInt())
|
||||||
|
}
|
||||||
|
|
||||||
private fun inetTransportDevice(
|
private fun inetTransportDevice(
|
||||||
sock: Socket,
|
sock: Socket,
|
||||||
suffix: String = "cli",
|
suffix: String = "cli",
|
||||||
): InetTransportDevice {
|
): InetTransportDevice {
|
||||||
val networkAddress = sock.remoteAddress.toJavaAddress().let { NetworkAddress(it.hostname, it.port) }
|
val networkAddress = (sock.remoteAddress as InetSocketAddress).let { NetworkAddress(it.hostname, it.port) }
|
||||||
val inputBlocks = Channel<UByteArray>(4096)
|
val inputBlocks = Channel<UByteArray>(4096)
|
||||||
val outputBlocks = Channel<UByteArray>(4096)
|
val outputBlocks = Channel<UByteArray>(4096)
|
||||||
|
|
||||||
val log = LogTag("TCPT${logCounter.incrementAndGet()}:$suffix:$networkAddress")
|
val log = LogTag("TCPT${logCounter.incrementAndGet()}:$suffix:$networkAddress")
|
||||||
|
val stopCalled = AtomicValue(false)
|
||||||
|
|
||||||
fun stop() {
|
fun stop() {
|
||||||
log.info { "stopping" }
|
stopCalled.mutate {
|
||||||
runCatching { inputBlocks.close()}
|
if (!it) {
|
||||||
runCatching { outputBlocks.close()}
|
log.debug { "stopping" }
|
||||||
if( !sock.isClosed ) runCatching { sock.close()}
|
runCatching { inputBlocks.close() }
|
||||||
|
runCatching { outputBlocks.close() }
|
||||||
|
if (!sock.isClosed)
|
||||||
|
runCatching {
|
||||||
|
log.debug { "closing socket by stop" }
|
||||||
|
sock.close()
|
||||||
|
}
|
||||||
|
else
|
||||||
|
log.debug { "socket is already closed when stop is called" }
|
||||||
|
} else
|
||||||
|
log.debug { "already stopped" }
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sock.launch {
|
sock.launch {
|
||||||
log.debug { "opening read channel" }
|
log.debug { "opening read channel" }
|
||||||
val sockInput = runCatching { sock.openReadChannel() }.getOrElse {
|
val sockInput = runCatching { sock.openReadChannel() }.getOrElse {
|
||||||
log.warning { "failed to open read channel $it" }
|
log.warning { "failed to open read channel $it" }
|
||||||
sock.close()
|
stop()
|
||||||
throw IllegalStateException("failed to open read channel")
|
throw IllegalStateException("failed to open read channel")
|
||||||
}
|
}
|
||||||
while (isActive && sock.isActive) {
|
while (isActive && sock.isActive) {
|
||||||
@ -82,15 +86,16 @@ private fun inetTransportDevice(
|
|||||||
val size = AsyncVarint.decodeUnsigned(sockInput).toInt()
|
val size = AsyncVarint.decodeUnsigned(sockInput).toInt()
|
||||||
if (size > MAX_TCP_BLOCK_SIZE) // 16M is a max command block
|
if (size > MAX_TCP_BLOCK_SIZE) // 16M is a max command block
|
||||||
throw ProtocolException("Illegal block size: $size should be < $MAX_TCP_BLOCK_SIZE")
|
throw ProtocolException("Illegal block size: $size should be < $MAX_TCP_BLOCK_SIZE")
|
||||||
log.info { "read size: $size" }
|
|
||||||
val data = ByteArray(size)
|
val data = ByteArray(size)
|
||||||
log.info { "data ready" }
|
|
||||||
sockInput.readFully(data, 0, size)
|
sockInput.readFully(data, 0, size)
|
||||||
inputBlocks.send(data.toUByteArray())
|
inputBlocks.send(data.toUByteArray())
|
||||||
} catch (e: ClosedReceiveChannelException) {
|
} catch (e: ClosedReceiveChannelException) {
|
||||||
|
log.error { "closed receive channel " }
|
||||||
stop()
|
stop()
|
||||||
break
|
break
|
||||||
} catch (_: CancellationException) {
|
} catch (_: CancellationException) {
|
||||||
|
log.error { "cancellation exception " }
|
||||||
|
break
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
log.exception { "unexpected exception in TCP socket read" to e }
|
log.exception { "unexpected exception in TCP socket read" to e }
|
||||||
stop()
|
stop()
|
||||||
@ -105,16 +110,17 @@ private fun inetTransportDevice(
|
|||||||
val block = outputBlocks.receive()
|
val block = outputBlocks.receive()
|
||||||
AsyncVarint.encodeUnsigned(block.size.toULong(), sockOutput)
|
AsyncVarint.encodeUnsigned(block.size.toULong(), sockOutput)
|
||||||
sockOutput.writeFully(block.toByteArray(), 0, block.size)
|
sockOutput.writeFully(block.toByteArray(), 0, block.size)
|
||||||
log.info { "Client sock output: ${block.size}" }
|
|
||||||
sockOutput.flush()
|
sockOutput.flush()
|
||||||
} catch (_: CancellationException) {
|
} catch (_: CancellationException) {
|
||||||
log.info { "Caught cancellation, closing transport" }
|
log.debug { "cancellation exception on output" }
|
||||||
|
stop()
|
||||||
|
break
|
||||||
} catch (_: LocalInterface.BreakConnectionException) {
|
} catch (_: LocalInterface.BreakConnectionException) {
|
||||||
log.info { "requested connection break" }
|
log.debug { "requested connection break" }
|
||||||
stop()
|
stop()
|
||||||
break
|
break
|
||||||
} catch (_: ClosedReceiveChannelException) {
|
} catch (_: ClosedReceiveChannelException) {
|
||||||
log.info { "receive block channel closed, closing the socket" }
|
log.debug { "receive block channel closed, closing the socket" }
|
||||||
stop()
|
stop()
|
||||||
break
|
break
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@ -124,10 +130,10 @@ private fun inetTransportDevice(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val device = InetTransportDevice(inputBlocks, outputBlocks, networkAddress, {
|
val device = InetTransportDevice(inputBlocks, outputBlocks, networkAddress, {
|
||||||
log.info { "Close has been called" }
|
|
||||||
stop()
|
stop()
|
||||||
})
|
})
|
||||||
log.info { "Transport ready" }
|
log.debug { "Transport ready" }
|
||||||
return device
|
return device
|
||||||
}
|
}
|
||||||
|
77
src/ktorSocketTest/kotlin/TcpTest.kt
Normal file
77
src/ktorSocketTest/kotlin/TcpTest.kt
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import net.sergeych.crypto2.initCrypto
|
||||||
|
import net.sergeych.kiloparsec.*
|
||||||
|
import net.sergeych.kiloparsec.adapter.acceptTcpDevice
|
||||||
|
import net.sergeych.kiloparsec.adapter.connectTcpDevice
|
||||||
|
import net.sergeych.mp_logger.Log
|
||||||
|
import kotlin.random.Random
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertIs
|
||||||
|
|
||||||
|
class TcpTest {
|
||||||
|
class TestException : Exception("test1")
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun tcpTest() = runTest {
|
||||||
|
initCrypto()
|
||||||
|
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 cli = KiloInterface<Session>().apply {
|
||||||
|
registerError { TestException() }
|
||||||
|
onConnected { session.data = "start" }
|
||||||
|
on(cmdSave) { session.data = it }
|
||||||
|
on(cmdLoad) {
|
||||||
|
session.data
|
||||||
|
}
|
||||||
|
on(cmdException) {
|
||||||
|
throw TestException()
|
||||||
|
}
|
||||||
|
on(cmdDrop) {
|
||||||
|
throw LocalInterface.BreakConnectionException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val server = KiloServer(cli, acceptTcpDevice(port)) {
|
||||||
|
Session("unknown")
|
||||||
|
}
|
||||||
|
|
||||||
|
val client = KiloClient<Unit>() {
|
||||||
|
addErrors(cli)
|
||||||
|
connect { connectTcpDevice("localhost:$port") }
|
||||||
|
}
|
||||||
|
delay(500)
|
||||||
|
|
||||||
|
assertEquals("start", client.call(cmdLoad))
|
||||||
|
|
||||||
|
client.call(cmdSave, "foobar")
|
||||||
|
assertEquals("foobar", client.call(cmdLoad))
|
||||||
|
|
||||||
|
val res = kotlin.runCatching { client.call(cmdException) }
|
||||||
|
println(res.exceptionOrNull())
|
||||||
|
assertIs<TestException>(res.exceptionOrNull())
|
||||||
|
assertEquals("foobar", client.call(cmdLoad))
|
||||||
|
|
||||||
|
println("----------------------------------- pre drops")
|
||||||
|
assertThrows<RemoteInterface.ClosedException> { client.call(cmdDrop) }
|
||||||
|
|
||||||
|
println("----------------------------------- DROPPED")
|
||||||
|
|
||||||
|
// reconnect?
|
||||||
|
assertEquals("start", client.call(cmdLoad))
|
||||||
|
|
||||||
|
println("------------------------------=---- RECONNECTED")
|
||||||
|
server.close()
|
||||||
|
println("****************************************************************")
|
||||||
|
}
|
||||||
|
}
|
@ -1,2 +0,0 @@
|
|||||||
package net.sergeych.kiloparsec.adapter
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
|||||||
package net.sergeych.kiloparsec.adapter
|
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
actual fun NetworkAddress(host: String, port: Int): NetworkAddress {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
actual fun acceptTcpDevice(port: Int): Flow<InetTransportDevice> {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
actual suspend fun connectTcpDevice(address: NetworkAddress): InetTransportDevice {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user