Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26564b6081 | ||
|
|
9ddb1209c9 | ||
|
|
b68232653a | ||
|
|
2e4f551e8e | ||
|
|
40b8723132 | ||
|
|
4d178d951f | ||
|
|
f6fbf8e58e | ||
|
|
3c915a8f58 | ||
|
|
515278c264 | ||
|
|
7100fa5f76 | ||
|
|
77e293bef2 | ||
|
|
d6f257de14 | ||
|
|
8a21a836e5 |
No files matched your search
+1
-2
@@ -23,8 +23,7 @@ out/
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
bin/
|
||||
.sts4-caches
|
||||
!**/src/main/**/bin/
|
||||
!**/src/test/**/bin/
|
||||
|
||||
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="kiloparsec-js-0.2.4">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="kiloparsec-js-0.2.4.jar">
|
||||
<element id="module-output" name="kiloparsec.jsMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="kiloparsec-js-0.2.5">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="kiloparsec-js-0.2.5.jar">
|
||||
<element id="module-output" name="kiloparsec.jsMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="kiloparsec-js-0.2.5-SNAPSHOT">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="kiloparsec-js-0.2.5-SNAPSHOT.jar">
|
||||
<element id="module-output" name="kiloparsec.jsMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="kiloparsec-js-0.2.6">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="kiloparsec-js-0.2.6.jar" />
|
||||
</artifact>
|
||||
</component>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="kiloparsec-js-0.3.1">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="kiloparsec-js-0.3.1.jar">
|
||||
<element id="module-output" name="kiloparsec.jsMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="kiloparsec-js-0.3.1-SNAPSHOT">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="kiloparsec-js-0.3.1-SNAPSHOT.jar">
|
||||
<element id="module-output" name="kiloparsec.jsMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="kiloparsec-js-0.3.2">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="kiloparsec-js-0.3.2.jar">
|
||||
<element id="module-output" name="kiloparsec.jsMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="kiloparsec-js-0.3.3">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="kiloparsec-js-0.3.3.jar">
|
||||
<element id="module-output" name="kiloparsec.jsMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="kiloparsec-jvm-0.2.4">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="kiloparsec-jvm-0.2.4.jar">
|
||||
<element id="module-output" name="kiloparsec.jvmMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="kiloparsec-jvm-0.2.5">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="kiloparsec-jvm-0.2.5.jar">
|
||||
<element id="module-output" name="kiloparsec.jvmMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="kiloparsec-jvm-0.2.5-SNAPSHOT">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="kiloparsec-jvm-0.2.5-SNAPSHOT.jar">
|
||||
<element id="module-output" name="kiloparsec.jvmMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="kiloparsec-jvm-0.2.6">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="kiloparsec-jvm-0.2.6.jar" />
|
||||
</artifact>
|
||||
</component>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="kiloparsec-jvm-0.3.1">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="kiloparsec-jvm-0.3.1.jar">
|
||||
<element id="module-output" name="kiloparsec.jvmMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="kiloparsec-jvm-0.3.1-SNAPSHOT">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="kiloparsec-jvm-0.3.1-SNAPSHOT.jar">
|
||||
<element id="module-output" name="kiloparsec.jvmMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="kiloparsec-jvm-0.3.2">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="kiloparsec-jvm-0.3.2.jar">
|
||||
<element id="module-output" name="kiloparsec.jvmMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="kiloparsec-jvm-0.3.3">
|
||||
<output-path>$PROJECT_DIR$/build/libs</output-path>
|
||||
<root id="archive" name="kiloparsec-jvm-0.3.3.jar">
|
||||
<element id="module-output" name="kiloparsec.jvmMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="MarkdownSettings">
|
||||
<option name="showProblemsInCodeBlocks" value="false" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ScalaCompilerConfiguration">
|
||||
<option name="separateProdTestSources" value="false" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+124
@@ -0,0 +1,124 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Palette2">
|
||||
<group name="Swing">
|
||||
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
|
||||
</item>
|
||||
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
|
||||
</item>
|
||||
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
|
||||
</item>
|
||||
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.svg" removable="false" auto-create-binding="false" can-attach-label="true">
|
||||
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
|
||||
</item>
|
||||
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
|
||||
<initial-values>
|
||||
<property name="text" value="Button" />
|
||||
</initial-values>
|
||||
</item>
|
||||
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
|
||||
<initial-values>
|
||||
<property name="text" value="RadioButton" />
|
||||
</initial-values>
|
||||
</item>
|
||||
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
|
||||
<initial-values>
|
||||
<property name="text" value="CheckBox" />
|
||||
</initial-values>
|
||||
</item>
|
||||
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
|
||||
<initial-values>
|
||||
<property name="text" value="Label" />
|
||||
</initial-values>
|
||||
</item>
|
||||
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
|
||||
<preferred-size width="150" height="-1" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
|
||||
<preferred-size width="150" height="-1" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
|
||||
<preferred-size width="150" height="-1" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
|
||||
</item>
|
||||
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
|
||||
<preferred-size width="200" height="200" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
|
||||
<preferred-size width="200" height="200" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
|
||||
</item>
|
||||
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
|
||||
</item>
|
||||
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
|
||||
</item>
|
||||
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
|
||||
</item>
|
||||
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
|
||||
<preferred-size width="-1" height="20" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
|
||||
</item>
|
||||
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
|
||||
</item>
|
||||
</group>
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,41 +1,62 @@
|
||||
# Kiloparsec
|
||||
|
||||
__Recommended version is `0.4.1`: to keep the code compatible with current and further versions we
|
||||
ask to upgrade to `0.4.2` at least.__ Starting from this version some pacakage names are changed for
|
||||
better clarity and fast UDP endpoints are added.
|
||||
|
||||
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:
|
||||
|
||||
| name | JVM | JS | native |
|
||||
|----------------|-----|----|----------|
|
||||
| TCP/IP server | ✓ | | β @0.2.6 |
|
||||
| TCP/IP client | ✓ | | β @0.2.6 |
|
||||
|----------------|--------|----|--------|
|
||||
| TCP/IP server | ✓ | | 0.2.6+ |
|
||||
| TCP/IP client | ✓ | | 0.2.6+ |
|
||||
| UDP server | 0.3.2+ | | 0.3.2+ |
|
||||
| UDP client | 0.3.2+ | | 0.3.2+ |
|
||||
| Websock server | ✓ | | |
|
||||
| Websock client | ✓ | ✓ | ✓ |
|
||||
|
||||
### Note on version compatibility
|
||||
|
||||
Protocols >= 0.3.0 are not binary compatible with previous version due to more compact binary
|
||||
format. The format from 0.3.0 onwards is supposed to keep compatible.
|
||||
|
||||
#### ID calculation algorithm is changed since 0.4.1
|
||||
|
||||
We recommend to upgrade to 0.4+ ASAP as public/shared key id derivation method was changed for even higher security.
|
||||
|
||||
### Supported native targets
|
||||
|
||||
- iosArm64, iosX64
|
||||
- macosArm64, macosArm64
|
||||
- linxArm64, linuxX64
|
||||
- linuxArm64, linuxX64
|
||||
|
||||
### Non-native targets
|
||||
|
||||
- JS (browser and nodeJS)
|
||||
- JVM (android, macos, windows, linx, everywhere where JRE is installed)
|
||||
|
||||
## TCP/IP transport
|
||||
## TCP/IP and UDP transports
|
||||
|
||||
It is the fastest based on async socket implementation of ktor client. It works everywhere but JS target as
|
||||
These are the fastest based on async socket implementation of ktor client. They works everywhere but JS target as
|
||||
there is currently no widely adopted sockets for browser javascript.
|
||||
|
||||
While UDP is faster than TCP/IP, it is less reliable, especially with commands and return values that serializes to more than 240 bytes approx, and has no retransmission facilities (use TCP!). UDP though shines when all you need is to [push](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec/-remote-interface/push.html) with little or no data in it.
|
||||
|
||||
## 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
|
||||
While it is much slower than TCP or UDP, it is still faster than any http-based API; 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.
|
||||
|
||||
## Websocket client
|
||||
|
||||
It is slower than TCP or UDP, but it works on literally all platforms. See the sample below.
|
||||
|
||||
# Usage
|
||||
|
||||
The library should be used as maven dependency, not as source.
|
||||
@@ -59,7 +80,7 @@ It could be, depending on your project structure, something like:
|
||||
```kotlin
|
||||
val commonMain by getting {
|
||||
dependencies {
|
||||
api("net.sergeych:kiloparsec:0.2.6")
|
||||
api("net.sergeych:kiloparsec:0.4.1")
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -118,7 +139,7 @@ assertEquals(FooArgs("bar", 117), client.call(cmdGetFoo))
|
||||
## Create ktor-based server
|
||||
|
||||
Normally server side needs some session. It is convenient and avoid sending repeating data on each request speeding up
|
||||
the protocol. With KILOPARSEC it is rather basic operation:\
|
||||
the protocol. With KILOPARSEC it is rather basic operation:
|
||||
|
||||
~~~kotlin
|
||||
// Our session just keeps Foo for cmd{Get|Set}Foo:
|
||||
@@ -143,9 +164,74 @@ val ns: NettyApplicationEngine = embeddedServer(Netty, port = 8080, host = "0.0.
|
||||
setupWebsocketServer(serverInterface) { Session() }
|
||||
}).start(wait = false)
|
||||
|
||||
|
||||
~~~
|
||||
|
||||
## Create TCP/IP client and server
|
||||
|
||||
Using plain TCP/IP is even simpler, and it works way faster than websocket one, and is _the same
|
||||
protected as `wss://` (and `ws://`) variant above due to same kiloparsec encryption in both cases. Still, a TCP/IP
|
||||
client is not available in Javascript browser targets and custom TCP ports could often be blocked by firewalls.
|
||||
|
||||
Documentation is available in samples here:
|
||||
|
||||
- [TCP/IP server creation](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec/-kilo-server/index.html)
|
||||
|
||||
- [TCP/IP client](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec/-kilo-client/index.html)
|
||||
|
||||
In short, there are two functions that implements asynchronous TCP/IP transport on all platforms buy JS:
|
||||
|
||||
- [acceptTcpDevice](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec.adapter/accept-tcp-device.html?query=fun%20acceptTcpDevice(port:%20Int):%20Flow%3CInetTransportDevice%3E) to create a server
|
||||
|
||||
- [connectTcpDevice](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec.adapter/connect-tcp-device.html) to connect to the server
|
||||
|
||||
## UDP client and server
|
||||
|
||||
Is very much straightforward, same as with TCP/IP:
|
||||
|
||||
- [UDP server creation](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec.adapter/accept-udp-device.html)
|
||||
- [Connect UDP client](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec.adapter/connect-udp-device.html)
|
||||
|
||||
### UDP specifics
|
||||
|
||||
#### Command size
|
||||
|
||||
Each command invocation and result are packed in a separate UDP diagram using effective binary packing.
|
||||
Thus for the best results commands and results should be relatively short, best to fit into 240 bytes. While bigger datagrams are often transmitted successfully, the probability of the effective transmission drops with the size.
|
||||
|
||||
Kiloparsec UDP transport does not retransmits not delivered packets. Use TCP/IP or websocket if it is a concern.
|
||||
|
||||
For the best results we recommend using [push](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec/-remote-interface/index.html#1558240250%2FFunctions%2F788909594) for remote interfaces with UDP.
|
||||
|
||||
#### Timeouts
|
||||
|
||||
As Datagrams do not form protocol itself, kiloparsec issues pings when no data is circulated between parties.
|
||||
When no pings are received long enough, kiloparsec connection is closed. There are `maxInactivityTimeout` in all
|
||||
relevant functions and constructors.
|
||||
|
||||
Client shoudl not issue pings manually.
|
||||
|
||||
## Reusing code between servers
|
||||
|
||||
The same instance of the [KiloInterface](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec/-kilo-interface/index.html?query=open%20class%20KiloInterface%3CS%3E%20:%20LocalInterface%3CKiloScope%3CS%3E%3E) could easily be reused with all instances of servers with different protocols.
|
||||
|
||||
This is a common proactive to create a business logic in a `KiloInterface`, then create a TCP/IP and Websocket servers passing the same instance of the logic to both.
|
||||
|
||||
## Note on the server identification
|
||||
|
||||
We do not recommend to rely on TLS (HTTPS://, WSS://) host identification solely, in the modern world there is
|
||||
a high probability of attacks on unfriendly (in respect to at least some of your users) states to the SSL certificates
|
||||
chain, in which case the MITM and spoofing will be undetected. Check the [remoteId](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec/-kilo-client/remote-id.html?query=suspend%20fun%20remoteId():%20VerifyingPublicKey?) in your client on each connection and provide the safe [serverSecretKey](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec/-kilo-server/index.html) when creating a server.
|
||||
|
||||
This will effectively protetcs against certificate chain spoofing in the case of the application installed from the trusted source.
|
||||
|
||||
__Important note__. The web application could not be completely secured this way unless is loaded from the IP-address, as the DNS could be spoofed the same, especially when used with `Cloudflare` or other CDN that can
|
||||
transparently substitute the whole site. In the case of we applications we strongly recommend not to use CDN except your own where you can control actual traffic rules.
|
||||
|
||||
## See also:
|
||||
|
||||
- [Source documentation](https://code.sergeych.net/docs/kiloparsec/)
|
||||
- [Project's WIKI](https://gitea.sergeych.net/SergeychWorks/kiloparsec/wiki)
|
||||
|
||||
# Details
|
||||
|
||||
It is not compatible with parsec family and no more based on an Universa crypto library. To better fit
|
||||
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
./gradlew dokkaHtml
|
||||
rsync -avz ./build/dokka/* code.sergeych.net:/bigstore/sergeych_pub/code/docs/kiloparsec
|
||||
+14
-3
@@ -1,11 +1,12 @@
|
||||
plugins {
|
||||
kotlin("multiplatform") version "2.0.0"
|
||||
kotlin("multiplatform") version "2.0.10"
|
||||
id("org.jetbrains.kotlin.plugin.serialization") version "2.0.0"
|
||||
`maven-publish`
|
||||
id("org.jetbrains.dokka") version "1.9.20"
|
||||
}
|
||||
|
||||
group = "net.sergeych"
|
||||
version = "0.2.6"
|
||||
version = "0.4.1"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
@@ -43,7 +44,7 @@ kotlin {
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1")
|
||||
api("io.ktor:ktor-client-core:$ktor_version")
|
||||
api("net.sergeych:crypto2:0.4.2")
|
||||
api("net.sergeych:crypto2:0.5.3")
|
||||
}
|
||||
}
|
||||
val ktorSocketMain by creating {
|
||||
@@ -135,3 +136,13 @@ kotlin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.dokkaHtml.configure {
|
||||
outputDirectory.set(buildDir.resolve("dokka"))
|
||||
dokkaSourceSets {
|
||||
configureEach {
|
||||
// includes.from("docs/bipack.md")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,11 @@ inline fun <reified A, reified R> command(overrideName: String? = null): Command
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Declare a Push: Unit-returning command usually used with [RemoteInterface.push]
|
||||
*/
|
||||
inline fun <reified A> push(overrideName: String? = null): CommandDelegate<A, Unit> = command(overrideName)
|
||||
|
||||
/**
|
||||
* Delegate to create [Command] via property
|
||||
*/
|
||||
|
||||
@@ -17,10 +17,33 @@ import net.sergeych.mp_logger.exception
|
||||
import net.sergeych.mp_tools.globalLaunch
|
||||
|
||||
/**
|
||||
* The auto-connecting client that reconnects to the kiloparsec server
|
||||
* The auto-connecting client that reconnects to the kiloparsec server,
|
||||
* [KiloServer],
|
||||
* and maintain connection state flow. Client factory launches a disconnected
|
||||
* set of coroutines to support automatic reconnection, so you _must_ [close]
|
||||
* it manually when it is not needed, otherwise it will continue to reconnect.
|
||||
* it manually when it is unnecessary, otherwise it will continue to reconnect.
|
||||
*
|
||||
* ## Usage
|
||||
*
|
||||
* Suppose we have TCP/IP server as in the [KiloServer] usage sample. Then we can connect
|
||||
* to it providing TCP/IP connector like:
|
||||
*
|
||||
* ```kotlin
|
||||
* val client = KiloClient<Unit>() {
|
||||
* connect { connectTcpDevice("localhost:$port") }
|
||||
* }
|
||||
*
|
||||
* // now we can invoke remote commands:
|
||||
* assertEquals("unknown", client.call(cmdLoad))
|
||||
*
|
||||
* client.call(cmdSave, "foobar")
|
||||
* assertEquals("foobar", client.call(cmdLoad))
|
||||
* ```
|
||||
*
|
||||
* ## See also
|
||||
*
|
||||
* [KiloServer]
|
||||
*
|
||||
*/
|
||||
class KiloClient<S>(
|
||||
val localInterface: KiloInterface<S>,
|
||||
@@ -28,7 +51,6 @@ class KiloClient<S>(
|
||||
connectionDataFactory: ConnectionDataFactory<S>,
|
||||
) : RemoteInterface,
|
||||
Loggable by LogTag("CLIF") {
|
||||
|
||||
val _state = MutableStateFlow(false)
|
||||
|
||||
/**
|
||||
@@ -67,6 +89,7 @@ class KiloClient<S>(
|
||||
}
|
||||
_state.value = false
|
||||
resetDeferredClient()
|
||||
// reconnection timeout
|
||||
delay(100)
|
||||
}
|
||||
}
|
||||
@@ -94,6 +117,15 @@ class KiloClient<S>(
|
||||
throw t
|
||||
}
|
||||
|
||||
override suspend fun <A> push(cmd: Command<A, Unit>, args: A) {
|
||||
try {
|
||||
deferredClient.await().push(cmd, args)
|
||||
} catch (t: RemoteInterface.ClosedException) {
|
||||
resetDeferredClient()
|
||||
throw t
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Current session token. This is a per-connection unique random value same on the client and server part so
|
||||
* it could be used as a nonce to pair MITM and like attacks, be sure that the server is actually
|
||||
|
||||
@@ -100,4 +100,8 @@ class KiloClientConnection<S>(
|
||||
suspend fun token() = deferredParams.await().token
|
||||
override suspend fun <A, R> call(cmd: Command<A, R>, args: A): R =
|
||||
kiloRemoteInterface.await().call(cmd, args)
|
||||
|
||||
override suspend fun <A> push(cmd: Command<A, Unit>, args: A) {
|
||||
kiloRemoteInterface.await().push(cmd, args)
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,13 @@ package net.sergeych.kiloparsec
|
||||
* The local interface to provide functions, register errors for Kiloparsec users. Use it
|
||||
* with [KiloClient], [KiloClientConnection], [KiloServerConnection], etc.
|
||||
*
|
||||
* BAse implementation registers relevant exceptions.
|
||||
* Base class implementation does the following:
|
||||
*
|
||||
* - It registers common exceptions from [RemoteInterface] and kotlin/java `IllegalArgumentException` and
|
||||
* `IllegalStateException`
|
||||
* - It provides [onConnected] handler
|
||||
*
|
||||
* See [KiloServer] for usage sample.
|
||||
*/
|
||||
open class KiloInterface<S> : LocalInterface<KiloScope<S>>() {
|
||||
|
||||
|
||||
@@ -35,5 +35,10 @@ class KiloRemoteInterface<S>(
|
||||
else -> throw RemoteInterface.Exception("unexpected block type: $block")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun <A> push(cmd: Command<A, Unit>, args: A) {
|
||||
val params = deferredParams.await()
|
||||
params.transport.call(L0Call, params.encrypt(cmd.packCall(args)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,94 @@ 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")
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* See [KiloClient] to connect to the server.
|
||||
*
|
||||
* @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>,
|
||||
@@ -29,20 +116,28 @@ class KiloServer<S>(
|
||||
KiloServerConnection(clientInterface, device, sessionBuilder(), serverSecretKey)
|
||||
.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
|
||||
}
|
||||
@@ -94,4 +94,8 @@ class KiloServerConnection<S>(
|
||||
override suspend fun <A, R> call(cmd: Command<A, R>, args: A): R {
|
||||
return kiloRemoteInterface.await().call(cmd, args)
|
||||
}
|
||||
|
||||
override suspend fun <A> push(cmd: Command<A, Unit>, args: A) {
|
||||
kiloRemoteInterface.await().push(cmd, args)
|
||||
}
|
||||
}
|
||||
@@ -74,7 +74,11 @@ open class LocalInterface<S> : Loggable by LogTag("LocalInterface${idCounter.inc
|
||||
private val errorByClass = mutableMapOf<KClass<*>, String>()
|
||||
private val errorBuilder = mutableMapOf<String, (String, UByteArray?) -> Throwable>()
|
||||
|
||||
fun <T : Throwable> registerError(
|
||||
/**
|
||||
* Register exception for automatic transmission over the kiloparsec connection using its [klass]
|
||||
* and possibly override code. In most cases it is simpler to use [registerError].
|
||||
*/
|
||||
fun <T : Throwable> registerErrorClass(
|
||||
klass: KClass<T>, code: String = klass.simpleName!!,
|
||||
exceptionBuilder: (String, UByteArray?) -> T,
|
||||
) {
|
||||
@@ -82,10 +86,17 @@ open class LocalInterface<S> : Loggable by LogTag("LocalInterface${idCounter.inc
|
||||
errorBuilder[code] = exceptionBuilder
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an exception to be transmitted automatically over the kiloparsec connection.
|
||||
* Example:
|
||||
* ```kotlin
|
||||
* localInterface.registerError { IllegalArgumentException(it) }
|
||||
* ```
|
||||
*/
|
||||
inline fun <reified T : Throwable> registerError(
|
||||
noinline exceptionBuilder: (String) -> T,
|
||||
) {
|
||||
registerError(T::class) { msg, _ -> exceptionBuilder(msg) }
|
||||
registerErrorClass(T::class) { msg, _ -> exceptionBuilder(msg) }
|
||||
}
|
||||
|
||||
val errorProviders = mutableListOf<LocalInterface<*>>()
|
||||
|
||||
@@ -47,7 +47,30 @@ interface RemoteInterface {
|
||||
suspend fun <R> call(cmd: Command<Unit, R>): R = call(cmd, Unit)
|
||||
|
||||
/**
|
||||
* Call the remote procedure with specified args and return its result
|
||||
* Call the remote procedure with specified [args] and return its result of type [R]. The calling coroutine
|
||||
* suspends until the request is performed on the remote side and the result value (also `Unit`) will be received.
|
||||
*
|
||||
* When it is not necessary to wait for the return value and/or command execution, it is recommended to
|
||||
* use [push] instead.
|
||||
*
|
||||
* @throws RemoteException if the execution caused exception on the remote size
|
||||
* @throws Exception for registered exceptions, see [LocalInterface.registerError], etc.
|
||||
*/
|
||||
suspend fun <A, R> call(cmd: Command<A, R>, args: A): R
|
||||
|
||||
/**
|
||||
* Push the notification without waiting for reception or processing.
|
||||
* It returns immediately after sending data to the transport (e.g., to the network).
|
||||
* Use [call] if it is necessary to wait until the command will be received and processed by the remote.
|
||||
*
|
||||
* Push is onlu available for commands without returned value.
|
||||
*/
|
||||
suspend fun <A> push(cmd: Command<A, Unit>, args: A)
|
||||
|
||||
/**
|
||||
* Push the command with no args.
|
||||
* It returns immediately after sending data to the transport (e.g., to the network).
|
||||
* Use [call] if it is necessary to wait until the command will be received and processed by the remote.
|
||||
*/
|
||||
suspend fun push(cmd: Command<Unit,Unit>) = push(cmd,Unit)
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.serializer
|
||||
import net.sergeych.bipack.Unsigned
|
||||
import net.sergeych.crypto2.toDump
|
||||
import net.sergeych.kiloparsec.Transport.Device
|
||||
import net.sergeych.mp_logger.*
|
||||
@@ -63,7 +64,12 @@ class Transport<S>(
|
||||
@Serializable(TransportBlockSerializer::class)
|
||||
sealed class Block {
|
||||
@Serializable
|
||||
data class Call(val id: UInt, val name: String, val packedArgs: UByteArray) : Block() {
|
||||
data class Call(
|
||||
@Unsigned
|
||||
val id: UInt,
|
||||
val name: String,
|
||||
val packedArgs: UByteArray
|
||||
) : Block() {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is Call) return false
|
||||
@@ -83,10 +89,10 @@ class Transport<S>(
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Response(val forId: UInt, val packedResult: UByteArray) : Block()
|
||||
data class Response(@Unsigned val forId: UInt, val packedResult: UByteArray) : Block()
|
||||
|
||||
@Serializable
|
||||
data class Error(val forId: UInt, val code: String, val text: String? = null, val extra: UByteArray? = null) :
|
||||
data class Error(@Unsigned val forId: UInt, val code: String, val text: String? = null, val extra: UByteArray? = null) :
|
||||
Block() {
|
||||
val message by lazy { text ?: "remote exception: $code" }
|
||||
}
|
||||
@@ -98,7 +104,8 @@ class Transport<S>(
|
||||
var isClosed: Boolean = false
|
||||
|
||||
/**
|
||||
* Send a call block for a command and packed args and return packed result if it is not an error
|
||||
* Send a call block for a command and packed args and return packed result if it is not an error. It suspends
|
||||
* until receiving answer from the remote side, even if returns `Unit`.
|
||||
* @throws RemoteInterface.RemoteException if the remote call caused an exception. Normally use [call] instead.
|
||||
* @throws RemoteInterface.ClosedException
|
||||
*/
|
||||
@@ -111,6 +118,7 @@ class Transport<S>(
|
||||
// We need to shield calls and lastID with mutex, but nothing more:
|
||||
access.withLock {
|
||||
if (isClosed) throw RemoteInterface.ClosedException()
|
||||
// the order is important: first id in use MUST BE >= 1, not zero:
|
||||
b = Block.Call(++lastId, name, packedArgs)
|
||||
calls[b.id] = deferred
|
||||
}
|
||||
@@ -131,6 +139,25 @@ class Transport<S>(
|
||||
return deferred.await()
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a call block for a command and packed args and return packed result if it is not an error. It suspends
|
||||
* until receiving answer from the remote side, even if returns `Unit`.
|
||||
* @throws RemoteInterface.RemoteException if the remote call caused an exception. Normally use [call] instead.
|
||||
* @throws RemoteInterface.ClosedException
|
||||
*/
|
||||
private suspend fun sendPushBlock(name: String, packedArgs: UByteArray) {
|
||||
if (isClosed) throw RemoteInterface.ClosedException()
|
||||
|
||||
// All push blocks have the same id == 0:
|
||||
val b = Block.Call(0u, name, packedArgs)
|
||||
val r = runCatching { device.output.send(pack(b).also { debug { ">>$\n${it.toDump()}" } }) }
|
||||
when(val e = r.exceptionOrNull()) {
|
||||
is RemoteInterface.ClosedException, is CancellationException, is RemoteInterface.RemoteException
|
||||
-> throw e
|
||||
else -> throw RemoteInterface.ClosedException()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the remote procedure with specified args and return its result
|
||||
*/
|
||||
@@ -139,6 +166,10 @@ class Transport<S>(
|
||||
return unpack(cmd.resultSerializer, result)
|
||||
}
|
||||
|
||||
override suspend fun <A>push(cmd: Command<A,Unit>,args: A) {
|
||||
sendPushBlock(cmd.name, pack(cmd.argsSerializer, args))
|
||||
}
|
||||
|
||||
/**
|
||||
* Start running the transport. This function suspends until the transport is closed
|
||||
* normally or by error. If you need to cancel it prematurely, cancel the coroutine
|
||||
@@ -176,6 +207,10 @@ class Transport<S>(
|
||||
|
||||
is Block.Call -> launch {
|
||||
try {
|
||||
if (b.id == 0u)
|
||||
// Command does not waits return
|
||||
localInterface.execute(commandContext, b.name, b.packedArgs)
|
||||
else
|
||||
send(
|
||||
Block.Response(
|
||||
b.id,
|
||||
@@ -196,7 +231,6 @@ class Transport<S>(
|
||||
.also { debug { "command executed: ${b.name}" } }
|
||||
}
|
||||
}
|
||||
debug { "=---------------------------------------------"}
|
||||
}
|
||||
debug { "input step performed closed=$isClosed active=$isActive" }
|
||||
} catch (_: ClosedSendChannelException) {
|
||||
@@ -205,17 +239,14 @@ class Transport<S>(
|
||||
} catch (_: ClosedReceiveChannelException) {
|
||||
info { "closed receive channel" }
|
||||
isClosed = true
|
||||
}
|
||||
catch(cce: LocalInterface.BreakConnectionException) {
|
||||
} catch (cce: LocalInterface.BreakConnectionException) {
|
||||
info { "closing connection by local request ($cce)" }
|
||||
device.close()
|
||||
}
|
||||
catch(t: RemoteInterface.ClosedException) {
|
||||
} catch (t: RemoteInterface.ClosedException) {
|
||||
// it is ok: we just exit the coroutine normally
|
||||
// and mark we're closing
|
||||
isClosed = true
|
||||
}
|
||||
catch (_: CancellationException) {
|
||||
} catch (_: CancellationException) {
|
||||
info { "loop is cancelled with CancellationException" }
|
||||
isClosed = true
|
||||
} catch (t: Throwable) {
|
||||
@@ -243,8 +274,7 @@ class Transport<S>(
|
||||
private suspend fun send(block: Block) {
|
||||
try {
|
||||
device.output.send(pack(block))
|
||||
}
|
||||
catch(_: ClosedSendChannelException) {
|
||||
} catch (_: ClosedSendChannelException) {
|
||||
throw RemoteInterface.ClosedException()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,46 +11,3 @@ data class NetworkAddress(
|
||||
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
|
||||
//
|
||||
//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,6 +1,24 @@
|
||||
package net.sergeych.kiloparsec
|
||||
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
fun String.encodeToUByteArray() =
|
||||
encodeToByteArray().toUByteArray()
|
||||
|
||||
fun UByteArray.decodeFromUByteArray(): String = toByteArray().decodeToString()
|
||||
class SyncValue<T>(initialValue: T) {
|
||||
private val access = Mutex()
|
||||
|
||||
var value = initialValue
|
||||
private set
|
||||
|
||||
suspend fun mutate(f: suspend (T)->T): T = access.withLock { f(value).also { value = it } }
|
||||
|
||||
@Suppress("unused")
|
||||
suspend fun getAndSet(newValue: T): T = mutate {
|
||||
val old = value
|
||||
value = newValue
|
||||
old
|
||||
}
|
||||
|
||||
}
|
||||
@@ -184,10 +184,16 @@ class TransportTest {
|
||||
val cmdRemoteExceptionTest by command<Unit, String>()
|
||||
val cmdBreak by command<Unit, Unit>()
|
||||
|
||||
val cmdPushServer by push<String>()
|
||||
val pushedFromServer = CompletableDeferred<String>()
|
||||
|
||||
val serverInterface = KiloInterface<String>().apply {
|
||||
on(cmdPing) {
|
||||
"pong! [$it]"
|
||||
}
|
||||
on(cmdPushServer) {
|
||||
pushedFromServer.complete(it)
|
||||
}
|
||||
on(cmdGetToken) {
|
||||
sessionToken
|
||||
}
|
||||
@@ -249,11 +255,17 @@ class TransportTest {
|
||||
assertEquals("client pong: foo", kiloServerConnection.call(cmdPing, "foo"))
|
||||
assertEquals("server push: bar", kiloServerConnection.call(cmdPush, "bar"))
|
||||
|
||||
client.push(cmdPushServer, "42")
|
||||
|
||||
assertEquals("**-s1-c1-s2-c2", client.call(cmdChainCallServer1, "**"))
|
||||
|
||||
|
||||
assertThrows<TestException> { client.call(cmdException) }
|
||||
assertEquals("ok: te-local", client.call(cmdRemoteExceptionTest))
|
||||
|
||||
// wait for push to be received and check
|
||||
assertEquals("42", pushedFromServer.await())
|
||||
|
||||
assertThrows<RemoteInterface.ClosedException> {
|
||||
client.call(cmdBreak)
|
||||
}
|
||||
|
||||
+16
-3
@@ -13,6 +13,8 @@ import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.datetime.Clock
|
||||
import net.sergeych.kiloparsec.AsyncVarint
|
||||
import net.sergeych.kiloparsec.KiloClient
|
||||
import net.sergeych.kiloparsec.KiloServer
|
||||
import net.sergeych.kiloparsec.LocalInterface
|
||||
import net.sergeych.mp_logger.*
|
||||
import net.sergeych.mp_tools.globalLaunch
|
||||
@@ -25,11 +27,15 @@ private val logCounter = AtomicCounter(0)
|
||||
class ProtocolException(text: String, cause: Throwable? = null) : RuntimeException(text, cause)
|
||||
|
||||
const val MAX_TCP_BLOCK_SIZE = 16776216
|
||||
val PING_INACTIVITY_TIME = 30.seconds
|
||||
internal val PING_INACTIVITY_TIME = 30.seconds
|
||||
|
||||
fun acceptTcpDevice(port: Int): Flow<InetTransportDevice> {
|
||||
/**
|
||||
* Listen for incoming TCP/IP connections on all local interfaces and the specified [port]
|
||||
* anc create flow of [InetTransportDevice] suitable for [KiloClient].
|
||||
*/
|
||||
fun acceptTcpDevice(port: Int,localInterface: String = "0.0.0.0"): Flow<InetTransportDevice> {
|
||||
val selectorManager = SelectorManager(Dispatchers.IO)
|
||||
val serverSocket = aSocket(selectorManager).tcp().bind("127.0.0.1", port)
|
||||
val serverSocket = aSocket(selectorManager).tcp().bind(localInterface, port)
|
||||
return flow {
|
||||
while (true) {
|
||||
serverSocket.accept().let { sock ->
|
||||
@@ -41,12 +47,19 @@ fun acceptTcpDevice(port: Int): Flow<InetTransportDevice> {
|
||||
|
||||
suspend fun connectTcpDevice(address: String) = connectTcpDevice(address.toNetworkAddress())
|
||||
|
||||
/**
|
||||
* Connect to the TCP/IP server (see [KiloServer]) at the specified address and provide th compatible
|
||||
* [InetTransportDevice] to use with [KiloClient].
|
||||
*/
|
||||
suspend fun connectTcpDevice(address: NetworkAddress): InetTransportDevice {
|
||||
val selectorManager = SelectorManager(Dispatchers.IO)
|
||||
val socket = aSocket(selectorManager).tcp().connect(address.host, address.port)
|
||||
return inetTransportDevice(socket)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse `host:port` string into the [NetworkAddress]
|
||||
*/
|
||||
fun String.toNetworkAddress(): NetworkAddress {
|
||||
val (host, port) = this.split(":").map { it.trim() }
|
||||
return NetworkAddress(host, port.toInt())
|
||||
@@ -0,0 +1,102 @@
|
||||
package net.sergeych.kiloparsec.adapter
|
||||
|
||||
import io.ktor.network.sockets.*
|
||||
import io.ktor.utils.io.core.*
|
||||
import net.sergeych.crypto2.toDump
|
||||
import net.sergeych.kiloparsec.adapter.UdpBlock.Companion.CANCEL_BLOCK
|
||||
import net.sergeych.kiloparsec.adapter.UdpBlock.Companion.ESCAPE_BYTE
|
||||
import net.sergeych.kiloparsec.adapter.UdpBlock.Companion.PING_BLOCK
|
||||
import net.sergeych.kiloparsec.adapter.UdpBlock.Companion.decode
|
||||
|
||||
/**
|
||||
* Encoded block for UDP datagram space-savvy. Minimum dara size is two bytes, which is fine
|
||||
* for Kiloparsec blocks.
|
||||
*
|
||||
* First byte is encoded using [ESCAPE_BYTE] depending on the second byte:
|
||||
*
|
||||
* | 0 | 1 | meaning |
|
||||
* |---|---|---------|
|
||||
* | [ESCAPE_BYTE] | [ESCAPE_BYTE] | Data block, dropping first byte |
|
||||
* | [ESCAPE_BYTE] | [PING_BLOCK] | Ping block, reset timers |
|
||||
* | [ESCAPE_BYTE] | [CANCEL_BLOCK] | close connection |
|
||||
* | any other | * | data block, all bytes |
|
||||
*
|
||||
* Use [encoded] and [toDatagram] to create binary or the datagram from a block, and [decode] to restore.
|
||||
*
|
||||
* We do not use serialization to speed up the transport layer.
|
||||
*/
|
||||
sealed class UdpBlock {
|
||||
/**
|
||||
* Block to show that the connection is closed and should also be closed on the other side
|
||||
*/
|
||||
object Cancel : UdpBlock()
|
||||
|
||||
/**
|
||||
* Parties show pings if there is no activity to keep it alive, detect connection loss and in some
|
||||
* cases revive NAT/Proxy state in routers.
|
||||
*/
|
||||
object Ping : UdpBlock()
|
||||
|
||||
/**
|
||||
* Parsec data block. Could not be smaller than two bytes.
|
||||
*/
|
||||
class Data(val data: UByteArray) : UdpBlock() {
|
||||
override fun toString(): String {
|
||||
return "UDP Data (${data.size}):\n${data.toDump()}"
|
||||
}
|
||||
init {
|
||||
if( data.size < 2) throw IllegalArgumentException("data must be at least 2 bytes")
|
||||
}
|
||||
}
|
||||
|
||||
val encoded: UByteArray by lazy {
|
||||
when(this) {
|
||||
is Data -> {
|
||||
// Do we need escaping?
|
||||
if( data[0] == ESCAPE_BYTE )
|
||||
escapeAsArray + data
|
||||
else
|
||||
data
|
||||
}
|
||||
is Cancel -> cancelAsArray
|
||||
is Ping -> pingAsArray
|
||||
}
|
||||
}
|
||||
|
||||
fun toDatagram(address: SocketAddress): Datagram {
|
||||
val encoded = encoded.toByteArray()
|
||||
return Datagram(ByteReadPacket(encoded, 0, encoded.size), address)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val ESCAPE_BYTE = 255.toUByte()
|
||||
val PING_BLOCK = 0.toUByte()
|
||||
val CANCEL_BLOCK = 1.toUByte()
|
||||
|
||||
private val escapeAsArray = ubyteArrayOf(ESCAPE_BYTE)
|
||||
private val pingAsArray = ubyteArrayOf(ESCAPE_BYTE, PING_BLOCK)
|
||||
private val cancelAsArray = ubyteArrayOf(ESCAPE_BYTE, CANCEL_BLOCK)
|
||||
|
||||
fun decode(data: UByteArray): UdpBlock {
|
||||
if (data.size < 2)
|
||||
throw UdpTransportException("block too short: ${data.size}")
|
||||
return if( data[0] != ESCAPE_BYTE )
|
||||
// plain data
|
||||
Data(data)
|
||||
else {
|
||||
when(val b2 = data[1]) {
|
||||
ESCAPE_BYTE -> {
|
||||
// Escaped first byte, then plain data
|
||||
Data(data.sliceArray(1 ..< data.size))
|
||||
}
|
||||
PING_BLOCK -> Ping
|
||||
CANCEL_BLOCK -> Cancel
|
||||
else -> throw UdpTransportException("invalid block type: $b2")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun decode(datagram: Datagram) =
|
||||
decode(datagram.packet.readBytes().toUByteArray())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package net.sergeych.kiloparsec.adapter
|
||||
|
||||
import io.ktor.network.sockets.*
|
||||
|
||||
/**
|
||||
* The interface for common UDP connector shared by UDP components
|
||||
*/
|
||||
internal interface UdpConnector {
|
||||
/**
|
||||
* Called when client connection is done so the provider could free resources
|
||||
*/
|
||||
suspend fun disconnectClient(address: SocketAddress)
|
||||
|
||||
/**
|
||||
* Send a block from a proper UDP socket
|
||||
*/
|
||||
suspend fun sendBlock(block: UdpBlock, toAddress: SocketAddress)
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package net.sergeych.kiloparsec.adapter
|
||||
|
||||
import io.ktor.network.selector.*
|
||||
import io.ktor.network.sockets.*
|
||||
import io.ktor.utils.io.CancellationException
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import net.sergeych.kiloparsec.KiloClient
|
||||
import net.sergeych.kiloparsec.KiloServer
|
||||
import net.sergeych.kiloparsec.RemoteInterface
|
||||
import net.sergeych.mp_tools.globalLaunch
|
||||
import net.sergeych.tools.AtomicCounter
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
internal val udpCounter = AtomicCounter(0)
|
||||
|
||||
class UdpTransportException(override val message: String) : RemoteInterface.InvalidDataException(message)
|
||||
|
||||
/**
|
||||
* Listen for incoming UDP connections and provide transport flow for it. See also [UdpServer.transportFlow]
|
||||
* for another way to create a server. Use it with [KiloServer]:
|
||||
* ```kotlin
|
||||
* // Whatever server session data we might need:
|
||||
* data class Session(
|
||||
* var data: String,
|
||||
* )
|
||||
*
|
||||
* // declare some commands (normally in a shared module):
|
||||
* val cmdSave by command<String, Unit>()
|
||||
* val cmdLoad by command<Unit, String>()
|
||||
* val cmdDrop by command<Unit, Unit>()
|
||||
* val cmdException by command<Unit, Unit>()
|
||||
*
|
||||
* // Interface using the session above, can be shared between many
|
||||
* // server types and instances (different ports and protocols):
|
||||
* val cli = KiloInterface<Session>().apply {
|
||||
* onConnected { session.data = "start" }
|
||||
* on(cmdSave) { session.data = it }
|
||||
* on(cmdLoad) {
|
||||
* session.data
|
||||
* }
|
||||
* on(cmdException) {
|
||||
* throw TestException()
|
||||
* }
|
||||
* on(cmdDrop) {
|
||||
* throw LocalInterface.BreakConnectionException()
|
||||
* }
|
||||
* }
|
||||
* // Now create a server to accept incoming UDPs on our port:
|
||||
* val server = KiloServer(cli, acceptUdpDevice(port)) {
|
||||
* // This initializes new session for each incoming command
|
||||
* Session("unknown")
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* See [connectUdpDevice] for the client sample.
|
||||
*
|
||||
* When it is necessary to stop listening to some port, use [UdpServer] instead.
|
||||
*
|
||||
* @param port port to listen
|
||||
* @param localInterface string form local interface to listen
|
||||
* @param maxInactivityTimeout maximum silence time after which the connection is supposed to be lost.
|
||||
* the module automatically issues pings on inactivity when there is no data often enough
|
||||
* to maintain the connection open.
|
||||
*/
|
||||
fun acceptUdpDevice(
|
||||
port: Int,
|
||||
localInterface: String = "0.0.0.0",
|
||||
maxInactivityTimeout: Duration = 2.minutes,
|
||||
): Flow<InetTransportDevice> =
|
||||
UdpServer(port, localInterface,maxInactivityTimeout).transportFlow
|
||||
|
||||
/**
|
||||
* Connect to UDP server (see [acceptUdpDevice] or [UdpServer]) and return a [InetTransportDevice] for it. It
|
||||
* should be used with [KiloClient] as connection provider:
|
||||
* ```kotlin
|
||||
* val client = KiloClient<Unit>() {
|
||||
* connect { connectUdpDevice("localhost:$port") }
|
||||
* }
|
||||
* // now we can execute remote commands:
|
||||
* assertEquals("start", client.call(cmdLoad))
|
||||
* ```
|
||||
*
|
||||
* @param hostPort "host:port" string address of the remote UDP port to connect to
|
||||
* @param maxInactivityTimeout maximum silence time after which the connection is supposed to be lost.
|
||||
* the module automatically issues pings on inactivity when there is no data often enough
|
||||
* to maintain the connection open.
|
||||
*/
|
||||
fun connectUdpDevice(
|
||||
hostPort: String,
|
||||
maxInactivityTimeout: Duration = 2.minutes,
|
||||
) = connectUdpDevice(hostPort.toNetworkAddress(), maxInactivityTimeout)
|
||||
|
||||
/**
|
||||
* Connect to UDP server (see [acceptUdpDevice]) and return a [InetTransportDevice] for it. It
|
||||
* should be used with [KiloClient] as connection provider:
|
||||
* ```kotlin
|
||||
* val client = KiloClient<Unit>() {
|
||||
* connect { connectUdpDevice("localhost:$port") }
|
||||
* }
|
||||
* // now we can execute remote commands:
|
||||
* assertEquals("start", client.call(cmdLoad))
|
||||
* ```
|
||||
* @param addr the network address where to connect to
|
||||
* @param maxInactivityTimeout maximum silence time after which the connection is supposed to be lost.
|
||||
* the module automatically issues pings on inactivity when there is no data often enough
|
||||
* to maintain the connection open.
|
||||
*/
|
||||
fun connectUdpDevice(
|
||||
addr: NetworkAddress,
|
||||
maxInactivityTimeout: Duration = 2.minutes,
|
||||
): InetTransportDevice {
|
||||
val selectorManager = SelectorManager(Dispatchers.IO)
|
||||
val remoteAddress = InetSocketAddress(addr.host, addr.port)
|
||||
val socket = aSocket(selectorManager).udp().connect(remoteAddress)
|
||||
|
||||
val done = CompletableDeferred<Unit>()
|
||||
|
||||
val transport = UdpSocketTransport(object : UdpConnector {
|
||||
override suspend fun sendBlock(block: UdpBlock, toAddress: SocketAddress) {
|
||||
socket.send(block.toDatagram(remoteAddress))
|
||||
}
|
||||
|
||||
override suspend fun disconnectClient(address: SocketAddress) {
|
||||
done.complete(Unit)
|
||||
}
|
||||
|
||||
}, remoteAddress, false, maxInactivityTimeout)
|
||||
|
||||
globalLaunch {
|
||||
launch {
|
||||
while (isActive) {
|
||||
try {
|
||||
transport.processIncoming(UdpBlock.decode(socket.receive()))
|
||||
} catch (_: CancellationException) {
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
transport.close()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
done.await()
|
||||
}
|
||||
|
||||
return transport.transportDevice
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package net.sergeych.kiloparsec.adapter
|
||||
|
||||
import io.ktor.network.selector.*
|
||||
import io.ktor.network.sockets.*
|
||||
import io.ktor.utils.io.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import net.sergeych.kiloparsec.KiloServer
|
||||
import net.sergeych.mp_logger.LogTag
|
||||
import net.sergeych.mp_logger.Loggable
|
||||
import net.sergeych.mp_logger.debug
|
||||
import net.sergeych.mp_logger.exception
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
/**
|
||||
* UDP server for kiloparsec. Unlike [acceptUdpDevice], it allow stopping listening
|
||||
* to the port when need with [close]. Use [transportFlow] with [KiloServer], here is the
|
||||
* basic sample:
|
||||
*
|
||||
* ```kotlin
|
||||
* val uServer = UdpServer(port)
|
||||
* KiloServer(cli, uServer.transportFlow()) {
|
||||
* Session("unknown")
|
||||
* }
|
||||
*
|
||||
* // server is now active and accepts connections
|
||||
* // ...
|
||||
*
|
||||
* // close and stop listening to the port:
|
||||
* uServer.close()
|
||||
* ```
|
||||
*
|
||||
* See [acceptUdpDevice] for more information.
|
||||
*
|
||||
* @param port port to listen to
|
||||
* @param localInterface string form of local interface to listen to
|
||||
* @param maxInactivityTimeout maximum silence time after which the connection is supposed to be lost.
|
||||
* the module automatically issues pings on inactivity when there is no data often enough
|
||||
* to maintain the connection open.
|
||||
|
||||
*/
|
||||
class UdpServer(val port: Int, localInterface: String = "0.0.0.0", maxInactivityTimeout: Duration = 2.minutes) :
|
||||
Loggable by LogTag("UDPS${udpCounter.incrementAndGet()}"), UdpConnector {
|
||||
|
||||
private val sessions = mutableMapOf<SocketAddress, UdpSocketTransport>()
|
||||
private val access = Mutex()
|
||||
|
||||
private val selectorManager = SelectorManager(Dispatchers.IO)
|
||||
private val serverSocket = aSocket(selectorManager).udp().bind(InetSocketAddress(localInterface, port))
|
||||
|
||||
override suspend fun disconnectClient(address: SocketAddress) {
|
||||
access.withLock { sessions.remove(address) }
|
||||
}
|
||||
|
||||
/**
|
||||
* a transport flow of [InetTransportDevice] suitable to be used with [KiloServer], see [UdpServer] for the
|
||||
* usage sample.
|
||||
*/
|
||||
val transportFlow by lazy {
|
||||
flow {
|
||||
while (true) {
|
||||
try {
|
||||
val datagram = serverSocket.receive()
|
||||
val block = UdpBlock.decode(datagram)
|
||||
val remoteAddress = datagram.address
|
||||
|
||||
access.withLock {
|
||||
if (block == UdpBlock.Cancel) {
|
||||
// if the cancel comes to already closed transport, do nothing
|
||||
sessions.remove(remoteAddress)?.processIncoming(block)
|
||||
} else {
|
||||
sessions.getOrPut(remoteAddress) {
|
||||
// new connection: create transport
|
||||
debug { "Creating new connection to $remoteAddress" }
|
||||
UdpSocketTransport(this@UdpServer, remoteAddress, true, maxInactivityTimeout)
|
||||
// and emit it:
|
||||
.also { emit(it.transportDevice) }
|
||||
}.processIncoming(block)
|
||||
}
|
||||
}
|
||||
} catch (_: CancellationException) {
|
||||
break
|
||||
} catch (_: ClosedReceiveChannelException) {
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
exception { "unexpected exception in incoming datagram processing" to e }
|
||||
close()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun sendBlock(block: UdpBlock, toAddress: SocketAddress) {
|
||||
serverSocket.send(block.toDatagram(toAddress))
|
||||
}
|
||||
|
||||
val isClosed: Boolean get() = serverSocket.isClosed
|
||||
|
||||
/**
|
||||
* Close the UDP server. Calling it will cause:
|
||||
*
|
||||
* - Closing nound UDP socket on [port]
|
||||
* - Closing all pending connections
|
||||
* - cancelling the [transportFlow], which will cause Kiloparsec server to also stop
|
||||
*
|
||||
* Call suspends until socket and all sessions are closed. Later calls do nothing.
|
||||
*/
|
||||
suspend fun close() {
|
||||
access.withLock {
|
||||
if (!isClosed) {
|
||||
runCatching { serverSocket.close() }
|
||||
}
|
||||
}
|
||||
while (sessions.isNotEmpty()) {
|
||||
runCatching {
|
||||
access.withLock { sessions.values.firstOrNull() }
|
||||
?.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package net.sergeych.kiloparsec.adapter
|
||||
|
||||
import io.ktor.network.sockets.*
|
||||
import io.ktor.utils.io.*
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||
import kotlinx.coroutines.channels.ClosedSendChannelException
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.Clock
|
||||
import net.sergeych.kiloparsec.SyncValue
|
||||
import net.sergeych.mp_logger.Log
|
||||
import net.sergeych.mp_logger.Loggable
|
||||
import net.sergeych.mp_logger.debug
|
||||
import net.sergeych.mp_logger.exception
|
||||
import net.sergeych.mp_tools.globalLaunch
|
||||
import kotlin.time.Duration
|
||||
|
||||
/**
|
||||
* This is a common part of UDP transport shared between client and server connections.
|
||||
* It should not be used directly but bu the [UdpServer], [acceptUdpDevice] and [connectUdpDevice]
|
||||
* respectively.
|
||||
*/
|
||||
internal class UdpSocketTransport(
|
||||
private val server: UdpConnector,
|
||||
val socketAddress: SocketAddress,
|
||||
val isServer: Boolean,
|
||||
val maxInactivityTimeout: Duration
|
||||
) :
|
||||
Loggable {
|
||||
|
||||
// IMPORTANT! Log stuff must be the first (or you shot your leg):
|
||||
val address = (socketAddress as InetSocketAddress).let { NetworkAddress(it.hostname, it.port) }
|
||||
override var logTag: String = "UDPT:$address${if (isServer) ":server" else ":client"}"
|
||||
override var logLevel: Log.Level? = Log.Level.DEBUG
|
||||
|
||||
// Pinger params: keep them first!
|
||||
private var lastSendAt = Clock.System.now()
|
||||
private var lastReceived = Clock.System.now()
|
||||
private val pingTimeout = maxInactivityTimeout / 3
|
||||
private val pingSleep = pingTimeout / 3
|
||||
private val pingMinTimeout = pingTimeout * 2 / 3
|
||||
|
||||
val inputDataBlocks = Channel<UByteArray>(256, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
val outputDataBlocks = Channel<UByteArray>(256, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
|
||||
val inputUdpBlocks = Channel<UdpBlock>(256, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
|
||||
private val job = globalLaunch {
|
||||
coroutineScope {
|
||||
launch { convertOutput() }
|
||||
launch { convertInput() }
|
||||
launch { pinger() }
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
// This is iverly important: it requires that
|
||||
// all members are initialized before use. Otherwise kotlin
|
||||
// may execute class members pr
|
||||
debug { "initialization done" }
|
||||
}
|
||||
|
||||
|
||||
val transportDevice: InetTransportDevice by lazy {
|
||||
InetTransportDevice(inputDataBlocks, outputDataBlocks, address, { close() }, {})
|
||||
}
|
||||
|
||||
private val closedFlag = SyncValue(false)
|
||||
|
||||
val isClosed: Boolean = closedFlag.value
|
||||
|
||||
suspend fun close() {
|
||||
closedFlag.mutate {
|
||||
if (!it) {
|
||||
runCatching { server.sendBlock(UdpBlock.Cancel, socketAddress) }
|
||||
server.disconnectClient(socketAddress)
|
||||
runCatching { inputDataBlocks.close() }
|
||||
runCatching { outputDataBlocks.close() }
|
||||
job.cancel()
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private suspend fun send(block: UdpBlock) {
|
||||
server.sendBlock(block, socketAddress)
|
||||
lastSendAt = Clock.System.now()
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the block recoded by the server. Note that it should properly process all
|
||||
* block types, e.g. close on [UdpBlock.Cancel], etc. Server will not close us!
|
||||
*
|
||||
* __Important: it should not block, instead, server expects it to return ASAP__, so it
|
||||
* executes in a local coroutine context.
|
||||
*
|
||||
* Also it should not throw exceptions.
|
||||
*/
|
||||
fun processIncoming(block: UdpBlock) {
|
||||
inputUdpBlocks.trySend(block)
|
||||
}
|
||||
|
||||
suspend fun convertInput() {
|
||||
while(!isClosed) {
|
||||
when (val block = inputUdpBlocks.receiveCatching().getOrNull()) {
|
||||
|
||||
null -> break
|
||||
|
||||
is UdpBlock.Cancel -> globalLaunch {
|
||||
debug { "received cancel block, requesting close" }
|
||||
kotlin.runCatching { close() }
|
||||
}
|
||||
|
||||
is UdpBlock.Data -> {
|
||||
// input does not block, it uses DROP_OLDEST policy
|
||||
lastReceived = Clock.System.now()
|
||||
val result = kotlin.runCatching { inputDataBlocks.send(block.data) }
|
||||
when (val e = result.exceptionOrNull()) {
|
||||
null -> {}
|
||||
is ClosedSendChannelException -> {
|
||||
debug { "received close channel" }
|
||||
close()
|
||||
}
|
||||
|
||||
is CancellationException -> {}
|
||||
else -> {
|
||||
exception { "unexpected exception" to e }
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UdpBlock.Ping -> {
|
||||
lastReceived = Clock.System.now()
|
||||
if (lastSendAt - lastReceived > pingMinTimeout) send(UdpBlock.Ping)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun pinger() {
|
||||
while (!isClosed) {
|
||||
delay(pingSleep)
|
||||
val inactivity = Clock.System.now() - lastSendAt
|
||||
if( inactivity > maxInactivityTimeout) {
|
||||
debug { "inactivity timout: closing the connection" }
|
||||
close()
|
||||
}
|
||||
if (inactivity >= pingTimeout) {
|
||||
debug { "pinger sends a ping on timeout" }
|
||||
send(UdpBlock.Ping)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun convertOutput() {
|
||||
while (!isClosed) {
|
||||
try {
|
||||
server.sendBlock(UdpBlock.Data(outputDataBlocks.receive()), socketAddress)
|
||||
} catch (e: CancellationException) {
|
||||
// this is ok
|
||||
break
|
||||
} catch (e: ClosedReceiveChannelException) {
|
||||
debug { "input channel is closed, closing" }
|
||||
close()
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
exception { "unexpected exception in convertOutput" to e }
|
||||
close()
|
||||
break
|
||||
}
|
||||
}
|
||||
debug { "exiting convertOutput" }
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
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 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) }
|
||||
assertIs<TestException>(res.exceptionOrNull())
|
||||
assertEquals("foobar", client.call(cmdLoad))
|
||||
|
||||
assertThrows<RemoteInterface.ClosedException> { client.call(cmdDrop) }
|
||||
|
||||
// reconnect?
|
||||
assertEquals("start", client.call(cmdLoad))
|
||||
|
||||
server.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package net.sergeych.kiloparsec.adapter
|
||||
|
||||
import assertThrows
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.sergeych.crypto2.initCrypto
|
||||
import net.sergeych.kiloparsec.*
|
||||
import net.sergeych.mp_logger.Log
|
||||
import kotlin.random.Random
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertIs
|
||||
|
||||
class InternetrTest {
|
||||
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)
|
||||
// TODO: add register error variant
|
||||
connect { connectTcpDevice("localhost:$port") }
|
||||
}
|
||||
|
||||
assertEquals("start", client.call(cmdLoad))
|
||||
|
||||
client.call(cmdSave, "foobar")
|
||||
assertEquals("foobar", client.call(cmdLoad))
|
||||
|
||||
val res = kotlin.runCatching { client.call(cmdException) }
|
||||
assertIs<TestException>(res.exceptionOrNull())
|
||||
assertEquals("foobar", client.call(cmdLoad))
|
||||
|
||||
assertThrows<RemoteInterface.ClosedException> { client.call(cmdDrop) }
|
||||
|
||||
// reconnect?
|
||||
assertEquals("start", client.call(cmdLoad))
|
||||
|
||||
server.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun udpTest() = 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, acceptUdpDevice(port)) {
|
||||
Session("unknown")
|
||||
}
|
||||
|
||||
val client = KiloClient<Unit>() {
|
||||
addErrors(cli)
|
||||
connect { connectUdpDevice("127.0.0.1:$port") }
|
||||
}
|
||||
|
||||
assertEquals("start", client.call(cmdLoad))
|
||||
|
||||
client.call(cmdSave, "foobar")
|
||||
assertEquals("foobar", client.call(cmdLoad))
|
||||
|
||||
val res = kotlin.runCatching { client.call(cmdException) }
|
||||
assertIs<TestException>(res.exceptionOrNull())
|
||||
assertEquals("foobar", client.call(cmdLoad))
|
||||
|
||||
assertThrows<RemoteInterface.ClosedException> { client.call(cmdDrop) }
|
||||
|
||||
// reconnect?
|
||||
assertEquals("start", client.call(cmdLoad))
|
||||
|
||||
server.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun udpServerTest() = runTest {
|
||||
initCrypto()
|
||||
Log.connectConsole(Log.Level.DEBUG)
|
||||
data class Session(
|
||||
var data: String,
|
||||
)
|
||||
|
||||
val port = 27170 + Random.nextInt(1, 900)
|
||||
|
||||
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 uServer = UdpServer(port)
|
||||
|
||||
KiloServer(cli, uServer.transportFlow) {
|
||||
Session("unknown")
|
||||
}
|
||||
|
||||
// second server
|
||||
KiloServer(cli,acceptUdpDevice(uServer.port+1)) { Session("unknown2") }
|
||||
|
||||
|
||||
val client = KiloClient<Unit>() {
|
||||
addErrors(cli)
|
||||
connect { connectUdpDevice("127.0.0.1:$port") }
|
||||
}
|
||||
val client2 = KiloClient<Unit>() { connect { connectUdpDevice("127.0.0.1:${port+1}") } }
|
||||
|
||||
assertEquals("start", client.call(cmdLoad))
|
||||
assertEquals("start", client2.call(cmdLoad))
|
||||
|
||||
client.call(cmdSave, "foobar")
|
||||
client2.call(cmdSave, "buzz")
|
||||
assertEquals("foobar", client.call(cmdLoad))
|
||||
assertEquals("buzz", client2.call(cmdLoad))
|
||||
|
||||
val res = kotlin.runCatching { client.call(cmdException) }
|
||||
assertIs<TestException>(res.exceptionOrNull())
|
||||
assertEquals("foobar", client.call(cmdLoad))
|
||||
|
||||
assertThrows<RemoteInterface.ClosedException> { client.call(cmdDrop) }
|
||||
|
||||
// reconnect?
|
||||
assertEquals("start", client.call(cmdLoad))
|
||||
|
||||
uServer.close()
|
||||
// server.close()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user