WIP changing the network lib from netty -> AERON
This commit is contained in:
parent
4c8c50e8a3
commit
fc7baa6c8d
132
build.gradle.kts
132
build.gradle.kts
|
@ -14,6 +14,8 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import dorkbox.gradle.kotlin
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import java.time.Instant
|
||||
|
||||
///////////////////////////////
|
||||
|
@ -25,12 +27,12 @@ import java.time.Instant
|
|||
plugins {
|
||||
java
|
||||
|
||||
id("com.dorkbox.GradleUtils") version "1.8"
|
||||
id("com.dorkbox.CrossCompile") version "1.1"
|
||||
id("com.dorkbox.Licensing") version "1.4.2"
|
||||
id("com.dorkbox.VersionUpdate") version "1.6.1"
|
||||
id("com.dorkbox.GradlePublish") version "1.1"
|
||||
id("com.dorkbox.GradlePublish") version "1.2"
|
||||
id("com.dorkbox.GradleModuleInfo") version "1.0"
|
||||
id("com.dorkbox.GradleUtils") version "1.6"
|
||||
|
||||
kotlin("jvm") version "1.3.72"
|
||||
}
|
||||
|
@ -49,23 +51,20 @@ object Extras {
|
|||
const val url = "https://git.dorkbox.com/dorkbox/Network"
|
||||
val buildDate = Instant.now().toString()
|
||||
|
||||
val JAVA_VERSION = JavaVersion.VERSION_11
|
||||
val JAVA_VERSION = JavaVersion.VERSION_11.toString()
|
||||
const val KOTLIN_API_VERSION = "1.3"
|
||||
const val KOTLIN_LANG_VERSION = "1.3"
|
||||
|
||||
const val bcVersion = "1.60"
|
||||
|
||||
var sonatypeUserName = ""
|
||||
var sonatypePassword = ""
|
||||
var sonatypePrivateKeyFile = ""
|
||||
var sonatypePrivateKeyPassword = ""
|
||||
const val atomicfuVer = "0.14.3"
|
||||
const val coroutineVer = "1.3.7"
|
||||
}
|
||||
|
||||
///////////////////////////////
|
||||
///// assign 'Extras'
|
||||
///////////////////////////////
|
||||
GradleUtils.load("$projectDir/../../gradle.properties", Extras)
|
||||
description = Extras.description
|
||||
group = Extras.group
|
||||
version = Extras.version
|
||||
GradleUtils.fixIntellijPaths()
|
||||
|
||||
// NOTE: now using aeron instead of netty
|
||||
|
||||
|
@ -100,6 +99,8 @@ version = Extras.version
|
|||
// }
|
||||
// }
|
||||
|
||||
|
||||
// NOTE: uses network util from netty!
|
||||
licensing {
|
||||
license(License.APACHE_2) {
|
||||
author(Extras.vendor)
|
||||
|
@ -224,6 +225,13 @@ sourceSets {
|
|||
// want to include java files for the source. 'setSrcDirs' resets includes...
|
||||
include("**/*.java")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
setSrcDirs(listOf("src"))
|
||||
|
||||
// want to include java files for the source. 'setSrcDirs' resets includes...
|
||||
include("**/*.java", "**/*.kt")
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
|
@ -233,6 +241,13 @@ sourceSets {
|
|||
// want to include java files for the source. 'setSrcDirs' resets includes...
|
||||
include("**/*.java")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
setSrcDirs(listOf("src"))
|
||||
|
||||
// want to include java files for the source. 'setSrcDirs' resets includes...
|
||||
include("**/*.java", "**/*.kt")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -244,19 +259,31 @@ repositories {
|
|||
///////////////////////////////
|
||||
////// Task defaults
|
||||
///////////////////////////////
|
||||
java {
|
||||
tasks.withType<JavaCompile> {
|
||||
doFirst {
|
||||
println("\tCompiling classes to Java $sourceCompatibility")
|
||||
}
|
||||
|
||||
options.encoding = "UTF-8"
|
||||
|
||||
sourceCompatibility = Extras.JAVA_VERSION
|
||||
targetCompatibility = Extras.JAVA_VERSION
|
||||
}
|
||||
|
||||
tasks.compileJava.get().apply {
|
||||
println("\tCompiling classes to Java $sourceCompatibility")
|
||||
}
|
||||
tasks.withType<KotlinCompile> {
|
||||
doFirst {
|
||||
println("\tCompiling classes to Kotlin, Java ${kotlinOptions.jvmTarget}")
|
||||
}
|
||||
|
||||
tasks.withType<JavaCompile> {
|
||||
options.encoding = "UTF-8"
|
||||
sourceCompatibility = Extras.JAVA_VERSION.toString()
|
||||
targetCompatibility = Extras.JAVA_VERSION.toString()
|
||||
sourceCompatibility = Extras.JAVA_VERSION
|
||||
targetCompatibility = Extras.JAVA_VERSION
|
||||
|
||||
// see: https://kotlinlang.org/docs/reference/using-gradle.html
|
||||
kotlinOptions {
|
||||
jvmTarget = Extras.JAVA_VERSION
|
||||
apiVersion = Extras.KOTLIN_API_VERSION
|
||||
languageVersion = Extras.KOTLIN_LANG_VERSION
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<Jar> {
|
||||
|
@ -281,30 +308,69 @@ tasks.jar.get().apply {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation("io.netty:netty-all:4.1.49.Final")
|
||||
implementation("com.esotericsoftware:kryo:5.0.0-RC2")
|
||||
implementation(kotlin("stdlib-jdk8"))
|
||||
|
||||
implementation("org.jetbrains.kotlinx:atomicfu:${Extras.atomicfuVer}")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${Extras.coroutineVer}")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-common:${Extras.coroutineVer}")
|
||||
|
||||
|
||||
// https://github.com/real-logic/aeron
|
||||
val aeronVer = "1.28.2"
|
||||
implementation("io.aeron:aeron-client:$aeronVer")
|
||||
implementation("io.aeron:aeron-driver:$aeronVer")
|
||||
|
||||
|
||||
implementation("io.netty:netty-buffer:4.1.49.Final")
|
||||
implementation("com.esotericsoftware:kryo:5.0.0-RC6")
|
||||
implementation("net.jpountz.lz4:lz4:1.3.0")
|
||||
|
||||
implementation("org.bouncycastle:bcprov-jdk15on:${Extras.bcVersion}")
|
||||
implementation("org.bouncycastle:bcpg-jdk15on:${Extras.bcVersion}")
|
||||
implementation("org.bouncycastle:bcmail-jdk15on:${Extras.bcVersion}")
|
||||
implementation("org.bouncycastle:bctls-jdk15on:${Extras.bcVersion}")
|
||||
// this is NOT the same thing as LMAX disruptor.
|
||||
// This is just a really fast queue (where LMAX is a fast queue + other things w/ a difficult DSL)
|
||||
// https://github.com/conversant/disruptor_benchmark
|
||||
// https://www.youtube.com/watch?v=jVMOgQgYzWU
|
||||
implementation("com.conversantmedia:disruptor:1.2.15")
|
||||
|
||||
// todo: remove BC! use conscrypt instead, or native java? (if possible. we are java 11 now, instead of 1.6)
|
||||
// java 14 is faster with aeron!
|
||||
// implementation("org.bouncycastle:bcprov-jdk15on:${Extras.bcVersion}")
|
||||
// implementation("org.bouncycastle:bcpg-jdk15on:${Extras.bcVersion}")
|
||||
// implementation("org.bouncycastle:bcmail-jdk15on:${Extras.bcVersion}")
|
||||
// implementation("org.bouncycastle:bctls-jdk15on:${Extras.bcVersion}")
|
||||
|
||||
implementation("net.jodah:typetools:0.6.2")
|
||||
implementation("de.javakaffee:kryo-serializers:0.45")
|
||||
implementation("org.javassist:javassist:3.27.0-GA")
|
||||
|
||||
implementation("com.dorkbox:ObjectPool:2.12")
|
||||
implementation("com.dorkbox:Utilities:1.2")
|
||||
implementation("com.dorkbox:Utilities:1.5.3")
|
||||
|
||||
implementation("io.github.microutils:kotlin-logging:1.7.9") // slick kotlin wrapper for slf4j
|
||||
implementation("org.slf4j:slf4j-api:1.7.30")
|
||||
|
||||
// https://github.com/real-logic/aeron
|
||||
implementation("io.aeron:aeron-all:1.28.2")
|
||||
|
||||
|
||||
testImplementation("junit:junit:4.13")
|
||||
testImplementation("ch.qos.logback:logback-classic:1.2.3")
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
resolutionStrategy {
|
||||
// fail eagerly on version conflict (includes transitive dependencies)
|
||||
// e.g. multiple different versions of the same dependency (group and name are equal)
|
||||
failOnVersionConflict()
|
||||
|
||||
// if there is a version we specified, USE THAT VERSION (over transitive versions)
|
||||
preferProjectModules()
|
||||
|
||||
// cache dynamic versions for 10 minutes
|
||||
cacheDynamicVersionsFor(10 * 60, "seconds")
|
||||
|
||||
// don't cache changing modules at all
|
||||
cacheChangingModulesFor(0, "seconds")
|
||||
}
|
||||
}
|
||||
|
||||
publishToSonatype {
|
||||
groupId = Extras.group
|
||||
artifactId = Extras.id
|
||||
|
@ -327,14 +393,4 @@ publishToSonatype {
|
|||
name = Extras.vendor
|
||||
email = "email@dorkbox.com"
|
||||
}
|
||||
|
||||
sonatype {
|
||||
userName = Extras.sonatypeUserName
|
||||
password = Extras.sonatypePassword
|
||||
}
|
||||
|
||||
privateKey {
|
||||
fileName = Extras.sonatypePrivateKeyFile
|
||||
password = Extras.sonatypePrivateKeyPassword
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,679 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.agrona.BufferUtil;
|
||||
import org.agrona.DirectBuffer;
|
||||
import org.agrona.concurrent.BackoffIdleStrategy;
|
||||
import org.agrona.concurrent.IdleStrategy;
|
||||
import org.agrona.concurrent.UnsafeBuffer;
|
||||
|
||||
import dorkbox.network.aeron.EchoChannels;
|
||||
import dorkbox.network.aeron.EchoMessages;
|
||||
import dorkbox.network.aeron.exceptions.ClientIOException;
|
||||
import dorkbox.network.aeron.exceptions.EchoClientException;
|
||||
import dorkbox.network.aeron.exceptions.EchoClientRejectedException;
|
||||
import dorkbox.network.aeron.exceptions.EchoClientTimedOutException;
|
||||
import dorkbox.network.connection.Connection;
|
||||
import dorkbox.network.connection.EndPoint;
|
||||
import dorkbox.network.connection.EndPointClient;
|
||||
import dorkbox.network.rmi.RemoteObject;
|
||||
import dorkbox.network.rmi.RemoteObjectCallback;
|
||||
import dorkbox.network.rmi.TimeoutException;
|
||||
import dorkbox.util.exceptions.SecurityException;
|
||||
import io.aeron.ConcurrentPublication;
|
||||
import io.aeron.FragmentAssembler;
|
||||
import io.aeron.Publication;
|
||||
import io.aeron.Subscription;
|
||||
import io.aeron.logbuffer.FragmentHandler;
|
||||
|
||||
/**
|
||||
* The client is both SYNC and ASYNC. It starts off SYNC (blocks thread until it's done), then once it's connected to the server, it's
|
||||
* ASYNC.
|
||||
*/
|
||||
@SuppressWarnings({"unused", "WeakerAccess"})
|
||||
public
|
||||
class Client<C extends Connection> extends EndPointClient implements Connection {
|
||||
/**
|
||||
* Gets the version number.
|
||||
*/
|
||||
public static
|
||||
String getVersion() {
|
||||
return "4.1";
|
||||
}
|
||||
|
||||
|
||||
private static final Pattern PATTERN_ERROR = Pattern.compile("^ERROR (.*)$");
|
||||
private static final Pattern PATTERN_CONNECT = Pattern.compile("^CONNECT ([0-9]+) ([0-9]+) ([0-9A-F]+)$");
|
||||
private static final Pattern PATTERN_ECHO = Pattern.compile("^ECHO (.*)$");
|
||||
|
||||
private final SecureRandom random = new SecureRandom();
|
||||
|
||||
private volatile int remote_data_port;
|
||||
private volatile int remote_control_port;
|
||||
private volatile boolean remote_ports_received;
|
||||
private volatile boolean failed;
|
||||
private volatile int remote_session;
|
||||
private volatile int duologue_key;
|
||||
|
||||
|
||||
/**
|
||||
* Starts a LOCAL <b>only</b> client, with the default local channel name and serialization scheme
|
||||
*/
|
||||
public
|
||||
Client() throws SecurityException, IOException {
|
||||
this(new ClientConfiguration());
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a REMOTE <b>only</b> client, which will connect to the specified host using the specified Connections Options
|
||||
*/
|
||||
@SuppressWarnings("AutoBoxing")
|
||||
public
|
||||
Client(final ClientConfiguration config) throws SecurityException, IOException {
|
||||
super(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows the client to reconnect to the last connected server
|
||||
*
|
||||
* @throws IOException
|
||||
* if the client is unable to reconnect in the previously requested connection-timeout
|
||||
*/
|
||||
public
|
||||
void reconnect() throws IOException, EchoClientException {
|
||||
reconnect(connectionTimeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows the client to reconnect to the last connected server
|
||||
*
|
||||
* @throws IOException
|
||||
* if the client is unable to reconnect in the requested time
|
||||
*/
|
||||
public
|
||||
void reconnect(final int connectionTimeout) throws IOException, EchoClientException {
|
||||
// make sure we are closed first
|
||||
close();
|
||||
|
||||
connect(connectionTimeout);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* will attempt to connect to the server, with a 30 second timeout.
|
||||
*
|
||||
* @throws IOException
|
||||
* if the client is unable to connect in 30 seconds
|
||||
*/
|
||||
public
|
||||
void connect() throws IOException, EchoClientException {
|
||||
connect(30000);
|
||||
}
|
||||
|
||||
/**
|
||||
* will attempt to connect to the server, and will the specified timeout.
|
||||
* <p/>
|
||||
* will BLOCK until completed
|
||||
*
|
||||
* @param connectionTimeout
|
||||
* wait for x milliseconds. 0 will wait indefinitely
|
||||
*
|
||||
* @throws IOException
|
||||
* if the client is unable to connect in the requested time
|
||||
*/
|
||||
public
|
||||
void connect(final int connectionTimeout) throws IOException, EchoClientException {
|
||||
this.connectionTimeout = connectionTimeout;
|
||||
|
||||
// make sure we are not trying to connect during a close or stop event.
|
||||
// This will wait until we have finished shutting down.
|
||||
// synchronized (shutdownInProgress) {
|
||||
// }
|
||||
|
||||
// // if we are in the SAME thread as netty -- start in a new thread (otherwise we will deadlock)
|
||||
// if (isNettyThread()) {
|
||||
// runNewThread("Restart Thread", new Runnable(){
|
||||
// @Override
|
||||
// public
|
||||
// void run() {
|
||||
// try {
|
||||
// connect(connectionTimeout);
|
||||
// } catch (IOException e) {
|
||||
// e.printStackTrace();
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// return;
|
||||
// }
|
||||
|
||||
/*
|
||||
* Generate a one-time pad.
|
||||
*/
|
||||
this.duologue_key = this.random.nextInt();
|
||||
|
||||
final UnsafeBuffer buffer = new UnsafeBuffer(BufferUtil.allocateDirectAligned(1024, 16));
|
||||
|
||||
final String session_name;
|
||||
try (final Subscription subscription = this.setupAllClientsSubscription()) {
|
||||
try (final Publication publication = this.setupAllClientsPublication()) {
|
||||
|
||||
/*
|
||||
* Send a one-time pad to the server.
|
||||
*/
|
||||
EchoMessages.sendMessage(publication,
|
||||
buffer,
|
||||
"HELLO " + Integer.toUnsignedString(this.duologue_key, 16)
|
||||
.toUpperCase());
|
||||
|
||||
session_name = Integer.toString(publication.sessionId());
|
||||
this.waitForConnectResponse(subscription, session_name);
|
||||
} catch (final IOException e) {
|
||||
throw new ClientIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Connect to the publication and subscription that the server has sent
|
||||
* back to this client.
|
||||
*/
|
||||
try (final Subscription subscription = this.setupConnectSubscription()) {
|
||||
try (final Publication publication = this.setupConnectPublication()) {
|
||||
|
||||
/**
|
||||
* Note: Reassembly has been shown to be minimal impact to latency. But not totally negligible. If the lowest latency is desired, then limiting message sizes to MTU size is a good practice.
|
||||
*
|
||||
* Note: There is a maximum length allowed for messages which is the min of 1/8th a term length or 16MB. Messages larger than this should chunked using an application level chunking protocol. Chunking has better recovery properties from failure and streams with mechanical sympathy.
|
||||
*/
|
||||
final FragmentHandler fragmentHandler = new FragmentAssembler((data, offset, length, header)->onEchoResponse(session_name,
|
||||
data,
|
||||
offset,
|
||||
length));
|
||||
|
||||
final IdleStrategy idleStrategy = new BackoffIdleStrategy(100, 10, TimeUnit.MICROSECONDS.toNanos(1), TimeUnit.MICROSECONDS.toNanos(100));
|
||||
|
||||
while (true) {
|
||||
// final int fragmentsRead = subscription.poll(fragmentHandler, 10);
|
||||
// idleStrategy.idle(fragmentsRead);
|
||||
|
||||
/*
|
||||
* Send ECHO messages to the server and wait for responses.
|
||||
*/
|
||||
EchoMessages.sendMessage(publication, buffer, "ECHO " + Long.toUnsignedString(this.random.nextLong(), 16));
|
||||
|
||||
for (int index = 0; index < 100; ++index) {
|
||||
subscription.poll(fragmentHandler, 1000);
|
||||
|
||||
try {
|
||||
Thread.sleep(10L);
|
||||
} catch (final InterruptedException e) {
|
||||
Thread.currentThread()
|
||||
.interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} catch (final IOException e) {
|
||||
throw new ClientIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// if (isShutdown()) {
|
||||
// throw new IOException("Unable to connect when shutdown...");
|
||||
// }
|
||||
|
||||
// if (localChannelName != null) {
|
||||
// logger.info("Connecting to local server: {}", localChannelName);
|
||||
// }
|
||||
// else {
|
||||
// if (config.tcpPort > 0 && config.udpPort > 0) {
|
||||
// logger.info("Connecting to TCP/UDP server [{}:{}]", hostName, config.tcpPort, config.udpPort);
|
||||
// }
|
||||
// else if (config.tcpPort > 0) {
|
||||
// logger.info("Connecting to TCP server [{}:{}]", hostName, config.tcpPort);
|
||||
// }
|
||||
// else {
|
||||
// logger.info("Connecting to UDP server [{}:{}]", hostName, config.udpPort);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // have to start the registration process. This will wait until registration is complete and RMI methods are initialized
|
||||
// // if this is called in the event dispatch thread for netty, it will deadlock!
|
||||
// startRegistration();
|
||||
|
||||
//
|
||||
// if (config.tcpPort == 0 && config.udpPort > 0) {
|
||||
// // AFTER registration is complete, if we are UDP only -- setup a heartbeat (must be the larger of 2x the idle timeout OR 10 seconds)
|
||||
// startUdpHeartbeat();
|
||||
// }
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
boolean hasRemoteKeyChanged() {
|
||||
return connection.hasRemoteKeyChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the remote address, as a string.
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
String getRemoteHost() {
|
||||
return connection.getRemoteHost();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if this connection is established on the loopback interface
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
boolean isLoopback() {
|
||||
return connection.isLoopback();
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
boolean isIPC() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if this connection is a network connection
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
boolean isNetwork() { return false; }
|
||||
|
||||
/**
|
||||
* @return the connection (TCP or LOCAL) id of this connection.
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
int id() {
|
||||
return connection.id();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the connection (TCP or LOCAL) id of this connection as a HEX string.
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
String idAsHex() {
|
||||
return connection.idAsHex();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Tells the remote connection to create a new proxy object that implements the specified interface. The methods on this object "map"
|
||||
* to an object that is created remotely.
|
||||
* <p>
|
||||
* The callback will be notified when the remote object has been created.
|
||||
* <p>
|
||||
* <p>
|
||||
* Methods that return a value will throw {@link TimeoutException} if the response is not received with the
|
||||
* {@link RemoteObject#setResponseTimeout(int) response timeout}.
|
||||
* <p/>
|
||||
* If {@link RemoteObject#setAsync(boolean) non-blocking} is false (the default), then methods that return a value must
|
||||
* not be called from the update thread for the connection. An exception will be thrown if this occurs. Methods with a
|
||||
* void return value can be called on the update thread.
|
||||
* <p/>
|
||||
* If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side
|
||||
* will have the proxy object replaced with the registered (non-proxy) object.
|
||||
* <p>
|
||||
* If one wishes to change the default behavior, cast the object to access the different methods.
|
||||
* ie: `RemoteObject remoteObject = (RemoteObject) test;`
|
||||
*
|
||||
* @see RemoteObject
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
<Iface> void createRemoteObject(final Class<Iface> interfaceClass, final RemoteObjectCallback<Iface> callback) {
|
||||
try {
|
||||
connection.createRemoteObject(interfaceClass, callback);
|
||||
} catch (NullPointerException e) {
|
||||
logger.error("Error creating remote object!", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the remote connection to create a new proxy object that implements the specified interface. The methods on this object "map"
|
||||
* to an object that is created remotely.
|
||||
* <p>
|
||||
* The callback will be notified when the remote object has been created.
|
||||
* <p>
|
||||
* <p>
|
||||
* Methods that return a value will throw {@link TimeoutException} if the response is not received with the
|
||||
* {@link RemoteObject#setResponseTimeout(int) response timeout}.
|
||||
* <p/>
|
||||
* If {@link RemoteObject#setAsync(boolean) non-blocking} is false (the default), then methods that return a value must
|
||||
* not be called from the update thread for the connection. An exception will be thrown if this occurs. Methods with a
|
||||
* void return value can be called on the update thread.
|
||||
* <p/>
|
||||
* If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side
|
||||
* will have the proxy object replaced with the registered (non-proxy) object.
|
||||
* <p>
|
||||
* If one wishes to change the default behavior, cast the object to access the different methods.
|
||||
* ie: `RemoteObject remoteObject = (RemoteObject) test;`
|
||||
*
|
||||
* @see RemoteObject
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
<Iface> void getRemoteObject(final int objectId, final RemoteObjectCallback<Iface> callback) {
|
||||
try {
|
||||
connection.getRemoteObject(objectId, callback);
|
||||
} catch (NullPointerException e) {
|
||||
logger.error("Error getting remote object!", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the connection used by the client.
|
||||
* <p/>
|
||||
* Make <b>sure</b> that you only call this <b>after</b> the client connects!
|
||||
* <p/>
|
||||
* This is preferred to {@link EndPoint#getConnections()}, as it properly does some error checking
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public
|
||||
C getConnection() {
|
||||
return (C) connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes all connections ONLY (keeps the client running), does not remove any listeners. To STOP the client, use stop().
|
||||
* <p/>
|
||||
* This is used, for example, when reconnecting to a server.
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
void close() {
|
||||
super.close();
|
||||
// closeConnection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if this client has connected yet or not.
|
||||
*
|
||||
* @return true if we are connected, false otherwise.
|
||||
*/
|
||||
public
|
||||
boolean isConnected() {
|
||||
return super.isConnected.get();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private
|
||||
void onEchoResponse(final String session_name, final DirectBuffer buffer, final int offset, final int length) {
|
||||
final String response = EchoMessages.parseMessageUTF8(buffer, offset, length);
|
||||
|
||||
logger.debug("[{}] response: {}", session_name, response);
|
||||
|
||||
final Matcher echo_matcher = PATTERN_ECHO.matcher(response);
|
||||
if (echo_matcher.matches()) {
|
||||
final String message = echo_matcher.group(1);
|
||||
logger.debug("[{}] ECHO {}", session_name, message);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.error("[{}] server returned unrecognized message: {}", session_name, response);
|
||||
}
|
||||
|
||||
private
|
||||
Publication setupConnectPublication() throws EchoClientTimedOutException {
|
||||
final ConcurrentPublication publication = EchoChannels.createPublicationWithSession(this.aeron,
|
||||
this.config.remoteAddress,
|
||||
this.remote_data_port,
|
||||
this.remote_session, UDP_STREAM_ID);
|
||||
|
||||
for (int index = 0; index < 1000; ++index) {
|
||||
if (publication.isConnected()) {
|
||||
logger.debug("CONNECT publication connected");
|
||||
return publication;
|
||||
}
|
||||
|
||||
try {
|
||||
Thread.sleep(10L);
|
||||
} catch (final InterruptedException e) {
|
||||
Thread.currentThread()
|
||||
.interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
publication.close();
|
||||
throw new EchoClientTimedOutException("Making CONNECT publication to server");
|
||||
}
|
||||
|
||||
private
|
||||
Subscription setupConnectSubscription() throws EchoClientTimedOutException {
|
||||
final Subscription subscription = EchoChannels.createSubscriptionDynamicMDCWithSession(this.aeron,
|
||||
this.config.remoteAddress,
|
||||
this.remote_control_port,
|
||||
this.remote_session, UDP_STREAM_ID);
|
||||
|
||||
for (int index = 0; index < 1000; ++index) {
|
||||
if (subscription.isConnected() && subscription.imageCount() > 0) {
|
||||
logger.debug("CONNECT subscription connected");
|
||||
return subscription;
|
||||
}
|
||||
|
||||
try {
|
||||
Thread.sleep(10L);
|
||||
} catch (final InterruptedException e) {
|
||||
Thread.currentThread()
|
||||
.interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
subscription.close();
|
||||
throw new EchoClientTimedOutException("Making CONNECT subscription to server");
|
||||
}
|
||||
|
||||
private
|
||||
void waitForConnectResponse(final Subscription subscription, final String session_name) throws EchoClientTimedOutException, EchoClientRejectedException {
|
||||
logger.debug("waiting for response");
|
||||
|
||||
final FragmentHandler handler = new FragmentAssembler((data, offset, length, header)->this.onInitialResponse(session_name,
|
||||
data,
|
||||
offset,
|
||||
length));
|
||||
|
||||
for (int index = 0; index < 1000; ++index) {
|
||||
subscription.poll(handler, 1000);
|
||||
|
||||
if (this.failed) {
|
||||
throw new EchoClientRejectedException("Server rejected this client");
|
||||
}
|
||||
|
||||
if (this.remote_ports_received) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Thread.sleep(10L);
|
||||
} catch (final InterruptedException e) {
|
||||
Thread.currentThread()
|
||||
.interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
throw new EchoClientTimedOutException("Waiting for CONNECT response from server");
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the initial response from the server.
|
||||
*/
|
||||
|
||||
private
|
||||
void onInitialResponse(final String session_name, final DirectBuffer buffer, final int offset, final int length) {
|
||||
final String response = EchoMessages.parseMessageUTF8(buffer, offset, length);
|
||||
|
||||
logger.trace("[{}] response: {}", session_name, response);
|
||||
|
||||
/*
|
||||
* Try to extract the session identifier to determine whether the message
|
||||
* was intended for this client or not.
|
||||
*/
|
||||
final int space = response.indexOf(" ");
|
||||
if (space == -1) {
|
||||
logger.error("[{}] server returned unrecognized message: {}", session_name, response);
|
||||
return;
|
||||
}
|
||||
|
||||
final String message_session = response.substring(0, space);
|
||||
if (!Objects.equals(message_session, session_name)) {
|
||||
logger.trace("[{}] ignored message intended for another client", session_name);
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* The message was intended for this client. Try to parse it as one
|
||||
* of the available message types.
|
||||
*/
|
||||
|
||||
final String text = response.substring(space)
|
||||
.trim();
|
||||
|
||||
final Matcher error_matcher = PATTERN_ERROR.matcher(text);
|
||||
if (error_matcher.matches()) {
|
||||
final String message = error_matcher.group(1);
|
||||
logger.error("[{}] server returned an error: {}", session_name, message);
|
||||
this.failed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
final Matcher connect_matcher = PATTERN_CONNECT.matcher(text);
|
||||
if (connect_matcher.matches()) {
|
||||
final int port_data = Integer.parseUnsignedInt(connect_matcher.group(1));
|
||||
final int port_control = Integer.parseUnsignedInt(connect_matcher.group(2));
|
||||
final int session_crypted = Integer.parseUnsignedInt(connect_matcher.group(3), 16);
|
||||
|
||||
logger.debug("[{}] connect {} {} (encrypted {})",
|
||||
session_name,
|
||||
Integer.valueOf(port_data),
|
||||
Integer.valueOf(port_control),
|
||||
|
||||
Integer.valueOf(session_crypted));
|
||||
this.remote_control_port = port_control;
|
||||
this.remote_data_port = port_data;
|
||||
this.remote_session = this.duologue_key ^ session_crypted;
|
||||
this.remote_ports_received = true;
|
||||
return;
|
||||
}
|
||||
|
||||
logger.error("[{}] server returned unrecognized message: {}", session_name, text);
|
||||
}
|
||||
|
||||
private
|
||||
Publication setupAllClientsPublication() throws EchoClientTimedOutException {
|
||||
// Note: The Aeron.addPublication method will block until the Media Driver acknowledges the request or a timeout occurs.
|
||||
final ConcurrentPublication publication = EchoChannels.createPublication(this.aeron,
|
||||
this.config.remoteAddress,
|
||||
this.config.port, UDP_STREAM_ID);
|
||||
|
||||
for (int index = 0; index < 1000; ++index) {
|
||||
if (publication.isConnected()) {
|
||||
logger.debug("initial publication connected");
|
||||
return publication;
|
||||
}
|
||||
|
||||
try {
|
||||
Thread.sleep(10L);
|
||||
} catch (final InterruptedException e) {
|
||||
Thread.currentThread()
|
||||
.interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
publication.close();
|
||||
throw new EchoClientTimedOutException("Making initial publication to server");
|
||||
}
|
||||
|
||||
private
|
||||
Subscription setupAllClientsSubscription() throws EchoClientTimedOutException {
|
||||
final Subscription subscription = EchoChannels.createSubscriptionDynamicMDC(this.aeron,
|
||||
this.config.remoteAddress,
|
||||
this.config.controlPort,
|
||||
UDP_STREAM_ID);
|
||||
|
||||
for (int index = 0; index < 1000; ++index) {
|
||||
if (subscription.isConnected() && subscription.imageCount() > 0) {
|
||||
logger.debug("initial subscription connected");
|
||||
return subscription;
|
||||
}
|
||||
|
||||
try {
|
||||
Thread.sleep(10L);
|
||||
} catch (final InterruptedException e) {
|
||||
Thread.currentThread()
|
||||
.interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
subscription.close();
|
||||
throw new EchoClientTimedOutException("Making initial subscription to server");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,528 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network
|
||||
|
||||
import dorkbox.network.aeron.client.ClientException
|
||||
import dorkbox.network.aeron.client.ClientTimedOutException
|
||||
import dorkbox.network.connection.*
|
||||
import dorkbox.util.exceptions.SecurityException
|
||||
import kotlinx.atomicfu.atomic
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* The client is both SYNC and ASYNC. It starts off SYNC (blocks thread until it's done), then once it's connected to the server, it's
|
||||
* ASYNC.
|
||||
*/
|
||||
open class Client<C : Connection>(config: Configuration = Configuration()) : EndPoint<C>(config) {
|
||||
companion object {
|
||||
/**
|
||||
* Gets the version number.
|
||||
*/
|
||||
const val version = "4.1"
|
||||
|
||||
/**
|
||||
* Split array into chunks, max of 256 chunks.
|
||||
* byte[0] = chunk ID
|
||||
* byte[1] = total chunks (0-255) (where 0->1, 2->3, 127->127 because this is indexed by a byte)
|
||||
*/
|
||||
private fun divideArray(source: ByteArray, chunksize: Int): Array<ByteArray>? {
|
||||
val fragments = Math.ceil(source.size / chunksize.toDouble()).toInt()
|
||||
if (fragments > 127) {
|
||||
// cannot allow more than 127
|
||||
return null
|
||||
}
|
||||
|
||||
// pre-allocate the memory
|
||||
val splitArray = Array(fragments) { ByteArray(chunksize + 2) }
|
||||
var start = 0
|
||||
for (i in splitArray.indices) {
|
||||
var length: Int
|
||||
length = if (start + chunksize > source.size) {
|
||||
source.size - start
|
||||
} else {
|
||||
chunksize
|
||||
}
|
||||
splitArray[i] = ByteArray(length + 2)
|
||||
splitArray[i][0] = i.toByte() // index
|
||||
splitArray[i][1] = fragments.toByte() // total number of fragments
|
||||
System.arraycopy(source, start, splitArray[i], 2, length)
|
||||
start += chunksize
|
||||
}
|
||||
return splitArray
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The network or IPC address for the client to connect to.
|
||||
*
|
||||
* For a network address, it can be:
|
||||
* - a network name ("localhost", "loopback", "lo", "bob.example.org")
|
||||
* - an IP address ("127.0.0.1", "123.123.123.123", "::1")
|
||||
*
|
||||
* For the IPC (Inter-Process-Communication) address. it must be:
|
||||
* - the IPC integer ID, "0x1337c0de", "0x12312312", etc.
|
||||
*/
|
||||
private var remoteAddress = ""
|
||||
|
||||
private val isConnected = atomic(false)
|
||||
|
||||
// is valid when there is a connection to the server, otherwise it is null
|
||||
private var connection: C? = null
|
||||
|
||||
@Volatile
|
||||
protected var connectionTimeoutMS: Long = 5000 // default is 5 seconds
|
||||
|
||||
private val previousClosedConnectionActivity: Long = 0
|
||||
|
||||
// we don't verify anything on the CLIENT. We only verify on the server.
|
||||
// we don't support registering NEW classes after the client starts.
|
||||
|
||||
// ALSO make sure to verify registration details
|
||||
|
||||
// we don't verify anything on the CLIENT. We only verify on the server.
|
||||
// we don't support registering NEW classes after the client starts.
|
||||
// if (!registrationWrapper.initClassRegistration(channel, registration))
|
||||
// {
|
||||
// // abort if something messed up!
|
||||
// shutdown(channel, registration.sessionID)
|
||||
// }
|
||||
|
||||
override val connectionManager = ConnectionManagerClient<C>(logger, config)
|
||||
|
||||
init {
|
||||
// have to do some basic validation of our configuration
|
||||
if (config.publicationPort <= 0) { throw ClientException("configuration port must be > 0") }
|
||||
if (config.publicationPort >= 65535) { throw ClientException("configuration port must be < 65535") }
|
||||
|
||||
if (config.subscriptionPort <= 0) { throw ClientException("configuration controlPort must be > 0") }
|
||||
if (config.subscriptionPort >= 65535) { throw ClientException("configuration controlPort must be < 65535") }
|
||||
|
||||
if (config.networkMtuSize <= 0) { throw ClientException("configuration networkMtuSize must be > 0") }
|
||||
if (config.networkMtuSize >= 9 * 1024) { throw ClientException("configuration networkMtuSize must be < ${9 * 1024}") }
|
||||
|
||||
closables.add(connectionManager)
|
||||
}
|
||||
|
||||
/**
|
||||
* Will attempt to connect to the server, with a default 30 second connection timeout and will BLOCK until completed
|
||||
*
|
||||
* For a network address, it can be:
|
||||
* - a network name ("localhost", "loopback", "lo", "bob.example.org")
|
||||
* - an IP address ("127.0.0.1", "123.123.123.123", "::1")
|
||||
*
|
||||
* For the IPC (Inter-Process-Communication) address. it must be:
|
||||
* - the IPC integer ID, "0x1337c0de", "0x12312312", etc.
|
||||
*
|
||||
* Note: Case does not matter, and "localhost" is the default. IPC address must be in HEX notation (starting with '0x')
|
||||
*
|
||||
*
|
||||
* @param remoteAddress The network or IPC address for the client to connect to
|
||||
* @param connectionTimeout wait for x milliseconds. 0 will wait indefinitely
|
||||
* @param reliable true if we want to create a reliable connection. IPC connections are always reliable
|
||||
*
|
||||
* @throws IOException if the client is unable to connect in x amount of time
|
||||
*/
|
||||
@JvmOverloads
|
||||
@Throws(IOException::class, ClientTimedOutException::class)
|
||||
suspend fun connect(remoteAddress: String = "localhost", connectionTimeoutMS: Long = 30_000L, reliable: Boolean = true) {
|
||||
if (isConnected.value) {
|
||||
throw IOException("Unable to connect when already connected!");
|
||||
}
|
||||
|
||||
this.connectionTimeoutMS = connectionTimeoutMS
|
||||
// localhost/loopback IP might not always be 127.0.0.1 or ::1
|
||||
when (remoteAddress) {
|
||||
"loopback", "localhost", "lo" -> this.remoteAddress = NetUtil.LOCALHOST.hostAddress
|
||||
else -> when {
|
||||
remoteAddress.startsWith("127.") -> this.remoteAddress = NetUtil.LOCALHOST.hostAddress
|
||||
remoteAddress.startsWith("::1") -> this.remoteAddress = NetUtil.LOCALHOST6.hostAddress
|
||||
else -> this.remoteAddress = remoteAddress
|
||||
}
|
||||
}
|
||||
|
||||
// if we are IPv6, the IP must be in '[]'
|
||||
if (this.remoteAddress.count { it == ':' } > 1 &&
|
||||
this.remoteAddress.count { it == '[' } < 1 &&
|
||||
this.remoteAddress.count { it == ']' } < 1) {
|
||||
|
||||
this.remoteAddress = """[${this.remoteAddress}]"""
|
||||
}
|
||||
|
||||
if (this.remoteAddress == "0.0.0.0") {
|
||||
throw IllegalArgumentException("0.0.0.0 is an invalid address to connect to!")
|
||||
}
|
||||
|
||||
|
||||
// this is an IPC address
|
||||
if (this.remoteAddress.startsWith("0x")) {
|
||||
val ipcAddress: Long
|
||||
try {
|
||||
ipcAddress = remoteAddress.toLong(radix = 16)
|
||||
} catch (e: Exception) {
|
||||
throw IOException(e)
|
||||
}
|
||||
|
||||
// val connectionType3 = ConnectionType(config.remoteAddress, config.controlPort, config.port, false, MediaDriverType.IPC, IPC_STREAM_ID)
|
||||
}
|
||||
else {
|
||||
// this is a network address
|
||||
|
||||
// initially we only connect to the client connect ports. Ports are flipped because they are in the perspective of the SERVER
|
||||
val handshakeConnection = UdpMediaDriverConnection(
|
||||
this.remoteAddress, config.publicationPort, config.subscriptionPort,
|
||||
UDP_HANDSHAKE_STREAM_ID, RESERVED_SESSION_ID_INVALID,
|
||||
connectionTimeoutMS, reliable)
|
||||
|
||||
closables.add(handshakeConnection)
|
||||
|
||||
// throws a ConnectTimedOutException if the client cannot connect for any reason to the server handshake ports
|
||||
handshakeConnection.buildClient(aeron)
|
||||
logger.debug(handshakeConnection.clientInfo())
|
||||
|
||||
|
||||
// this will block until the connection timeout, and throw an exception if we were unable to connect with the server
|
||||
|
||||
// @Throws(ConnectTimedOutException::class, ClientRejectedException::class)
|
||||
val connectionInfo = connectionManager.initHandshake(handshakeConnection, connectionTimeoutMS, this@Client)
|
||||
|
||||
|
||||
// we are now connected, so we can connect to the NEW client-specific ports
|
||||
val reliableClientConnection = UdpMediaDriverConnection(handshakeConnection.address, connectionInfo.subscriptionPort, connectionInfo.publicationPort,
|
||||
connectionInfo.streamId, connectionInfo.sessionId, connectionTimeoutMS, handshakeConnection.isReliable)
|
||||
|
||||
// VALIDATE:: check to see if the remote connection's public key has changed!
|
||||
if (!validateRemoteAddress(NetworkUtil.IP.toInt(this.remoteAddress), connectionInfo.publicKey)) {
|
||||
// TODO: this should provide info to a callback
|
||||
println("connection not allowed! public key mismatch")
|
||||
return
|
||||
}
|
||||
|
||||
// VALIDATE TODO: make sure the serialization matches between the client/server! ?? One the client, it will just time out i
|
||||
// think, because we don't want to give validation information to the client... (so clients cannot probe what the registrations
|
||||
// are)
|
||||
|
||||
|
||||
|
||||
|
||||
// only the client connects to the server, so here we have to connect. The server (when creating the new "connection" object)
|
||||
// does not need to do anything
|
||||
//
|
||||
// throws a ConnectTimedOutException if the client cannot connect for any reason to the server-assigned client ports
|
||||
logger.debug(reliableClientConnection.clientInfo())
|
||||
|
||||
val newConnection = newConnection(this, reliableClientConnection)
|
||||
closables.add(newConnection)
|
||||
connection = newConnection
|
||||
|
||||
// VALIDATE are we allowed to connect to this server (now that we have the initial server information)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val permitConnection = connectionManager.notifyFilter(newConnection)
|
||||
if (!permitConnection) {
|
||||
// TODO: this should provide info to a callback
|
||||
println("connection not allowed!")
|
||||
return
|
||||
}
|
||||
|
||||
// have to make a new thread to listen for incoming data!
|
||||
// SUBSCRIPTIONS ARE NOT THREAD SAFE!
|
||||
actionDispatch.launch {
|
||||
val pollIdleStrategy = config.pollIdleStrategy
|
||||
|
||||
while (!isShutdown()) {
|
||||
val pollCount = newConnection.pollSubscriptions()
|
||||
|
||||
// 0 means we idle. >0 means reset and don't idle (because there are likely more poll events)
|
||||
pollIdleStrategy.idle(pollCount)
|
||||
}
|
||||
}
|
||||
|
||||
isConnected.lazySet(true)
|
||||
connectionManager.notifyConnect(newConnection)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if this client has connected yet or not.
|
||||
*
|
||||
* @return true if we are connected, false otherwise.
|
||||
*/
|
||||
override fun isConnected(): Boolean {
|
||||
return isConnected.value
|
||||
}
|
||||
|
||||
// override fun hasRemoteKeyChanged(): Boolean {
|
||||
// return connection!!.hasRemoteKeyChanged()
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * @return the remote address, as a string.
|
||||
// */
|
||||
// override fun getRemoteHost(): String {
|
||||
// return connection!!.remoteHost
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * @return true if this connection is established on the loopback interface
|
||||
// */
|
||||
// override fun isLoopback(): Boolean {
|
||||
// return connection!!.isLoopback
|
||||
// }
|
||||
//
|
||||
// override fun isIPC(): Boolean {
|
||||
// return false
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * @return true if this connection is a network connection
|
||||
// */
|
||||
// override fun isNetwork(): Boolean {
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * @return the connection (TCP or LOCAL) id of this connection.
|
||||
// */
|
||||
// override fun id(): Int {
|
||||
// return connection!!.id()
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * @return the connection (TCP or LOCAL) id of this connection as a HEX string.
|
||||
// */
|
||||
// override fun idAsHex(): String {
|
||||
// return connection!!.idAsHex()
|
||||
// }
|
||||
|
||||
/**
|
||||
* Tells the remote connection to create a new proxy object that implements the specified interface. The methods on this object "map"
|
||||
* to an object that is created remotely.
|
||||
*
|
||||
*
|
||||
* The callback will be notified when the remote object has been created.
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
* Methods that return a value will throw [TimeoutException] if the response is not received with the
|
||||
* [response timeout][RemoteObject.setResponseTimeout].
|
||||
*
|
||||
*
|
||||
* If [non-blocking][RemoteObject.setAsync] is false (the default), then methods that return a value must
|
||||
* not be called from the update thread for the connection. An exception will be thrown if this occurs. Methods with a
|
||||
* void return value can be called on the update thread.
|
||||
*
|
||||
*
|
||||
* If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side
|
||||
* will have the proxy object replaced with the registered (non-proxy) object.
|
||||
*
|
||||
*
|
||||
* If one wishes to change the default behavior, cast the object to access the different methods.
|
||||
* ie: `RemoteObject remoteObject = (RemoteObject) test;`
|
||||
*
|
||||
* @see RemoteObject
|
||||
*/
|
||||
// override fun <Iface> createRemoteObject(interfaceClass: Class<Iface>, callback: RemoteObjectCallback<Iface>) {
|
||||
// try {
|
||||
// connection!!.createRemoteObject(interfaceClass, callback)
|
||||
// } catch (e: NullPointerException) {
|
||||
// logger.error("Error creating remote object!", e)
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Tells the remote connection to create a new proxy object that implements the specified interface. The methods on this object "map"
|
||||
* to an object that is created remotely.
|
||||
*
|
||||
*
|
||||
* The callback will be notified when the remote object has been created.
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
* Methods that return a value will throw [TimeoutException] if the response is not received with the
|
||||
* [response timeout][RemoteObject.setResponseTimeout].
|
||||
*
|
||||
*
|
||||
* If [non-blocking][RemoteObject.setAsync] is false (the default), then methods that return a value must
|
||||
* not be called from the update thread for the connection. An exception will be thrown if this occurs. Methods with a
|
||||
* void return value can be called on the update thread.
|
||||
*
|
||||
*
|
||||
* If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side
|
||||
* will have the proxy object replaced with the registered (non-proxy) object.
|
||||
*
|
||||
*
|
||||
* If one wishes to change the default behavior, cast the object to access the different methods.
|
||||
* ie: `RemoteObject remoteObject = (RemoteObject) test;`
|
||||
*
|
||||
* @see RemoteObject
|
||||
*/
|
||||
// override fun <Iface> getRemoteObject(objectId: Int, callback: RemoteObjectCallback<Iface>) {
|
||||
// try {
|
||||
// connection!!.getRemoteObject(objectId, callback)
|
||||
// } catch (e: NullPointerException) {
|
||||
// logger.error("Error getting remote object!", e)
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
/**
|
||||
* Fetches the connection used by the client.
|
||||
*
|
||||
*
|
||||
* Make **sure** that you only call this **after** the client connects!
|
||||
*
|
||||
*
|
||||
* This is preferred to [EndPoint.getConnections], as it properly does some error checking
|
||||
*/
|
||||
// can =just use super.get connection?
|
||||
// override var connection: C = TODO()
|
||||
// get() = field
|
||||
// set(connection) {
|
||||
// super.connection = connection
|
||||
// }
|
||||
|
||||
|
||||
|
||||
@Throws(ClientException::class)
|
||||
suspend fun send(message: Any) {
|
||||
val c = connection
|
||||
if (c != null) {
|
||||
c.send(message)
|
||||
} else {
|
||||
throw ClientException("Cannot send a message when there is no connection!")
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(ClientException::class)
|
||||
suspend fun send(message: Any, priority: Byte) {
|
||||
val c = connection
|
||||
if (c != null) {
|
||||
c.send(message, priority)
|
||||
} else {
|
||||
throw ClientException("Cannot send a message when there is no connection!")
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(ClientException::class)
|
||||
suspend fun ping(): Ping {
|
||||
val c = connection
|
||||
if (c != null) {
|
||||
return c.ping()
|
||||
} else {
|
||||
throw ClientException("Cannot ping a connection when there is no connection!")
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(SecurityException::class)
|
||||
fun removeRegisteredServerKey(hostAddress: Int) {
|
||||
val savedPublicKey = settingsStore.getRegisteredServerKey(hostAddress)
|
||||
if (savedPublicKey != null) {
|
||||
val logger2 = logger
|
||||
if (logger2.isDebugEnabled) {
|
||||
logger2.debug("Deleting remote IP address key ${NetworkUtil.IP.toString(hostAddress)}")
|
||||
}
|
||||
settingsStore.removeRegisteredServerKey(hostAddress)
|
||||
}
|
||||
}
|
||||
|
||||
// fun initClassRegistration(channel: Channel, registration: Registration): Boolean {
|
||||
// val details = serialization.getKryoRegistrationDetails()
|
||||
// val length = details.size
|
||||
// if (length > Serialization.CLASS_REGISTRATION_VALIDATION_FRAGMENT_SIZE) {
|
||||
// // it is too large to send in a single packet
|
||||
//
|
||||
// // child arrays have index 0 also as their 'index' and 1 is the total number of fragments
|
||||
// val fragments = divideArray(details, Serialization.CLASS_REGISTRATION_VALIDATION_FRAGMENT_SIZE)
|
||||
// if (fragments == null) {
|
||||
// logger.error("Too many classes have been registered for Serialization. Please report this issue")
|
||||
// return false
|
||||
// }
|
||||
// val allButLast = fragments.size - 1
|
||||
// for (i in 0 until allButLast) {
|
||||
// val fragment = fragments[i]
|
||||
// val fragmentedRegistration = Registration.hello(registration.oneTimePad, config.settingsStore.getPublicKey())
|
||||
// fragmentedRegistration.payload = fragment
|
||||
//
|
||||
// // tell the server we are fragmented
|
||||
// fragmentedRegistration.upgradeType = UpgradeType.FRAGMENTED
|
||||
//
|
||||
// // tell the server we are upgraded (it will bounce back telling us to connect)
|
||||
// fragmentedRegistration.upgraded = true
|
||||
// channel.writeAndFlush(fragmentedRegistration)
|
||||
// }
|
||||
//
|
||||
// // now tell the server we are done with the fragments
|
||||
// val fragmentedRegistration = Registration.hello(registration.oneTimePad, config.settingsStore.getPublicKey())
|
||||
// fragmentedRegistration.payload = fragments[allButLast]
|
||||
//
|
||||
// // tell the server we are fragmented
|
||||
// fragmentedRegistration.upgradeType = UpgradeType.FRAGMENTED
|
||||
//
|
||||
// // tell the server we are upgraded (it will bounce back telling us to connect)
|
||||
// fragmentedRegistration.upgraded = true
|
||||
// channel.writeAndFlush(fragmentedRegistration)
|
||||
// } else {
|
||||
// registration.payload = details
|
||||
//
|
||||
// // tell the server we are upgraded (it will bounce back telling us to connect)
|
||||
// registration.upgraded = true
|
||||
// channel.writeAndFlush(registration)
|
||||
// }
|
||||
// return true
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Closes all connections ONLY (keeps the client running). To STOP the client, use stop().
|
||||
// * <p/>
|
||||
// * This is used, for example, when reconnecting to a server.
|
||||
// */
|
||||
// protected
|
||||
// void closeConnection() {
|
||||
// if (isConnected.get()) {
|
||||
// // make sure we're not waiting on registration
|
||||
// stopRegistration();
|
||||
//
|
||||
// // for the CLIENT only, we clear these connections! (the server only clears them on shutdown)
|
||||
//
|
||||
// // stop does the same as this + more. Only keep the listeners for connections IF we are the client. If we remove listeners as a client,
|
||||
// // ALL of the client logic will be lost. The server is reactive, so listeners are added to connections as needed (instead of before startup)
|
||||
// connectionManager.closeConnections(true);
|
||||
//
|
||||
// // Sometimes there might be "lingering" connections (ie, halfway though registration) that need to be closed.
|
||||
// registrationWrapper.clearSessions();
|
||||
//
|
||||
//
|
||||
// closeConnections(true);
|
||||
// shutdownAllChannels();
|
||||
// // shutdownEventLoops(); we don't do this here!
|
||||
//
|
||||
// connection = null;
|
||||
// isConnected.set(false);
|
||||
//
|
||||
// previousClosedConnectionActivity = System.nanoTime();
|
||||
// }
|
||||
// }
|
||||
// /**
|
||||
// * Internal call to abort registration if the shutdown command is issued during channel registration.
|
||||
// */
|
||||
// @Suppress("unused")
|
||||
// fun abortRegistration() {
|
||||
// // make sure we're not waiting on registration
|
||||
//// stopRegistration()
|
||||
// }
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network;
|
||||
|
||||
public
|
||||
class ClientConfiguration extends Configuration {
|
||||
|
||||
/**
|
||||
* The network or IPC address for the client to connect to.
|
||||
*
|
||||
* For a network connection, it can be:
|
||||
* - a network name ("localhost", "loopback", "lo", "bob.example.org")
|
||||
* - an IP address ("127.0.0.1", "123.123.123.123")
|
||||
*
|
||||
* For the IPC (Inter-Process-Communication) connection. it must be:
|
||||
* - the IPC designation, "ipc"
|
||||
*
|
||||
* Note: Case does not matter, and "localhost" is the default
|
||||
*/
|
||||
public String remoteAddress = "localhost";
|
||||
|
||||
public
|
||||
ClientConfiguration() {
|
||||
super();
|
||||
}
|
||||
}
|
|
@ -1,256 +0,0 @@
|
|||
package dorkbox.network;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.agrona.BufferUtil;
|
||||
import org.agrona.concurrent.UnsafeBuffer;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import dorkbox.network.aeron.EchoMessages;
|
||||
import dorkbox.network.aeron.EchoServerAddressCounter;
|
||||
import dorkbox.network.aeron.EchoServerDuologue;
|
||||
import dorkbox.network.aeron.EchoServerExecutorService;
|
||||
import dorkbox.network.aeron.EchoServerSessionAllocator;
|
||||
import dorkbox.network.aeron.exceptions.EchoServerException;
|
||||
import dorkbox.network.aeron.exceptions.EchoServerPortAllocationException;
|
||||
import dorkbox.network.aeron.exceptions.EchoServerSessionAllocationException;
|
||||
import dorkbox.network.aeron.server.PortAllocator;
|
||||
import dorkbox.network.connection.EndPoint;
|
||||
import io.aeron.Aeron;
|
||||
import io.aeron.Publication;
|
||||
|
||||
class ClientStates {
|
||||
private static final Pattern PATTERN_HELLO = Pattern.compile("^HELLO ([0-9A-F]+)$");
|
||||
|
||||
private final Map<Integer, InetAddress> client_session_addresses;
|
||||
private final Map<Integer, EchoServerDuologue> client_duologues;
|
||||
private final PortAllocator port_allocator;
|
||||
private final Aeron aeron;
|
||||
private final Clock clock;
|
||||
private final ServerConfiguration configuration;
|
||||
private Logger logger;
|
||||
private final UnsafeBuffer send_buffer;
|
||||
private final EchoServerExecutorService exec;
|
||||
private final EchoServerAddressCounter address_counter;
|
||||
private final EchoServerSessionAllocator session_allocator;
|
||||
|
||||
ClientStates(final Aeron in_aeron,
|
||||
final Clock in_clock,
|
||||
final EchoServerExecutorService in_exec,
|
||||
final ServerConfiguration in_configuration,
|
||||
final Logger logger) {
|
||||
|
||||
this.aeron = Objects.requireNonNull(in_aeron, "Aeron");
|
||||
this.clock = Objects.requireNonNull(in_clock, "Clock");
|
||||
this.exec = Objects.requireNonNull(in_exec, "Executor");
|
||||
this.configuration = Objects.requireNonNull(in_configuration, "Configuration");
|
||||
this.logger = logger;
|
||||
|
||||
this.client_duologues = new HashMap<>(32);
|
||||
this.client_session_addresses = new HashMap<>(32);
|
||||
|
||||
this.port_allocator = PortAllocator.create(this.configuration.clientStartPort,
|
||||
2 * this.configuration.maxClientCount); // 2 ports used per connection
|
||||
|
||||
this.address_counter = EchoServerAddressCounter.create();
|
||||
|
||||
this.session_allocator = EchoServerSessionAllocator.create(EndPoint.RESERVED_SESSION_ID_LOW,
|
||||
EndPoint.RESERVED_SESSION_ID_HIGH,
|
||||
new SecureRandom());
|
||||
|
||||
this.send_buffer = new UnsafeBuffer(BufferUtil.allocateDirectAligned(1024, 16));
|
||||
}
|
||||
|
||||
void onInitialClientMessageProcess(final Publication publication,
|
||||
final String session_name,
|
||||
final Integer session_boxed,
|
||||
final String message) throws EchoServerException, IOException {
|
||||
this.exec.assertIsExecutorThread();
|
||||
|
||||
logger.debug("[{}] received: {}", session_name, message);
|
||||
|
||||
/*
|
||||
* The HELLO command is the only acceptable message from clients
|
||||
* on the all-clients channel.
|
||||
*/
|
||||
final Matcher hello_matcher = PATTERN_HELLO.matcher(message);
|
||||
if (!hello_matcher.matches()) {
|
||||
EchoMessages.sendMessage(publication, this.send_buffer, errorMessage(session_name, "bad message"));
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* Check to see if there are already too many clients connected.
|
||||
*/
|
||||
if (this.client_duologues.size() >= this.configuration.maxClientCount) {
|
||||
logger.debug("server is full");
|
||||
EchoMessages.sendMessage(publication, this.send_buffer, errorMessage(session_name, "server full"));
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* Check to see if this IP address already has the maximum number of
|
||||
* duologues allocated to it.
|
||||
*/
|
||||
final InetAddress owner = this.client_session_addresses.get(session_boxed);
|
||||
|
||||
if (this.address_counter.countFor(owner) >= this.configuration.maxConnectionsPerIpAddress) {
|
||||
logger.debug("too many connections for IP address");
|
||||
EchoMessages.sendMessage(publication, this.send_buffer, errorMessage(session_name, "too many connections for IP address"));
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* Parse the one-time pad with which the client wants the server to
|
||||
* encrypt the identifier of the session that will be created.
|
||||
*/
|
||||
final int duologue_key = Integer.parseUnsignedInt(hello_matcher.group(1), 16);
|
||||
|
||||
/*
|
||||
* Allocate a new duologue, encrypt the resulting session ID, and send
|
||||
* a message to the client telling it where to find the new duologue.
|
||||
*/
|
||||
final EchoServerDuologue duologue = this.allocateNewDuologue(session_name, session_boxed, owner);
|
||||
|
||||
final String session_crypt = Integer.toUnsignedString(duologue_key ^ duologue.session(), 16)
|
||||
.toUpperCase();
|
||||
|
||||
EchoMessages.sendMessage(publication,
|
||||
this.send_buffer,
|
||||
connectMessage(session_name, duologue.portData(), duologue.portControl(), session_crypt));
|
||||
}
|
||||
|
||||
private static
|
||||
String connectMessage(final String session_name, final int port_data, final int port_control, final String session) {
|
||||
return new StringBuilder(64).append(session_name)
|
||||
.append(" CONNECT ")
|
||||
.append(port_data)
|
||||
.append(" ")
|
||||
.append(port_control)
|
||||
.append(" ")
|
||||
.append(session)
|
||||
.toString();
|
||||
}
|
||||
|
||||
private static
|
||||
String errorMessage(final String session_name, final String message) {
|
||||
return new StringBuilder(64).append(session_name)
|
||||
.append(" ERROR ")
|
||||
.append(message)
|
||||
.toString();
|
||||
}
|
||||
|
||||
private
|
||||
EchoServerDuologue allocateNewDuologue(final String session_name, final Integer session_boxed, final InetAddress owner)
|
||||
throws EchoServerPortAllocationException, EchoServerSessionAllocationException {
|
||||
this.address_counter.increment(owner);
|
||||
|
||||
final EchoServerDuologue duologue;
|
||||
try {
|
||||
final int[] ports = this.port_allocator.allocate(2);
|
||||
try {
|
||||
final int session = this.session_allocator.allocate();
|
||||
try {
|
||||
duologue = EchoServerDuologue.create(this.aeron,
|
||||
this.clock,
|
||||
this.exec,
|
||||
this.configuration.listenIpAddress,
|
||||
owner,
|
||||
session,
|
||||
ports[0],
|
||||
ports[1]);
|
||||
logger.debug("[{}] created new duologue", session_name);
|
||||
this.client_duologues.put(session_boxed, duologue);
|
||||
} catch (final Exception e) {
|
||||
this.session_allocator.free(session);
|
||||
throw e;
|
||||
}
|
||||
} catch (final EchoServerSessionAllocationException e) {
|
||||
this.port_allocator.free(ports[0]);
|
||||
this.port_allocator.free(ports[1]);
|
||||
throw e;
|
||||
}
|
||||
} catch (final EchoServerPortAllocationException e) {
|
||||
this.address_counter.decrement(owner);
|
||||
throw e;
|
||||
}
|
||||
return duologue;
|
||||
}
|
||||
|
||||
void onInitialClientDisconnected(final int session_id) {
|
||||
this.exec.assertIsExecutorThread();
|
||||
|
||||
this.client_session_addresses.remove(Integer.valueOf(session_id));
|
||||
}
|
||||
|
||||
void onInitialClientConnected(final int session_id, final InetAddress client_address) {
|
||||
this.exec.assertIsExecutorThread();
|
||||
|
||||
this.client_session_addresses.put(Integer.valueOf(session_id), client_address);
|
||||
}
|
||||
|
||||
public
|
||||
void poll() {
|
||||
this.exec.assertIsExecutorThread();
|
||||
|
||||
final Iterator<Map.Entry<Integer, EchoServerDuologue>> iter = this.client_duologues.entrySet()
|
||||
.iterator();
|
||||
|
||||
/*
|
||||
* Get the current time; used to expire duologues.
|
||||
*/
|
||||
|
||||
final Instant now = this.clock.instant();
|
||||
|
||||
while (iter.hasNext()) {
|
||||
final Map.Entry<Integer, EchoServerDuologue> entry = iter.next();
|
||||
final EchoServerDuologue duologue = entry.getValue();
|
||||
|
||||
final String session_name = Integer.toString(entry.getKey()
|
||||
.intValue());
|
||||
|
||||
/*
|
||||
* If the duologue has either been closed, or has expired, it needs
|
||||
* to be deleted.
|
||||
*/
|
||||
boolean delete = false;
|
||||
if (duologue.isExpired(now)) {
|
||||
logger.debug("[{}] duologue expired", session_name);
|
||||
delete = true;
|
||||
}
|
||||
|
||||
if (duologue.isClosed()) {
|
||||
logger.debug("[{}] duologue closed", session_name);
|
||||
delete = true;
|
||||
}
|
||||
|
||||
if (delete) {
|
||||
try {
|
||||
duologue.close();
|
||||
} finally {
|
||||
logger.debug("[{}] deleted duologue", session_name);
|
||||
iter.remove();
|
||||
this.port_allocator.free(duologue.portData());
|
||||
this.port_allocator.free(duologue.portControl());
|
||||
this.address_counter.decrement(duologue.ownerAddress());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* Otherwise, poll the duologue for activity.
|
||||
*/
|
||||
duologue.poll();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,160 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.agrona.concurrent.BackoffIdleStrategy;
|
||||
import org.agrona.concurrent.IdleStrategy;
|
||||
|
||||
import dorkbox.network.serialization.NetworkSerializationManager;
|
||||
import dorkbox.network.store.SettingsStore;
|
||||
import io.aeron.driver.ThreadingMode;
|
||||
|
||||
public
|
||||
class Configuration {
|
||||
/**
|
||||
* Specify the UDP port to use. This port is used to establish client-server connections.
|
||||
* <p>
|
||||
* Must be greater than 0
|
||||
*/
|
||||
public int port = 0;
|
||||
|
||||
/**
|
||||
* Specify the UDP MDC control port to use. This port is used to establish client-server connections.
|
||||
* <p>
|
||||
* Must be greater than 0
|
||||
*/
|
||||
public int controlPort = 0;
|
||||
|
||||
/**
|
||||
* Allows the end user to change how server settings are stored. For example, a custom database instead of the default.
|
||||
*/
|
||||
public SettingsStore settingsStore = null;
|
||||
|
||||
/**
|
||||
* Specify the serialization manager to use. If null, it uses the default.
|
||||
*/
|
||||
public NetworkSerializationManager serialization = null;
|
||||
|
||||
|
||||
/**
|
||||
* The idle strategy used when polling the Media Driver for new messages. BackOffIdleStrategy is the DEFAULT.
|
||||
*
|
||||
* There are a couple strategies of importance to understand.
|
||||
* <p>
|
||||
* BusySpinIdleStrategy uses a busy spin as an idle and will eat up CPU by default.
|
||||
* <p>
|
||||
* BackOffIdleStrategy uses a backoff strategy of spinning, yielding, and parking to be kinder to the CPU, but to be less
|
||||
* responsive to activity when idle for a little while.
|
||||
* <p>
|
||||
* <p>
|
||||
* The main difference in strategies is how responsive to changes should the idler be when idle for a little bit of time and
|
||||
* how much CPU should be consumed when no work is being done. There is an inherent tradeoff to consider.
|
||||
*/
|
||||
public IdleStrategy messagePollIdleStrategy = new BackoffIdleStrategy(100, 10,
|
||||
TimeUnit.MICROSECONDS.toNanos(10),
|
||||
TimeUnit.MILLISECONDS.toNanos(100));
|
||||
|
||||
|
||||
/**
|
||||
* A Media Driver, whether being run embedded or not, needs 1-3 threads to perform its operation.
|
||||
* <p>
|
||||
* There are three main Agents in the driver:
|
||||
* <p>
|
||||
* Conductor: Responsible for reacting to client requests and house keeping duties as well as detecting loss, sending NAKs,
|
||||
* rotating buffers, etc.
|
||||
* Sender: Responsible for shovelling messages from publishers to the network.
|
||||
* Receiver: Responsible for shovelling messages from the network to subscribers.
|
||||
* <p>
|
||||
* This value can be one of:
|
||||
* <p>
|
||||
* INVOKER: No threads. The client is responsible for using the MediaDriver.Context.driverAgentInvoker() to invoke the duty
|
||||
* cycle directly.
|
||||
* SHARED: All Agents share a single thread. 1 thread in total.
|
||||
* SHARED_NETWORK: Sender and Receiver shares a thread, conductor has its own thread. 2 threads in total.
|
||||
* DEDICATED: The default and dedicates one thread per Agent. 3 threads in total.
|
||||
* <p>
|
||||
* For performance, it is recommended to use DEDICATED as long as the number of busy threads is less than or equal to the number of
|
||||
* spare cores on the machine. If there are not enough cores to dedicate, then it is recommended to consider sharing some with
|
||||
* SHARED_NETWORK or SHARED. INVOKER can be used for low resource environments while the application using Aeron can invoke the
|
||||
* media driver to carry out its duty cycle on a regular interval.
|
||||
*/
|
||||
public ThreadingMode threadingMode = ThreadingMode.SHARED;
|
||||
|
||||
/**
|
||||
* Log Buffer Locations for the Media Driver. The default location is a TEMP dir. This must be unique PER application and instance!
|
||||
*/
|
||||
public File aeronLogDirectory = null;
|
||||
|
||||
/**
|
||||
* The Aeron MTU value impacts a lot of things.
|
||||
* <p>
|
||||
* The default MTU is set to a value that is a good trade-off. However, it is suboptimal for some use cases involving very large
|
||||
* (> 4KB) messages and for maximizing throughput above everything else. Various checks during publication and subscription/connection
|
||||
* setup are done to verify a decent relationship with MTU.
|
||||
* <p>
|
||||
* However, it is good to understand these relationships.
|
||||
* <p>
|
||||
* The MTU on the Media Driver controls the length of the MTU of data frames. This value is communicated to the Aeron clients during
|
||||
* registration. So, applications do not have to concern themselves with the MTU value used by the Media Driver and use the same value.
|
||||
* <p>
|
||||
* An MTU value over the interface MTU will cause IP to fragment the datagram. This may increase the likelihood of loss under several
|
||||
* circumstances. If increasing the MTU over the interface MTU, consider various ways to increase the interface MTU first in preparation.
|
||||
* <p>
|
||||
* The MTU value indicates the largest message that Aeron will send as a single data frame.
|
||||
* <p>
|
||||
* MTU length also has implications for socket buffer sizing.
|
||||
* <p>
|
||||
* <p>
|
||||
* Default value is 1408 for internet; for a LAN, 9k is possible with jumbo frames (if the routers/interfaces support it)
|
||||
*/
|
||||
public int networkMtuSize = io.aeron.driver.Configuration.MTU_LENGTH_DEFAULT;
|
||||
|
||||
/**
|
||||
* This option (ultimately SO_SNDBUF for the network socket) can impact loss rate. Loss can occur on the sender side due
|
||||
* to this buffer being too small.
|
||||
* <p>
|
||||
* This buffer must be large enough to accommodate the MTU as a minimum. In addition, some systems, most notably Windows,
|
||||
* need plenty of buffering on the send side to reach adequate throughput rates. If too large, this buffer can increase latency
|
||||
* or cause loss.
|
||||
* <p>
|
||||
* This usually should be less than 2MB.
|
||||
*/
|
||||
public int sendBufferSize = 0;
|
||||
|
||||
/**
|
||||
* This option (ultimately SO_RCVBUF for the network socket) can impact loss rates when too small for the given processing.
|
||||
* If too large, this buffer can increase latency.
|
||||
* <p>
|
||||
* Values that tend to work well with Aeron are 2MB to 4MB. This setting must be large enough for the MTU of the sender. If not,
|
||||
* persistent loss can result. In addition, the receiver window length should be less than or equal to this value to allow plenty
|
||||
* of space for burst traffic from a sender.
|
||||
*/
|
||||
public int receiveBufferSize = 0;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public
|
||||
Configuration() {
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,220 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network
|
||||
|
||||
import dorkbox.network.aeron.CoroutineBackoffIdleStrategy
|
||||
import dorkbox.network.aeron.CoroutineIdleStrategy
|
||||
import dorkbox.network.aeron.CoroutineSleepingMillisIdleStrategy
|
||||
import dorkbox.network.serialization.NetworkSerializationManager
|
||||
import dorkbox.network.serialization.Serialization
|
||||
import dorkbox.network.store.PropertyStore
|
||||
import dorkbox.network.store.SettingsStore
|
||||
import dorkbox.util.storage.StorageBuilder
|
||||
import dorkbox.util.storage.StorageSystem
|
||||
import io.aeron.driver.Configuration
|
||||
import io.aeron.driver.ThreadingMode
|
||||
import java.io.File
|
||||
|
||||
class ServerConfiguration : dorkbox.network.Configuration() {
|
||||
/**
|
||||
* The address for the server to listen on. "*" will accept connections from all interfaces, otherwise specify
|
||||
* the hostname (or IP) to bind to.
|
||||
*/
|
||||
var listenIpAddress = "*"
|
||||
|
||||
/**
|
||||
* The starting port for clients to use. The upper bound of this value is limited by the maximum number of clients allowed.
|
||||
*/
|
||||
var clientStartPort = 0
|
||||
|
||||
/**
|
||||
* The maximum number of clients allowed for a server
|
||||
*/
|
||||
var maxClientCount = 0
|
||||
|
||||
/**
|
||||
* The maximum number of client connection allowed per IP address
|
||||
*/
|
||||
var maxConnectionsPerIpAddress = 0
|
||||
}
|
||||
|
||||
open class Configuration {
|
||||
|
||||
/**
|
||||
* When connecting to a remote client/server, should connections be allowed if the remote machine signature has changed?
|
||||
*/
|
||||
var enableRemoteSignatureValidation: Boolean = true
|
||||
|
||||
/**
|
||||
* Specify the UDP port to use. This port is used to establish client-server connections, and is from the
|
||||
* perspective of the server
|
||||
*
|
||||
* This means that server-pub -> {{network}} -> client-sub
|
||||
*
|
||||
* Must be greater than 0
|
||||
*/
|
||||
var publicationPort: Int = 0
|
||||
|
||||
/**
|
||||
* Specify the UDP MDC subscription port to use. This port is used to establish client-server connections, and is from the
|
||||
* perspective of the server.
|
||||
*
|
||||
* This means that client-pub -> {{network}} -> server-sub
|
||||
*
|
||||
* Must be greater than 0
|
||||
*/
|
||||
var subscriptionPort: Int = 0
|
||||
|
||||
|
||||
/**
|
||||
* How long a connection must be disconnected before we cleanup the memory associated with it
|
||||
*/
|
||||
var connectionCleanupTimeoutInSeconds: Int = 10
|
||||
|
||||
/**
|
||||
* Allows the end user to change how endpoint settings are stored. For example, a custom database instead of the default.
|
||||
*/
|
||||
var settingsStore: SettingsStore = PropertyStore()
|
||||
|
||||
/**
|
||||
* Specify the type of storage used for the endpoint settings , the options are Disk and Memory
|
||||
*/
|
||||
var settingsStorageSystem: StorageBuilder = StorageSystem.Memory()
|
||||
|
||||
/**
|
||||
* Specify the serialization manager to use.
|
||||
*/
|
||||
var serialization: NetworkSerializationManager = Serialization.DEFAULT()
|
||||
|
||||
/**
|
||||
* The idle strategy used when polling the Media Driver for new messages. BackOffIdleStrategy is the DEFAULT.
|
||||
*
|
||||
* There are a couple strategies of importance to understand.
|
||||
* * BusySpinIdleStrategy uses a busy spin as an idle and will eat up CPU by default.
|
||||
* * BackOffIdleStrategy uses a backoff strategy of spinning, yielding, and parking to be kinder to the CPU, but to be less
|
||||
* responsive to activity when idle for a little while.
|
||||
*
|
||||
* The main difference in strategies is how responsive to changes should the idler be when idle for a little bit of time and
|
||||
* how much CPU should be consumed when no work is being done. There is an inherent tradeoff to consider.
|
||||
*/
|
||||
var pollIdleStrategy: CoroutineIdleStrategy = CoroutineBackoffIdleStrategy(maxSpins = 100, maxYields = 10, minParkPeriodMs = 1, maxParkPeriodMs = 100)
|
||||
|
||||
/**
|
||||
* The idle strategy used when polling the Media Driver for new messages. BackOffIdleStrategy is the DEFAULT.
|
||||
*
|
||||
* There are a couple strategies of importance to understand.
|
||||
* * BusySpinIdleStrategy uses a busy spin as an idle and will eat up CPU by default.
|
||||
* * BackOffIdleStrategy uses a backoff strategy of spinning, yielding, and parking to be kinder to the CPU, but to be less
|
||||
* responsive to activity when idle for a little while.
|
||||
*
|
||||
* The main difference in strategies is how responsive to changes should the idler be when idle for a little bit of time and
|
||||
* how much CPU should be consumed when no work is being done. There is an inherent tradeoff to consider.
|
||||
*/
|
||||
var sendIdleStrategy: CoroutineIdleStrategy = CoroutineSleepingMillisIdleStrategy(sleepPeriodMs = 100)
|
||||
|
||||
/**
|
||||
* A Media Driver, whether being run embedded or not, needs 1-3 threads to perform its operation.
|
||||
*
|
||||
*
|
||||
* There are three main Agents in the driver:
|
||||
*
|
||||
*
|
||||
* Conductor: Responsible for reacting to client requests and house keeping duties as well as detecting loss, sending NAKs,
|
||||
* rotating buffers, etc.
|
||||
* Sender: Responsible for shovelling messages from publishers to the network.
|
||||
* Receiver: Responsible for shovelling messages from the network to subscribers.
|
||||
*
|
||||
*
|
||||
* This value can be one of:
|
||||
*
|
||||
*
|
||||
* INVOKER: No threads. The client is responsible for using the MediaDriver.Context.driverAgentInvoker() to invoke the duty
|
||||
* cycle directly.
|
||||
* SHARED: All Agents share a single thread. 1 thread in total.
|
||||
* SHARED_NETWORK: Sender and Receiver shares a thread, conductor has its own thread. 2 threads in total.
|
||||
* DEDICATED: The default and dedicates one thread per Agent. 3 threads in total.
|
||||
*
|
||||
*
|
||||
* For performance, it is recommended to use DEDICATED as long as the number of busy threads is less than or equal to the number of
|
||||
* spare cores on the machine. If there are not enough cores to dedicate, then it is recommended to consider sharing some with
|
||||
* SHARED_NETWORK or SHARED. INVOKER can be used for low resource environments while the application using Aeron can invoke the
|
||||
* media driver to carry out its duty cycle on a regular interval.
|
||||
*/
|
||||
var threadingMode = ThreadingMode.SHARED
|
||||
|
||||
/**
|
||||
* Log Buffer Locations for the Media Driver. The default location is a TEMP dir. This must be unique PER application and instance!
|
||||
*/
|
||||
var aeronLogDirectory: File? = null
|
||||
|
||||
/**
|
||||
* The Aeron MTU value impacts a lot of things.
|
||||
*
|
||||
*
|
||||
* The default MTU is set to a value that is a good trade-off. However, it is suboptimal for some use cases involving very large
|
||||
* (> 4KB) messages and for maximizing throughput above everything else. Various checks during publication and subscription/connection
|
||||
* setup are done to verify a decent relationship with MTU.
|
||||
*
|
||||
*
|
||||
* However, it is good to understand these relationships.
|
||||
*
|
||||
*
|
||||
* The MTU on the Media Driver controls the length of the MTU of data frames. This value is communicated to the Aeron clients during
|
||||
* registration. So, applications do not have to concern themselves with the MTU value used by the Media Driver and use the same value.
|
||||
*
|
||||
*
|
||||
* An MTU value over the interface MTU will cause IP to fragment the datagram. This may increase the likelihood of loss under several
|
||||
* circumstances. If increasing the MTU over the interface MTU, consider various ways to increase the interface MTU first in preparation.
|
||||
*
|
||||
*
|
||||
* The MTU value indicates the largest message that Aeron will send as a single data frame.
|
||||
*
|
||||
*
|
||||
* MTU length also has implications for socket buffer sizing.
|
||||
*
|
||||
*
|
||||
* Default value is 1408 for internet; for a LAN, 9k is possible with jumbo frames (if the routers/interfaces support it)
|
||||
*/
|
||||
var networkMtuSize = Configuration.MTU_LENGTH_DEFAULT
|
||||
|
||||
/**
|
||||
* This option (ultimately SO_SNDBUF for the network socket) can impact loss rate. Loss can occur on the sender side due
|
||||
* to this buffer being too small.
|
||||
*
|
||||
*
|
||||
* This buffer must be large enough to accommodate the MTU as a minimum. In addition, some systems, most notably Windows,
|
||||
* need plenty of buffering on the send side to reach adequate throughput rates. If too large, this buffer can increase latency
|
||||
* or cause loss.
|
||||
*
|
||||
* This should be less than 2MB for most use-cases.
|
||||
*
|
||||
* A value of 0 will 'auto-configure' this setting
|
||||
*/
|
||||
var sendBufferSize = 0
|
||||
|
||||
/**
|
||||
* This option (ultimately SO_RCVBUF for the network socket) can impact loss rates when too small for the given processing.
|
||||
* If too large, this buffer can increase latency.
|
||||
*
|
||||
*
|
||||
* Values that tend to work well with Aeron are 2MB to 4MB. This setting must be large enough for the MTU of the sender. If not,
|
||||
* persistent loss can result. In addition, the receiver window length should be less than or equal to this value to allow plenty
|
||||
* of space for burst traffic from a sender.
|
||||
*
|
||||
* A value of 0 will 'auto-configure' this setting.
|
||||
*/
|
||||
var receiveBufferSize = 0
|
||||
}
|
|
@ -316,7 +316,7 @@ public final class NetUtil {
|
|||
* @param sysctlKey The key which the return value corresponds to.
|
||||
* @return The <a href ="https://www.freebsd.org/cgi/man.cgi?sysctl(8)">sysctl</a> value for {@code sysctlKey}.
|
||||
*/
|
||||
private static Integer sysctlGetInt(String sysctlKey) throws IOException {
|
||||
public static Integer sysctlGetInt(String sysctlKey) throws IOException {
|
||||
Process process = new ProcessBuilder("sysctl", sysctlKey).start();
|
||||
try {
|
||||
InputStream is = process.getInputStream();
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,319 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Clock;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.agrona.DirectBuffer;
|
||||
|
||||
import dorkbox.network.aeron.EchoAddresses;
|
||||
import dorkbox.network.aeron.EchoChannels;
|
||||
import dorkbox.network.aeron.EchoMessages;
|
||||
import dorkbox.network.aeron.EchoServerExecutor;
|
||||
import dorkbox.network.aeron.EchoServerExecutorService;
|
||||
import dorkbox.network.connection.Connection;
|
||||
import dorkbox.network.connection.EndPointServer;
|
||||
import dorkbox.network.connection.connectionType.ConnectionRule;
|
||||
import dorkbox.network.ipFilter.IpFilterRule;
|
||||
import dorkbox.util.exceptions.SecurityException;
|
||||
import io.aeron.FragmentAssembler;
|
||||
import io.aeron.Image;
|
||||
import io.aeron.Publication;
|
||||
import io.aeron.Subscription;
|
||||
import io.aeron.logbuffer.FragmentHandler;
|
||||
import io.aeron.logbuffer.Header;
|
||||
|
||||
/**
|
||||
* The server can only be accessed in an ASYNC manner. This means that the server can only be used in RESPONSE to events. If you access the
|
||||
* server OUTSIDE of events, you will get inaccurate information from the server (such as getConnections())
|
||||
* <p/>
|
||||
* To put it bluntly, ONLY have the server do work inside of a listener!
|
||||
*/
|
||||
public
|
||||
class Server<C extends Connection> extends EndPointServer {
|
||||
|
||||
/**
|
||||
* Gets the version number.
|
||||
*/
|
||||
public static
|
||||
String getVersion() {
|
||||
return "4.1";
|
||||
}
|
||||
|
||||
private volatile boolean isRunning = false;
|
||||
|
||||
private final EchoServerExecutorService executor;
|
||||
private final ClientStates clients;
|
||||
|
||||
/**
|
||||
* Starts a LOCAL <b>only</b> server, with the default serialization scheme.
|
||||
*/
|
||||
public
|
||||
Server() throws SecurityException, IOException {
|
||||
this(new ServerConfiguration());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to starts a server with the specified Connection Options
|
||||
*/
|
||||
@SuppressWarnings("AutoBoxing")
|
||||
public
|
||||
Server(ServerConfiguration config) throws SecurityException, IOException {
|
||||
// watch-out for serialization... it can be NULL incoming. The EndPoint (superclass) sets it, if null, so
|
||||
// you have to make sure to use this.serialization
|
||||
super(config);
|
||||
|
||||
EchoServerExecutorService exec = null;
|
||||
try {
|
||||
this.executor = EchoServerExecutor.create(Server.class);
|
||||
|
||||
try {
|
||||
this.clients = new ClientStates(this.aeron, Clock.systemUTC(), this.executor, this.config, logger);
|
||||
} catch (final Exception e) {
|
||||
if (mediaDriver != null) {
|
||||
mediaDriver.close();
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
try {
|
||||
if (exec != null) {
|
||||
exec.close();
|
||||
}
|
||||
} catch (final Exception c_ex) {
|
||||
e.addSuppressed(c_ex);
|
||||
}
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the server to the configured, underlying protocols.
|
||||
* <p/>
|
||||
* This method will also BLOCK until the stop method is called, and if you want to continue running code after this method invocation,
|
||||
* bind should be called in a separate, non-daemon thread.
|
||||
*/
|
||||
public
|
||||
void bind() {
|
||||
bind(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the server to the configured, underlying protocols.
|
||||
* <p/>
|
||||
* This is a more advanced method, and you should consider calling <code>bind()</code> instead.
|
||||
*
|
||||
* @param blockUntilTerminate
|
||||
* will BLOCK until the server stop method is called, and if you want to continue running code after this method
|
||||
* invocation, bind should be called in a separate, non-daemon thread - or with false as the parameter.
|
||||
*/
|
||||
@SuppressWarnings("AutoBoxing")
|
||||
public
|
||||
void bind(boolean blockUntilTerminate) {
|
||||
if (isRunning) {
|
||||
logger.error("Unable to bind when the server is already running!");
|
||||
return;
|
||||
}
|
||||
|
||||
isRunning = true;
|
||||
|
||||
Publication publication = null;
|
||||
Subscription subscription = null;
|
||||
FragmentHandler handler = null;
|
||||
try {
|
||||
publication = EchoChannels.createPublicationDynamicMDC(this.aeron,
|
||||
this.config.listenIpAddress,
|
||||
this.config.controlPort,
|
||||
UDP_STREAM_ID);
|
||||
|
||||
subscription = EchoChannels.createSubscriptionWithHandlers(this.aeron,
|
||||
this.config.listenIpAddress,
|
||||
this.config.port,
|
||||
UDP_STREAM_ID,
|
||||
this::onInitialClientConnected,
|
||||
this::onInitialClientDisconnected);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Note: Reassembly has been shown to be minimal impact to latency. But not totally negligible. If the lowest latency is desired, then limiting message sizes to MTU size is a good practice.
|
||||
*
|
||||
* Note: There is a maximum length allowed for messages which is the min of 1/8th a term length or 16MB. Messages larger than this should chunked using an application level chunking protocol. Chunking has better recovery properties from failure and streams with mechanical sympathy.
|
||||
*/
|
||||
final Publication finalPublication = publication;
|
||||
handler = new FragmentAssembler((buffer, offset, length, header)->this.onInitialClientMessage(finalPublication,
|
||||
buffer,
|
||||
offset,
|
||||
length,
|
||||
header));
|
||||
|
||||
final FragmentHandler initialConnectionHandler = handler;
|
||||
final Subscription initialConnectionSubscription = subscription;
|
||||
|
||||
while (true) {
|
||||
this.executor.execute(()->{
|
||||
initialConnectionSubscription.poll(initialConnectionHandler, 100); // this checks to see if there are NEW clients
|
||||
this.clients.poll(); // this manages existing clients
|
||||
});
|
||||
|
||||
try {
|
||||
Thread.sleep(100L);
|
||||
} catch (final InterruptedException e) {
|
||||
Thread.currentThread()
|
||||
.interrupt();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (publication != null) {
|
||||
publication.close();
|
||||
}
|
||||
|
||||
if (subscription != null) {
|
||||
subscription.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// we now BLOCK until the stop method is called.
|
||||
// if we want to continue running code in the server, bind should be called in a separate, non-daemon thread.
|
||||
// if (blockUntilTerminate) {
|
||||
// waitForShutdown();
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an IP+subnet rule that defines if that IP+subnet is allowed/denied connectivity to this server.
|
||||
* <p>
|
||||
* If there are any IP+subnet added to this list - then ONLY those are permitted (all else are denied)
|
||||
* <p>
|
||||
* If there is nothing added to this list - then ALL are permitted
|
||||
*/
|
||||
public
|
||||
void addIpFilterRule(IpFilterRule... rules) {
|
||||
ipFilterRules.addAll(Arrays.asList(rules));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an IP+subnet rule that defines what type of connection this IP+subnet should have.
|
||||
* - NOTHING : Nothing happens to the in/out bytes
|
||||
* - COMPRESS: The in/out bytes are compressed with LZ4-fast
|
||||
* - COMPRESS_AND_ENCRYPT: The in/out bytes are compressed (LZ4-fast) THEN encrypted (AES-256-GCM)
|
||||
*
|
||||
* If no rules are defined, then for LOOPBACK, it will always be `COMPRESS` and for everything else it will always be `COMPRESS_AND_ENCRYPT`.
|
||||
*
|
||||
* If rules are defined, then everything by default is `COMPRESS_AND_ENCRYPT`.
|
||||
*
|
||||
* The compression algorithm is LZ4-fast, so there is a small performance impact for a very large gain
|
||||
* Compress : 6.210 micros/op; 629.0 MB/s (output: 55.4%)
|
||||
* Uncompress : 0.641 micros/op; 6097.9 MB/s
|
||||
*/
|
||||
public
|
||||
void addConnectionRules(ConnectionRule... rules) {
|
||||
connectionRules.addAll(Arrays.asList(rules));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if this server has successfully bound to an IP address and is running
|
||||
*/
|
||||
public
|
||||
boolean isRunning() {
|
||||
return isRunning;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if a server (using the specified configuration) is running. This will check across JVMs by checking the
|
||||
* network socket directly, and assumes that if the port is in use and answers, then the server is "running". This does not try to
|
||||
* authenticate or validate the connection.
|
||||
* <p>
|
||||
* This does not check local-channels (which are intra-JVM only). Uses `Broadcast` to check for UDP servers
|
||||
* </p>
|
||||
*
|
||||
* @return true if the configuration matches and can connect (but not verify) to the TCP control socket.
|
||||
*/
|
||||
public static
|
||||
boolean isRunning(Configuration config) {
|
||||
// create an IPC client to see if we can connect to the same machine. IF YES, then
|
||||
// String host = config.host;
|
||||
//
|
||||
// for us, we want a "*" host to connect to the "any" interface.
|
||||
// if (host == null) {
|
||||
// host = "0.0.0.0";
|
||||
// }
|
||||
|
||||
|
||||
// create a client and see if it can connect
|
||||
|
||||
if (config.port > 0) {
|
||||
// List<BroadcastResponse> broadcastResponses = null;
|
||||
// try {
|
||||
// broadcastResponses = Broadcast.discoverHosts0(null, config.controlPort1, 500, true);
|
||||
// return !broadcastResponses.isEmpty();
|
||||
// } catch (IOException ignored) {
|
||||
// }
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private
|
||||
void onInitialClientMessage(final Publication publication,
|
||||
final DirectBuffer buffer,
|
||||
final int offset,
|
||||
final int length,
|
||||
final Header header) {
|
||||
final String message = EchoMessages.parseMessageUTF8(buffer, offset, length);
|
||||
|
||||
final String session_name = Integer.toString(header.sessionId());
|
||||
final Integer session_boxed = Integer.valueOf(header.sessionId());
|
||||
|
||||
this.executor.execute(()->{
|
||||
try {
|
||||
this.clients.onInitialClientMessageProcess(publication, session_name, session_boxed, message);
|
||||
} catch (final Exception e) {
|
||||
logger.error("could not process client message: ", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private
|
||||
void onInitialClientConnected(final Image image) {
|
||||
this.executor.execute(()->{
|
||||
logger.debug("[{}] initial client connected ({})", Integer.toString(image.sessionId()), image.sourceIdentity());
|
||||
|
||||
this.clients.onInitialClientConnected(image.sessionId(), EchoAddresses.extractAddress(image.sourceIdentity()));
|
||||
});
|
||||
}
|
||||
|
||||
private
|
||||
void onInitialClientDisconnected(final Image image) {
|
||||
this.executor.execute(()->{
|
||||
logger.debug("[{}] initial client disconnected ({})", Integer.toString(image.sessionId()), image.sourceIdentity());
|
||||
|
||||
this.clients.onInitialClientDisconnected(image.sessionId());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void close() {
|
||||
super.close();
|
||||
isRunning = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,438 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network
|
||||
|
||||
import dorkbox.network.aeron.server.ServerException
|
||||
import dorkbox.network.connection.Connection
|
||||
import dorkbox.network.connection.ConnectionManagerServer
|
||||
import dorkbox.network.connection.EndPoint
|
||||
import dorkbox.network.connection.UdpMediaDriverConnection
|
||||
import dorkbox.network.connection.connectionType.ConnectionProperties
|
||||
import dorkbox.network.connection.connectionType.ConnectionRule
|
||||
import dorkbox.network.ipFilter.IpFilterRule
|
||||
import dorkbox.network.ipFilter.IpFilterRuleType
|
||||
import io.aeron.FragmentAssembler
|
||||
import io.aeron.logbuffer.FragmentHandler
|
||||
import io.aeron.logbuffer.Header
|
||||
import kotlinx.coroutines.launch
|
||||
import org.agrona.DirectBuffer
|
||||
import java.net.InetSocketAddress
|
||||
import java.util.*
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
/**
|
||||
* NOTE: when using "server.publish(A)", this will go to ALL CLIENTS! add this to aeron via "publication.addDestination" so aeron manages it
|
||||
*
|
||||
*
|
||||
* The server can only be accessed in an ASYNC manner. This means that the server can only be used in RESPONSE to events. If you access the
|
||||
* server OUTSIDE of events, you will get inaccurate information from the server (such as getConnections())
|
||||
*
|
||||
*
|
||||
* To put it bluntly, ONLY have the server do work inside of a listener!
|
||||
*/
|
||||
open class Server<C : Connection>(config: ServerConfiguration = ServerConfiguration()) : EndPoint<C>(config) {
|
||||
companion object {
|
||||
/**
|
||||
* Gets the version number.
|
||||
*/
|
||||
const val version = "4.1"
|
||||
|
||||
/**
|
||||
* Checks to see if a server (using the specified configuration) is running.
|
||||
*
|
||||
* @return true if the configuration matches and can connect (but not verify) to the TCP control socket.
|
||||
*/
|
||||
fun isRunning(configuration: ServerConfiguration): Boolean {
|
||||
val server = Server<Connection>(configuration)
|
||||
|
||||
val running = server.isRunning()
|
||||
server.close()
|
||||
|
||||
return running
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return true if this server has successfully bound to an IP address and is running
|
||||
*/
|
||||
@Volatile
|
||||
private var bindAlreadyCalled = false
|
||||
|
||||
|
||||
override val connectionManager = ConnectionManagerServer<C>(logger, config)
|
||||
|
||||
/**
|
||||
* Maintains a thread-safe collection of rules to allow/deny connectivity to this server.
|
||||
*/
|
||||
protected val ipFilterRules = CopyOnWriteArrayList<IpFilterRule>()
|
||||
|
||||
/**
|
||||
* Maintains a thread-safe collection of rules used to define the connection type with this server.
|
||||
*/
|
||||
protected val connectionRules = CopyOnWriteArrayList<ConnectionRule>()
|
||||
|
||||
init {
|
||||
// have to do some basic validation of our configuration
|
||||
config.listenIpAddress = config.listenIpAddress.toLowerCase()
|
||||
|
||||
// localhost/loopback IP might not always be 127.0.0.1 or ::1
|
||||
when (config.listenIpAddress) {
|
||||
"loopback", "localhost", "lo" -> config.listenIpAddress = NetUtil.LOCALHOST.hostAddress
|
||||
else -> when {
|
||||
config.listenIpAddress.startsWith("127.") -> config.listenIpAddress = NetUtil.LOCALHOST.hostAddress
|
||||
config.listenIpAddress.startsWith("::1") -> config.listenIpAddress = NetUtil.LOCALHOST6.hostAddress
|
||||
else -> config.listenIpAddress = "0.0.0.0" // we set this to "0.0.0.0" so that it is clear that we are trying to bind to that address.
|
||||
}
|
||||
}
|
||||
|
||||
// if we are IPv6, the IP must be in '[]'
|
||||
if (config.listenIpAddress.count { it == ':' } > 1 &&
|
||||
config.listenIpAddress.count { it == '[' } < 1 &&
|
||||
config.listenIpAddress.count { it == ']' } < 1) {
|
||||
|
||||
config.listenIpAddress = """[${config.listenIpAddress}]"""
|
||||
}
|
||||
|
||||
if (config.listenIpAddress == "0.0.0.0") {
|
||||
// fixup windows!
|
||||
config.listenIpAddress = NetworkUtil.WILDCARD_IPV4
|
||||
}
|
||||
|
||||
|
||||
if (config.publicationPort <= 0) { throw ServerException("configuration port must be > 0") }
|
||||
if (config.publicationPort >= 65535) { throw ServerException("configuration port must be < 65535") }
|
||||
|
||||
if (config.subscriptionPort <= 0) { throw ServerException("configuration controlPort must be > 0") }
|
||||
if (config.subscriptionPort >= 65535) { throw ServerException("configuration controlPort must be < 65535") }
|
||||
|
||||
if (config.networkMtuSize <= 0) { throw ServerException("configuration networkMtuSize must be > 0") }
|
||||
if (config.networkMtuSize >= 9 * 1024) { throw ServerException("configuration networkMtuSize must be < ${9 * 1024}") }
|
||||
|
||||
closables.add(connectionManager)
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the server to AERON configuration
|
||||
*
|
||||
* @param blockUntilTerminate if true, will BLOCK until the server [close] method is called, and if you want to continue running code
|
||||
* after this pass in false
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun bind(blockUntilTerminate: Boolean = true) {
|
||||
if (bindAlreadyCalled) {
|
||||
logger.error("Unable to bind when the server is already running!")
|
||||
return
|
||||
}
|
||||
|
||||
bindAlreadyCalled = true
|
||||
|
||||
config as ServerConfiguration
|
||||
|
||||
// setup the "HANDSHAKE" ports, for initial clients to connect.
|
||||
// The is how clients then get the new ports to connect to + other configuration options
|
||||
|
||||
val handshakeDriver = UdpMediaDriverConnection(
|
||||
config.listenIpAddress, config.subscriptionPort, config.publicationPort,
|
||||
UDP_HANDSHAKE_STREAM_ID, RESERVED_SESSION_ID_INVALID)
|
||||
|
||||
handshakeDriver.buildServer(aeron)
|
||||
|
||||
val handshakePublication = handshakeDriver.publication
|
||||
val handshakeSubscription = handshakeDriver.subscription
|
||||
|
||||
logger.debug(handshakeDriver.serverInfo())
|
||||
logger.debug("Server listening for incomming clients on ${handshakePublication.localSocketAddresses()}")
|
||||
|
||||
/**
|
||||
* Note:
|
||||
* Reassembly has been shown to be minimal impact to latency. But not totally negligible. If the lowest latency is
|
||||
* desired, then limiting message sizes to MTU size is a good practice.
|
||||
*
|
||||
* There is a maximum length allowed for messages which is the min of 1/8th a term length or 16MB.
|
||||
* Messages larger than this should chunked using an application level chunking protocol. Chunking has better recovery
|
||||
* properties from failure and streams with mechanical sympathy.
|
||||
*/
|
||||
val initialConnectionHandler = FragmentAssembler(FragmentHandler { buffer: DirectBuffer, offset: Int, length: Int, header: Header ->
|
||||
actionDispatch.launch {
|
||||
connectionManager.receiveHandshakeMessageServer(handshakePublication, buffer, offset, length, header, this@Server)
|
||||
}
|
||||
})
|
||||
|
||||
actionDispatch.launch {
|
||||
val pollIdleStrategy = config.pollIdleStrategy
|
||||
|
||||
try {
|
||||
while (!isShutdown()) {
|
||||
// this checks to see if there are NEW clients
|
||||
var pollCount = handshakeSubscription.poll(initialConnectionHandler, 100)
|
||||
|
||||
// this manages existing clients (for cleanup + connection polling)
|
||||
pollCount += connectionManager.poll()
|
||||
|
||||
// 0 means we idle. >0 means reset and don't idle (because there are likely more poll events)
|
||||
pollIdleStrategy.idle(pollCount)
|
||||
}
|
||||
} finally {
|
||||
handshakePublication.close()
|
||||
handshakeSubscription.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// we now BLOCK until the stop method is called.
|
||||
if (blockUntilTerminate) {
|
||||
waitForShutdown();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds an IP+subnet rule that defines if that IP+subnet is allowed/denied connectivity to this server.
|
||||
*
|
||||
*
|
||||
* If there are any IP+subnet added to this list - then ONLY those are permitted (all else are denied)
|
||||
*
|
||||
*
|
||||
* If there is nothing added to this list - then ALL are permitted
|
||||
*/
|
||||
fun addIpFilterRule(vararg rules: IpFilterRule?) {
|
||||
ipFilterRules.addAll(Arrays.asList(*rules))
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an IP+subnet rule that defines what type of connection this IP+subnet should have.
|
||||
* - NOTHING : Nothing happens to the in/out bytes
|
||||
* - COMPRESS: The in/out bytes are compressed with LZ4-fast
|
||||
* - COMPRESS_AND_ENCRYPT: The in/out bytes are compressed (LZ4-fast) THEN encrypted (AES-256-GCM)
|
||||
*
|
||||
* If no rules are defined, then for LOOPBACK, it will always be `COMPRESS` and for everything else it will always be `COMPRESS_AND_ENCRYPT`.
|
||||
*
|
||||
* If rules are defined, then everything by default is `COMPRESS_AND_ENCRYPT`.
|
||||
*
|
||||
* The compression algorithm is LZ4-fast, so there is a small performance impact for a very large gain
|
||||
* Compress : 6.210 micros/op; 629.0 MB/s (output: 55.4%)
|
||||
* Uncompress : 0.641 micros/op; 6097.9 MB/s
|
||||
*/
|
||||
fun addConnectionRules(vararg rules: ConnectionRule?) {
|
||||
connectionRules.addAll(Arrays.asList(*rules))
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// verify the class ID registration details.
|
||||
// the client will send their class registration data. VERIFY IT IS CORRECT!
|
||||
|
||||
// verify the class ID registration details.
|
||||
// the client will send their class registration data. VERIFY IT IS CORRECT!
|
||||
// var state: dorkbox.network.connection.RegistrationWrapper.STATE = registrationWrapper.verifyClassRegistration(metaChannel, registration)
|
||||
// if (state == RegistrationWrapper.STATE.ERROR)
|
||||
// {
|
||||
// // abort! There was an error
|
||||
// shutdown(channel, 0)
|
||||
// return
|
||||
// } else if (state == RegistrationWrapper.STATE.WAIT)
|
||||
// {
|
||||
// return
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Checks to seeOnce a server has connected to ANY client, it will always return true until server.close() is called
|
||||
*
|
||||
* @return true if we are connected, false otherwise.
|
||||
*/
|
||||
override fun isConnected(): Boolean {
|
||||
return connectionManager.connectionCount() > 0
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Safely sends objects to a destination
|
||||
*/
|
||||
suspend fun send(message: Any) {
|
||||
connectionManager.send(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* When called by a server, NORMALLY listeners are added at the GLOBAL level (meaning, I add one listener,
|
||||
* and ALL connections are notified of that listener.
|
||||
* <br></br>
|
||||
* It is POSSIBLE to add a server-connection 'local' listener (via connection.addListener), meaning that ONLY
|
||||
* that listener attached to the connection is notified on that event (ie, admin type listeners)
|
||||
*
|
||||
* @return a newly created listener manager for the connection
|
||||
*/
|
||||
// fun addListenerManager(connection: C): ConnectionManager<C> {
|
||||
// return connectionManager.addListenerManager(connection)
|
||||
// }
|
||||
|
||||
/**
|
||||
* When called by a server, NORMALLY listeners are added at the GLOBAL level (meaning, I add one listener,
|
||||
* and ALL connections are notified of that listener.
|
||||
* <br></br>
|
||||
* It is POSSIBLE to remove a server-connection 'local' listener (via connection.removeListener), meaning that ONLY
|
||||
* that listener attached to the connection is removed
|
||||
*
|
||||
*
|
||||
* This removes the listener manager for that specific connection
|
||||
*/
|
||||
// fun removeListenerManager(connection: C) {
|
||||
// connectionManager.removeListenerManager(connection)
|
||||
// }
|
||||
|
||||
/**
|
||||
* Adds a custom connection to the server.
|
||||
*
|
||||
* This should only be used in situations where there can be DIFFERENT types of connections (such as a 'web-based' connection) and
|
||||
* you want *this* server instance to manage listeners + message dispatch
|
||||
*
|
||||
* @param connection the connection to add
|
||||
*/
|
||||
fun add(connection: C) {
|
||||
connectionManager.addConnection(connection)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a custom connection to the server.
|
||||
*
|
||||
*
|
||||
* This should only be used in situations where there can be DIFFERENT types of connections (such as a 'web-based' connection) and
|
||||
* you want *this* server instance to manage listeners + message dispatch
|
||||
*
|
||||
* @param connection the connection to remove
|
||||
*/
|
||||
fun remove(connection: C) {
|
||||
connectionManager.removeConnection(connection)
|
||||
}
|
||||
|
||||
// if no rules, then always yes
|
||||
// if rules, then default no unless a rule says yes. ACCEPT rules take precedence over REJECT (so if you have both rules, ACCEPT will happen)
|
||||
fun acceptRemoteConnection(remoteAddress: InetSocketAddress): Boolean {
|
||||
val size = ipFilterRules.size
|
||||
if (size == 0) {
|
||||
return true
|
||||
}
|
||||
val address = remoteAddress.address
|
||||
|
||||
// it's possible for a remote address to match MORE than 1 rule.
|
||||
var isAllowed = false
|
||||
for (i in 0 until size) {
|
||||
val rule = ipFilterRules[i] ?: continue
|
||||
if (isAllowed) {
|
||||
break
|
||||
}
|
||||
if (rule.matches(remoteAddress)) {
|
||||
isAllowed = rule.ruleType() == IpFilterRuleType.ACCEPT
|
||||
}
|
||||
}
|
||||
logger.debug("Validating {} Connection allowed: {}", address, isAllowed)
|
||||
return isAllowed
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if a server (using the specified configuration) is running.
|
||||
*
|
||||
* @return true if the server is active and running
|
||||
*/
|
||||
fun isRunning(): Boolean {
|
||||
return mediaDriver.context().isDriverActive(10_000, logger::debug)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
super.close()
|
||||
bindAlreadyCalled = false
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Only called by the server!
|
||||
*
|
||||
* If we are loopback or the client is a specific IP/CIDR address, then we do things differently. The LOOPBACK address will never encrypt or compress the traffic.
|
||||
*/
|
||||
// after the handshake, what sort of connection do we want (NONE, COMPRESS, ENCRYPT+COMPRESS)
|
||||
fun getConnectionUpgradeType(remoteAddress: InetSocketAddress): Byte {
|
||||
val address = remoteAddress.address
|
||||
val size = connectionRules.size
|
||||
|
||||
// if it's unknown, then by default we encrypt the traffic
|
||||
var connectionType = ConnectionProperties.COMPRESS_AND_ENCRYPT
|
||||
if (size == 0 && address == NetUtil.LOCALHOST) {
|
||||
// if nothing is specified, then by default localhost is compression and everything else is encrypted
|
||||
connectionType = ConnectionProperties.COMPRESS
|
||||
}
|
||||
for (i in 0 until size) {
|
||||
val rule = connectionRules[i] ?: continue
|
||||
if (rule.matches(remoteAddress)) {
|
||||
connectionType = rule.ruleType()
|
||||
break
|
||||
}
|
||||
}
|
||||
logger.debug("Validating {} Permitted type is: {}", remoteAddress, connectionType)
|
||||
return connectionType.type
|
||||
}
|
||||
|
||||
enum class STATE {
|
||||
ERROR, WAIT, CONTINUE
|
||||
}
|
||||
|
||||
// fun verifyClassRegistration(metaChannel: MetaChannel, registration: Registration): STATE {
|
||||
// if (registration.upgradeType == UpgradeType.FRAGMENTED) {
|
||||
// val fragment = registration.payload!!
|
||||
//
|
||||
// // this means that the registrations are FRAGMENTED!
|
||||
// // max size of ALL fragments is xxx * 127
|
||||
// if (metaChannel.fragmentedRegistrationDetails == null) {
|
||||
// metaChannel.remainingFragments = fragment[1]
|
||||
// metaChannel.fragmentedRegistrationDetails = ByteArray(Serialization.CLASS_REGISTRATION_VALIDATION_FRAGMENT_SIZE * fragment[1])
|
||||
// }
|
||||
// System.arraycopy(fragment, 2, metaChannel.fragmentedRegistrationDetails, fragment[0] * Serialization.CLASS_REGISTRATION_VALIDATION_FRAGMENT_SIZE, fragment.size - 2)
|
||||
//
|
||||
// metaChannel.remainingFragments--
|
||||
//
|
||||
// if (fragment[0] + 1 == fragment[1].toInt()) {
|
||||
// // this is the last fragment in the in byte array (but NOT necessarily the last fragment to arrive)
|
||||
// val correctSize = Serialization.CLASS_REGISTRATION_VALIDATION_FRAGMENT_SIZE * (fragment[1] - 1) + (fragment.size - 2)
|
||||
// val correctlySized = ByteArray(correctSize)
|
||||
// System.arraycopy(metaChannel.fragmentedRegistrationDetails, 0, correctlySized, 0, correctSize)
|
||||
// metaChannel.fragmentedRegistrationDetails = correctlySized
|
||||
// }
|
||||
// if (metaChannel.remainingFragments.toInt() == 0) {
|
||||
// // there are no more fragments available
|
||||
// val details = metaChannel.fragmentedRegistrationDetails
|
||||
// metaChannel.fragmentedRegistrationDetails = null
|
||||
// if (!serialization.verifyKryoRegistration(details)) {
|
||||
// // error
|
||||
// return STATE.ERROR
|
||||
// }
|
||||
// } else {
|
||||
// // wait for more fragments
|
||||
// return STATE.WAIT
|
||||
// }
|
||||
// } else {
|
||||
// if (!serialization.verifyKryoRegistration(registration.payload!!)) {
|
||||
// return STATE.ERROR
|
||||
// }
|
||||
// }
|
||||
// return STATE.CONTINUE
|
||||
// }
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network;
|
||||
|
||||
public
|
||||
class ServerConfiguration extends Configuration {
|
||||
|
||||
/**
|
||||
* The address for the server to listen on. "*" will accept connections from all interfaces, otherwise specify
|
||||
* the hostname (or IP) to bind to.
|
||||
*/
|
||||
public String listenIpAddress = "*";
|
||||
|
||||
/**
|
||||
* The starting port for clients to use. The upper bound of this value is limited by the maximum number of clients allowed.
|
||||
*/
|
||||
public int clientStartPort;
|
||||
|
||||
/**
|
||||
* The maximum number of clients allowed for a server
|
||||
*/
|
||||
public int maxClientCount;
|
||||
|
||||
/**
|
||||
* The maximum number of client connection allowed per IP address
|
||||
*/
|
||||
public int maxConnectionsPerIpAddress;
|
||||
|
||||
public
|
||||
ServerConfiguration() {
|
||||
super();
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,338 @@
|
|||
/*
|
||||
* Copyright 2014-2020 Real Logic Limited.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.aeron
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.yield
|
||||
import org.agrona.hints.ThreadHints
|
||||
|
||||
abstract class BackoffIdleStrategyPrePad {
|
||||
var p000: Byte = 0
|
||||
var p001: Byte = 0
|
||||
var p002: Byte = 0
|
||||
var p003: Byte = 0
|
||||
var p004: Byte = 0
|
||||
var p005: Byte = 0
|
||||
var p006: Byte = 0
|
||||
var p007: Byte = 0
|
||||
var p008: Byte = 0
|
||||
var p009: Byte = 0
|
||||
var p010: Byte = 0
|
||||
var p011: Byte = 0
|
||||
var p012: Byte = 0
|
||||
var p013: Byte = 0
|
||||
var p014: Byte = 0
|
||||
var p015: Byte = 0
|
||||
var p016: Byte = 0
|
||||
var p017: Byte = 0
|
||||
var p018: Byte = 0
|
||||
var p019: Byte = 0
|
||||
var p020: Byte = 0
|
||||
var p021: Byte = 0
|
||||
var p022: Byte = 0
|
||||
var p023: Byte = 0
|
||||
var p024: Byte = 0
|
||||
var p025: Byte = 0
|
||||
var p026: Byte = 0
|
||||
var p027: Byte = 0
|
||||
var p028: Byte = 0
|
||||
var p029: Byte = 0
|
||||
var p030: Byte = 0
|
||||
var p031: Byte = 0
|
||||
var p032: Byte = 0
|
||||
var p033: Byte = 0
|
||||
var p034: Byte = 0
|
||||
var p035: Byte = 0
|
||||
var p036: Byte = 0
|
||||
var p037: Byte = 0
|
||||
var p038: Byte = 0
|
||||
var p039: Byte = 0
|
||||
var p040: Byte = 0
|
||||
var p041: Byte = 0
|
||||
var p042: Byte = 0
|
||||
var p043: Byte = 0
|
||||
var p044: Byte = 0
|
||||
var p045: Byte = 0
|
||||
var p046: Byte = 0
|
||||
var p047: Byte = 0
|
||||
var p048: Byte = 0
|
||||
var p049: Byte = 0
|
||||
var p050: Byte = 0
|
||||
var p051: Byte = 0
|
||||
var p052: Byte = 0
|
||||
var p053: Byte = 0
|
||||
var p054: Byte = 0
|
||||
var p055: Byte = 0
|
||||
var p056: Byte = 0
|
||||
var p057: Byte = 0
|
||||
var p058: Byte = 0
|
||||
var p059: Byte = 0
|
||||
var p060: Byte = 0
|
||||
var p061: Byte = 0
|
||||
var p062: Byte = 0
|
||||
var p063: Byte = 0
|
||||
}
|
||||
|
||||
abstract class BackoffIdleStrategyData(
|
||||
protected val maxSpins: Long, protected val maxYields: Long, protected val minParkPeriodMs: Long, protected val maxParkPeriodMs: Long) : BackoffIdleStrategyPrePad() {
|
||||
|
||||
protected var state = 0 // NOT_IDLE
|
||||
protected var spins: Long = 0
|
||||
protected var yields: Long = 0
|
||||
protected var parkPeriodMs: Long = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Idling strategy for threads when they have no work to do.
|
||||
* <p>
|
||||
* Spin for maxSpins, then
|
||||
* [Coroutine.yield] for maxYields, then
|
||||
* [Coroutine.delay] on an exponential backoff to maxParkPeriodMs
|
||||
*/
|
||||
@Suppress("unused")
|
||||
class CoroutineBackoffIdleStrategy : BackoffIdleStrategyData, CoroutineIdleStrategy {
|
||||
var p064: Byte = 0
|
||||
var p065: Byte = 0
|
||||
var p066: Byte = 0
|
||||
var p067: Byte = 0
|
||||
var p068: Byte = 0
|
||||
var p069: Byte = 0
|
||||
var p070: Byte = 0
|
||||
var p071: Byte = 0
|
||||
var p072: Byte = 0
|
||||
var p073: Byte = 0
|
||||
var p074: Byte = 0
|
||||
var p075: Byte = 0
|
||||
var p076: Byte = 0
|
||||
var p077: Byte = 0
|
||||
var p078: Byte = 0
|
||||
var p079: Byte = 0
|
||||
var p080: Byte = 0
|
||||
var p081: Byte = 0
|
||||
var p082: Byte = 0
|
||||
var p083: Byte = 0
|
||||
var p084: Byte = 0
|
||||
var p085: Byte = 0
|
||||
var p086: Byte = 0
|
||||
var p087: Byte = 0
|
||||
var p088: Byte = 0
|
||||
var p089: Byte = 0
|
||||
var p090: Byte = 0
|
||||
var p091: Byte = 0
|
||||
var p092: Byte = 0
|
||||
var p093: Byte = 0
|
||||
var p094: Byte = 0
|
||||
var p095: Byte = 0
|
||||
var p096: Byte = 0
|
||||
var p097: Byte = 0
|
||||
var p098: Byte = 0
|
||||
var p099: Byte = 0
|
||||
var p100: Byte = 0
|
||||
var p101: Byte = 0
|
||||
var p102: Byte = 0
|
||||
var p103: Byte = 0
|
||||
var p104: Byte = 0
|
||||
var p105: Byte = 0
|
||||
var p106: Byte = 0
|
||||
var p107: Byte = 0
|
||||
var p108: Byte = 0
|
||||
var p109: Byte = 0
|
||||
var p110: Byte = 0
|
||||
var p111: Byte = 0
|
||||
var p112: Byte = 0
|
||||
var p113: Byte = 0
|
||||
var p114: Byte = 0
|
||||
var p115: Byte = 0
|
||||
var p116: Byte = 0
|
||||
var p117: Byte = 0
|
||||
var p118: Byte = 0
|
||||
var p119: Byte = 0
|
||||
var p120: Byte = 0
|
||||
var p121: Byte = 0
|
||||
var p122: Byte = 0
|
||||
var p123: Byte = 0
|
||||
var p124: Byte = 0
|
||||
var p125: Byte = 0
|
||||
var p126: Byte = 0
|
||||
var p127: Byte = 0
|
||||
|
||||
companion object {
|
||||
private const val NOT_IDLE = 0
|
||||
private const val SPINNING = 1
|
||||
private const val YIELDING = 2
|
||||
private const val PARKING = 3
|
||||
|
||||
/**
|
||||
* Name to be returned from [.alias].
|
||||
*/
|
||||
const val ALIAS = "backoff"
|
||||
|
||||
/**
|
||||
* Default number of times the strategy will spin without work before going to next state.
|
||||
*/
|
||||
const val DEFAULT_MAX_SPINS = 10L
|
||||
|
||||
/**
|
||||
* Default number of times the strategy will yield without work before going to next state.
|
||||
*/
|
||||
const val DEFAULT_MAX_YIELDS = 5L
|
||||
|
||||
/**
|
||||
* Default interval the strategy will park the thread on entering the park state in milliseconds.
|
||||
*/
|
||||
const val DEFAULT_MIN_PARK_PERIOD_MS = 1L
|
||||
|
||||
/**
|
||||
* Default interval the strategy will park the thread will expand interval to as a max in milliseconds.
|
||||
*/
|
||||
const val DEFAULT_MAX_PARK_PERIOD_MS = 1000L
|
||||
}
|
||||
|
||||
/**
|
||||
* Default constructor using [.DEFAULT_MAX_SPINS], [.DEFAULT_MAX_YIELDS], [.DEFAULT_MIN_PARK_PERIOD_MS], and [.DEFAULT_MAX_PARK_PERIOD_MS].
|
||||
*/
|
||||
constructor() : super(DEFAULT_MAX_SPINS, DEFAULT_MAX_YIELDS, DEFAULT_MIN_PARK_PERIOD_MS, DEFAULT_MAX_PARK_PERIOD_MS) {}
|
||||
|
||||
/**
|
||||
* Create a set of state tracking idle behavior
|
||||
* <p>
|
||||
* @param maxSpins to perform before moving to [Coroutine.yield]
|
||||
* @param maxYields to perform before moving to [Coroutine.delay]
|
||||
* @param minParkPeriodMs to use when initiating parking
|
||||
* @param maxParkPeriodMs to use for end duration when parking
|
||||
*/
|
||||
constructor(
|
||||
maxSpins: Long, maxYields: Long, minParkPeriodMs: Long, maxParkPeriodMs: Long)
|
||||
: super(maxSpins, maxYields, minParkPeriodMs, maxParkPeriodMs) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform current idle action (e.g. nothing/yield/sleep). This method signature expects users to call into it on
|
||||
* every work 'cycle'. The implementations may use the indication "workCount > 0" to reset internal backoff
|
||||
* state. This method works well with 'work' APIs which follow the following rules:
|
||||
* <ul>
|
||||
* <li>'work' returns a value larger than 0 when some work has been done</li>
|
||||
* <li>'work' returns 0 when no work has been done</li>
|
||||
* <li>'work' may return error codes which are less than 0, but which amount to no work has been done</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Callers are expected to follow this pattern:
|
||||
*
|
||||
* <pre>
|
||||
* <code>
|
||||
* while (isRunning)
|
||||
* {
|
||||
* idleStrategy.idle(doWork());
|
||||
* }
|
||||
* </code>
|
||||
* </pre>
|
||||
*
|
||||
* @param workCount performed in last duty cycle.
|
||||
*/
|
||||
override suspend fun idle(workCount: Int) {
|
||||
if (workCount > 0) {
|
||||
reset()
|
||||
} else {
|
||||
idle()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform current idle action (e.g. nothing/yield/sleep). To be used in conjunction with
|
||||
* {@link IdleStrategy#reset()} to clear internal state when idle period is over (or before it begins).
|
||||
* Callers are expected to follow this pattern:
|
||||
*
|
||||
* <pre>
|
||||
* <code>
|
||||
* while (isRunning)
|
||||
* {
|
||||
* if (!hasWork())
|
||||
* {
|
||||
* idleStrategy.reset();
|
||||
* while (!hasWork())
|
||||
* {
|
||||
* if (!isRunning)
|
||||
* {
|
||||
* return;
|
||||
* }
|
||||
* idleStrategy.idle();
|
||||
* }
|
||||
* }
|
||||
* doWork();
|
||||
* }
|
||||
* </code>
|
||||
* </pre>
|
||||
*/
|
||||
override suspend fun idle() {
|
||||
when (state) {
|
||||
NOT_IDLE -> {
|
||||
state = SPINNING
|
||||
spins++
|
||||
}
|
||||
|
||||
SPINNING -> {
|
||||
ThreadHints.onSpinWait()
|
||||
if (++spins > maxSpins) {
|
||||
state = YIELDING
|
||||
yields = 0
|
||||
}
|
||||
}
|
||||
|
||||
YIELDING -> if (++yields > maxYields) {
|
||||
state = PARKING
|
||||
parkPeriodMs = minParkPeriodMs
|
||||
} else {
|
||||
yield()
|
||||
}
|
||||
|
||||
PARKING -> {
|
||||
delay(parkPeriodMs)
|
||||
// double the delay until we get to MAX
|
||||
parkPeriodMs = (parkPeriodMs shl 1).coerceAtMost(maxParkPeriodMs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reset the internal state in preparation for entering an idle state again.
|
||||
*/
|
||||
override fun reset() {
|
||||
spins = 0
|
||||
yields = 0
|
||||
parkPeriodMs = minParkPeriodMs
|
||||
state = NOT_IDLE
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple name by which the strategy can be identified.
|
||||
*
|
||||
* @return simple name by which the strategy can be identified.
|
||||
*/
|
||||
override fun alias(): String {
|
||||
return ALIAS
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "BackoffIdleStrategy{" +
|
||||
"alias=" + ALIAS +
|
||||
", maxSpins=" + maxSpins +
|
||||
", maxYields=" + maxYields +
|
||||
", minParkPeriodMs=" + minParkPeriodMs +
|
||||
", maxParkPeriodMs=" + maxParkPeriodMs +
|
||||
'}'
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* Copyright 2014-2020 Real Logic Limited.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.aeron
|
||||
|
||||
/**
|
||||
* Idle strategy for use by threads when they do not have work to do.
|
||||
*
|
||||
*
|
||||
* **Note regarding implementor state**
|
||||
*
|
||||
*
|
||||
* Some implementations are known to be stateful, please note that you cannot safely assume implementations to be
|
||||
* stateless. Where implementations are stateful it is recommended that implementation state is padded to avoid false
|
||||
* sharing.
|
||||
*
|
||||
*
|
||||
* **Note regarding potential for TTSP(Time To Safe Point) issues**
|
||||
*
|
||||
*
|
||||
* If the caller spins in a 'counted' loop, and the implementation does not include a a safepoint poll this may cause a
|
||||
* TTSP (Time To SafePoint) problem. If this is the case for your application you can solve it by preventing the idle
|
||||
* method from being inlined by using a Hotspot compiler command as a JVM argument e.g:
|
||||
* `-XX:CompileCommand=dontinline,org.agrona.concurrent.NoOpIdleStrategy::idle`
|
||||
*/
|
||||
interface CoroutineIdleStrategy {
|
||||
/**
|
||||
* Perform current idle action (e.g. nothing/yield/sleep). This method signature expects users to call into it on
|
||||
* every work 'cycle'. The implementations may use the indication "workCount > 0" to reset internal backoff
|
||||
* state. This method works well with 'work' APIs which follow the following rules:
|
||||
* <ul>
|
||||
* <li>'work' returns a value larger than 0 when some work has been done</li>
|
||||
* <li>'work' returns 0 when no work has been done</li>
|
||||
* <li>'work' may return error codes which are less than 0, but which amount to no work has been done</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Callers are expected to follow this pattern:
|
||||
*
|
||||
* <pre>
|
||||
* <code>
|
||||
* while (isRunning)
|
||||
* {
|
||||
* idleStrategy.idle(doWork());
|
||||
* }
|
||||
* </code>
|
||||
* </pre>
|
||||
*
|
||||
* @param workCount performed in last duty cycle.
|
||||
*/
|
||||
suspend fun idle(workCount: Int)
|
||||
|
||||
/**
|
||||
* Perform current idle action (e.g. nothing/yield/sleep). To be used in conjunction with
|
||||
* {@link IdleStrategy#reset()} to clear internal state when idle period is over (or before it begins).
|
||||
* Callers are expected to follow this pattern:
|
||||
*
|
||||
* <pre>
|
||||
* <code>
|
||||
* while (isRunning)
|
||||
* {
|
||||
* if (!hasWork())
|
||||
* {
|
||||
* idleStrategy.reset();
|
||||
* while (!hasWork())
|
||||
* {
|
||||
* if (!isRunning)
|
||||
* {
|
||||
* return;
|
||||
* }
|
||||
* idleStrategy.idle();
|
||||
* }
|
||||
* }
|
||||
* doWork();
|
||||
* }
|
||||
* </code>
|
||||
* </pre>
|
||||
*/
|
||||
suspend fun idle()
|
||||
|
||||
/**
|
||||
* Reset the internal state in preparation for entering an idle state again.
|
||||
*/
|
||||
fun reset()
|
||||
|
||||
/**
|
||||
* Simple name by which the strategy can be identified.
|
||||
*
|
||||
* @return simple name by which the strategy can be identified.
|
||||
*/
|
||||
fun alias(): String {
|
||||
return ""
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* Copyright 2014-2020 Real Logic Limited.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.aeron
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
/**
|
||||
* When idle this strategy is to sleep for a specified period time in milliseconds.
|
||||
*
|
||||
*
|
||||
* This class uses [Coroutine.delay] to idle.
|
||||
*/
|
||||
class CoroutineSleepingMillisIdleStrategy : CoroutineIdleStrategy {
|
||||
companion object {
|
||||
/**
|
||||
* Name to be returned from [.alias].
|
||||
*/
|
||||
const val ALIAS = "sleep-ms"
|
||||
|
||||
/**
|
||||
* Default sleep period when the default constructor is used.
|
||||
*/
|
||||
const val DEFAULT_SLEEP_PERIOD_MS = 1L
|
||||
}
|
||||
|
||||
private val sleepPeriodMs: Long
|
||||
|
||||
/**
|
||||
* Default constructor that uses [.DEFAULT_SLEEP_PERIOD_MS].
|
||||
*/
|
||||
constructor() {
|
||||
sleepPeriodMs = DEFAULT_SLEEP_PERIOD_MS
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructed a new strategy that will sleep for a given period when idle.
|
||||
*
|
||||
* @param sleepPeriodMs period in milliseconds for which the strategy will sleep when work count is 0.
|
||||
*/
|
||||
constructor(sleepPeriodMs: Long) {
|
||||
this.sleepPeriodMs = sleepPeriodMs
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
override suspend fun idle(workCount: Int) {
|
||||
if (workCount > 0) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
delay(sleepPeriodMs)
|
||||
} catch (ignore: InterruptedException) {
|
||||
Thread.currentThread().interrupt()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
override suspend fun idle() {
|
||||
try {
|
||||
delay(sleepPeriodMs)
|
||||
} catch (ignore: InterruptedException) {
|
||||
Thread.currentThread().interrupt()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
override fun reset() {}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
override fun alias(): String {
|
||||
return ALIAS
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "SleepingMillisIdleStrategy{" +
|
||||
"alias=" + ALIAS +
|
||||
", sleepPeriodMs=" + sleepPeriodMs +
|
||||
'}'
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
package dorkbox.network.aeron;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
/**
|
||||
* Functions to parse addresses.
|
||||
*/
|
||||
|
||||
public final
|
||||
class EchoAddresses {
|
||||
private
|
||||
EchoAddresses() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract an IP address from the given string of the form "ip:port", where
|
||||
* {@code ip} may be an IPv4 or IPv6 address, and {@code port} is an unsigned
|
||||
* integer port value.
|
||||
*
|
||||
* @param text The text
|
||||
*
|
||||
* @return An IP address
|
||||
*
|
||||
* @throws IllegalArgumentException If the input is unparseable
|
||||
*/
|
||||
|
||||
public static
|
||||
InetAddress extractAddress(final String text) throws IllegalArgumentException {
|
||||
try {
|
||||
final URI uri = new URI("fake://" + text);
|
||||
return InetAddress.getByName(uri.getHost());
|
||||
} catch (final URISyntaxException | UnknownHostException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,295 +0,0 @@
|
|||
package dorkbox.network.aeron;
|
||||
|
||||
import static java.lang.Boolean.TRUE;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import io.aeron.Aeron;
|
||||
import io.aeron.AvailableImageHandler;
|
||||
import io.aeron.ChannelUriStringBuilder;
|
||||
import io.aeron.ConcurrentPublication;
|
||||
import io.aeron.Subscription;
|
||||
import io.aeron.UnavailableImageHandler;
|
||||
|
||||
/**
|
||||
* Convenience functions to construct publications and subscriptions.
|
||||
*/
|
||||
public final
|
||||
class EchoChannels {
|
||||
private
|
||||
EchoChannels() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a publication at the given address and port, using the given
|
||||
* stream ID.
|
||||
*
|
||||
* @param aeron The Aeron instance
|
||||
* @param address The address
|
||||
* @param port The port
|
||||
* @param stream_id The stream ID
|
||||
*
|
||||
* @return A new publication
|
||||
*/
|
||||
|
||||
public static
|
||||
ConcurrentPublication createPublication(final Aeron aeron, final String address, final int port, final int stream_id) {
|
||||
Objects.requireNonNull(aeron, "aeron");
|
||||
Objects.requireNonNull(address, "address");
|
||||
|
||||
final String pub_uri = new ChannelUriStringBuilder().reliable(TRUE)
|
||||
.media("udp")
|
||||
.endpoint(new StringBuilder(64).append(address)
|
||||
.append(":")
|
||||
.append(Integer.toUnsignedString(port))
|
||||
.toString())
|
||||
.build();
|
||||
|
||||
// Note: The Aeron.addPublication method will block until the Media Driver acknowledges the request or a timeout occurs.
|
||||
return aeron.addPublication(pub_uri, stream_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a subscription with a control port (for dynamic MDC) at the given
|
||||
* address and port, using the given stream ID.
|
||||
*
|
||||
* @param aeron The Aeron instance
|
||||
* @param address The address
|
||||
* @param port The port
|
||||
* @param stream_id The stream ID
|
||||
*
|
||||
* @return A new publication
|
||||
*/
|
||||
public static
|
||||
Subscription createSubscriptionDynamicMDC(final Aeron aeron, final String address, final int port, final int stream_id) {
|
||||
Objects.requireNonNull(aeron, "aeron");
|
||||
Objects.requireNonNull(address, "address");
|
||||
|
||||
final String sub_uri = new ChannelUriStringBuilder().reliable(TRUE)
|
||||
.media("udp")
|
||||
.controlEndpoint(new StringBuilder(64).append(address)
|
||||
.append(":")
|
||||
.append(Integer.toUnsignedString(port))
|
||||
.toString())
|
||||
.controlMode("dynamic")
|
||||
.build();
|
||||
|
||||
return aeron.addSubscription(sub_uri, stream_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a publication with a control port (for dynamic MDC) at the given
|
||||
* address and port, using the given stream ID.
|
||||
*
|
||||
* @param aeron The Aeron instance
|
||||
* @param address The address
|
||||
* @param port The port
|
||||
* @param stream_id The stream ID
|
||||
*
|
||||
* @return A new publication
|
||||
*/
|
||||
|
||||
public static
|
||||
ConcurrentPublication createPublicationDynamicMDC(final Aeron aeron, final String address, final int port, final int stream_id) {
|
||||
Objects.requireNonNull(aeron, "aeron");
|
||||
Objects.requireNonNull(address, "address");
|
||||
|
||||
final String pub_uri = new ChannelUriStringBuilder().reliable(TRUE)
|
||||
.media("udp")
|
||||
.controlEndpoint(new StringBuilder(32).append(address)
|
||||
.append(":")
|
||||
.append(Integer.toUnsignedString(port))
|
||||
.toString())
|
||||
.controlMode("dynamic")
|
||||
.build();
|
||||
|
||||
return aeron.addPublication(pub_uri, stream_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a subscription at the given address and port, using the given
|
||||
* stream ID and image handlers.
|
||||
*
|
||||
* @param aeron The Aeron instance
|
||||
* @param address The address
|
||||
* @param port The port
|
||||
* @param stream_id The stream ID
|
||||
* @param on_image_available Called when an image becomes available
|
||||
* @param on_image_unavailable Called when an image becomes unavailable
|
||||
*
|
||||
* @return A new publication
|
||||
*/
|
||||
|
||||
public static
|
||||
Subscription createSubscriptionWithHandlers(final Aeron aeron,
|
||||
final String address,
|
||||
final int port,
|
||||
final int stream_id,
|
||||
final AvailableImageHandler on_image_available,
|
||||
final UnavailableImageHandler on_image_unavailable) {
|
||||
Objects.requireNonNull(aeron, "aeron");
|
||||
Objects.requireNonNull(address, "address");
|
||||
Objects.requireNonNull(on_image_available, "on_image_available");
|
||||
Objects.requireNonNull(on_image_unavailable, "on_image_unavailable");
|
||||
|
||||
final String sub_uri = new ChannelUriStringBuilder().reliable(TRUE)
|
||||
.media("udp")
|
||||
.endpoint(new StringBuilder(32).append(address)
|
||||
.append(":")
|
||||
.append(Integer.toUnsignedString(port))
|
||||
.toString())
|
||||
.build();
|
||||
|
||||
return aeron.addSubscription(sub_uri, stream_id, on_image_available, on_image_unavailable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a publication with a control port (for dynamic MDC) at the given
|
||||
* address and port, using the given stream ID and session ID.
|
||||
*
|
||||
* @param aeron The Aeron instance
|
||||
* @param address The address
|
||||
* @param port The port
|
||||
* @param stream_id The stream ID
|
||||
* @param session The session ID
|
||||
*
|
||||
* @return A new publication
|
||||
*/
|
||||
|
||||
public static
|
||||
ConcurrentPublication createPublicationDynamicMDCWithSession(final Aeron aeron,
|
||||
final String address,
|
||||
final int port,
|
||||
final int stream_id,
|
||||
final int session) {
|
||||
Objects.requireNonNull(aeron, "aeron");
|
||||
Objects.requireNonNull(address, "address");
|
||||
|
||||
final String pub_uri = new ChannelUriStringBuilder().reliable(TRUE)
|
||||
.media("udp")
|
||||
.controlEndpoint(new StringBuilder(32).append(address)
|
||||
.append(":")
|
||||
.append(Integer.toUnsignedString(port))
|
||||
.toString())
|
||||
.controlMode("dynamic")
|
||||
.sessionId(Integer.valueOf(session))
|
||||
.build();
|
||||
|
||||
// Note: The Aeron.addPublication method will block until the Media Driver acknowledges the request or a timeout occurs.
|
||||
return aeron.addPublication(pub_uri, stream_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a subscription at the given address and port, using the given
|
||||
* stream ID, session ID, and image handlers.
|
||||
*
|
||||
* @param aeron The Aeron instance
|
||||
* @param address The address
|
||||
* @param port The port
|
||||
* @param stream_id The stream ID
|
||||
* @param on_image_available Called when an image becomes available
|
||||
* @param on_image_unavailable Called when an image becomes unavailable
|
||||
* @param session The session ID
|
||||
*
|
||||
* @return A new publication
|
||||
*/
|
||||
|
||||
public static
|
||||
Subscription createSubscriptionWithHandlersAndSession(final Aeron aeron,
|
||||
final String address,
|
||||
final int port,
|
||||
final int stream_id,
|
||||
final AvailableImageHandler on_image_available,
|
||||
final UnavailableImageHandler on_image_unavailable,
|
||||
final int session) {
|
||||
Objects.requireNonNull(aeron, "aeron");
|
||||
Objects.requireNonNull(address, "address");
|
||||
Objects.requireNonNull(on_image_available, "on_image_available");
|
||||
Objects.requireNonNull(on_image_unavailable, "on_image_unavailable");
|
||||
|
||||
final String sub_uri = new ChannelUriStringBuilder().reliable(TRUE)
|
||||
.media("udp")
|
||||
.endpoint(new StringBuilder(32).append(address)
|
||||
.append(":")
|
||||
.append(Integer.toUnsignedString(port))
|
||||
.toString())
|
||||
.sessionId(Integer.valueOf(session))
|
||||
.build();
|
||||
|
||||
return aeron.addSubscription(sub_uri, stream_id, on_image_available, on_image_unavailable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a subscription with a control port (for dynamic MDC) at the given
|
||||
* address and port, using the given stream ID, and session ID.
|
||||
*
|
||||
* @param aeron The Aeron instance
|
||||
* @param address The address
|
||||
* @param port The port
|
||||
* @param stream_id The stream ID
|
||||
* @param session The session ID
|
||||
*
|
||||
* @return A new publication
|
||||
*/
|
||||
|
||||
public static
|
||||
Subscription createSubscriptionDynamicMDCWithSession(final Aeron aeron,
|
||||
final String address,
|
||||
final int port,
|
||||
final int session,
|
||||
final int stream_id) {
|
||||
Objects.requireNonNull(aeron, "aeron");
|
||||
Objects.requireNonNull(address, "address");
|
||||
|
||||
final String sub_uri = new ChannelUriStringBuilder().reliable(TRUE)
|
||||
.media("udp")
|
||||
.controlEndpoint(new StringBuilder(64).append(address)
|
||||
.append(":")
|
||||
.append(Integer.toUnsignedString(port))
|
||||
.toString())
|
||||
.controlMode("dynamic")
|
||||
.sessionId(Integer.valueOf(session))
|
||||
.build();
|
||||
|
||||
return aeron.addSubscription(sub_uri, stream_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a publication at the given address and port, using the given
|
||||
* stream ID and session ID.
|
||||
*
|
||||
* @param aeron The Aeron instance
|
||||
* @param address The address
|
||||
* @param port The port
|
||||
* @param stream_id The stream ID
|
||||
* @param session The session ID
|
||||
*
|
||||
* @return A new publication
|
||||
*/
|
||||
|
||||
public static
|
||||
ConcurrentPublication createPublicationWithSession(final Aeron aeron,
|
||||
final String address,
|
||||
final int port,
|
||||
final int session,
|
||||
final int stream_id) {
|
||||
Objects.requireNonNull(aeron, "aeron");
|
||||
Objects.requireNonNull(address, "address");
|
||||
|
||||
// final String addr_string = address.toString()
|
||||
// .replaceFirst("^/", "");
|
||||
|
||||
final String pub_uri = new ChannelUriStringBuilder().reliable(TRUE)
|
||||
.media("udp")
|
||||
.endpoint(new StringBuilder(64).append(address)
|
||||
.append(":")
|
||||
.append(Integer.toUnsignedString(port))
|
||||
.toString())
|
||||
.sessionId(Integer.valueOf(session))
|
||||
.build();
|
||||
|
||||
// Note: The Aeron.addPublication method will block until the Media Driver acknowledges the request or a timeout occurs.
|
||||
return aeron.addPublication(pub_uri, stream_id);
|
||||
}
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
package dorkbox.network.aeron;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.agrona.DirectBuffer;
|
||||
import org.agrona.concurrent.UnsafeBuffer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.aeron.Publication;
|
||||
|
||||
/**
|
||||
* Convenience functions to send messages.
|
||||
*/
|
||||
|
||||
public final
|
||||
class EchoMessages {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(EchoMessages.class);
|
||||
|
||||
private
|
||||
EchoMessages() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the given message to the given publication. If the publication fails
|
||||
* to accept the message, the method will retry {@code 5} times, waiting
|
||||
* {@code 100} milliseconds each time, before throwing an exception.
|
||||
*
|
||||
* @param pub The publication
|
||||
* @param buffer A buffer that will hold the message for sending
|
||||
* @param text The message
|
||||
*
|
||||
* @return The new publication stream position
|
||||
*
|
||||
* @throws IOException If the message cannot be sent
|
||||
*/
|
||||
|
||||
public static
|
||||
long sendMessage(final Publication pub, final UnsafeBuffer buffer, final String text) throws IOException {
|
||||
Objects.requireNonNull(pub, "publication");
|
||||
Objects.requireNonNull(buffer, "buffer");
|
||||
Objects.requireNonNull(text, "text");
|
||||
|
||||
LOG.trace("[{}] send: {}", Integer.toString(pub.sessionId()), text);
|
||||
|
||||
final byte[] value = text.getBytes(UTF_8);
|
||||
buffer.putBytes(0, value);
|
||||
|
||||
long result = 0L;
|
||||
for (int index = 0; index < 5; ++index) {
|
||||
result = pub.offer(buffer, 0, value.length);
|
||||
if (result < 0L) {
|
||||
try {
|
||||
Thread.sleep(100L);
|
||||
} catch (final InterruptedException e) {
|
||||
Thread.currentThread()
|
||||
.interrupt();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new IOException("Could not send message: Error code: " + errorCodeName(result));
|
||||
}
|
||||
|
||||
private static
|
||||
String errorCodeName(final long result) {
|
||||
if (result == Publication.NOT_CONNECTED) {
|
||||
return "Not connected";
|
||||
}
|
||||
if (result == Publication.ADMIN_ACTION) {
|
||||
return "Administrative action";
|
||||
}
|
||||
if (result == Publication.BACK_PRESSURED) {
|
||||
return "Back pressured";
|
||||
}
|
||||
if (result == Publication.CLOSED) {
|
||||
return "Publication is closed";
|
||||
}
|
||||
if (result == Publication.MAX_POSITION_EXCEEDED) {
|
||||
return "Maximum term position exceeded";
|
||||
}
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a UTF-8 encoded string from the given buffer.
|
||||
*
|
||||
* @param buffer The buffer
|
||||
* @param offset The offset from the start of the buffer
|
||||
* @param length The number of bytes to extract
|
||||
*
|
||||
* @return A string
|
||||
*/
|
||||
|
||||
public static
|
||||
String parseMessageUTF8(final DirectBuffer buffer, final int offset, final int length) {
|
||||
Objects.requireNonNull(buffer, "buffer");
|
||||
final byte[] data = new byte[length];
|
||||
buffer.getBytes(offset, data);
|
||||
return new String(data, UTF_8);
|
||||
}
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
package dorkbox.network.aeron;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A counter for IP addresses.
|
||||
*/
|
||||
|
||||
public final
|
||||
class EchoServerAddressCounter {
|
||||
private final Map<InetAddress, Integer> counts;
|
||||
|
||||
private
|
||||
EchoServerAddressCounter() {
|
||||
this.counts = new HashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new counter.
|
||||
*
|
||||
* @return A new counter
|
||||
*/
|
||||
|
||||
public static
|
||||
EchoServerAddressCounter create() {
|
||||
return new EchoServerAddressCounter();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param address The IP address
|
||||
*
|
||||
* @return The current count for the given address
|
||||
*/
|
||||
|
||||
public
|
||||
int countFor(final InetAddress address) {
|
||||
Objects.requireNonNull(address, "address");
|
||||
|
||||
if (this.counts.containsKey(address)) {
|
||||
return this.counts.get(address)
|
||||
.intValue();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the count for the given address.
|
||||
*
|
||||
* @param address The IP address
|
||||
*
|
||||
* @return The current count for the given address
|
||||
*/
|
||||
|
||||
public
|
||||
int increment(final InetAddress address) {
|
||||
Objects.requireNonNull(address, "address");
|
||||
|
||||
if (this.counts.containsKey(address)) {
|
||||
final int next = this.counts.get(address)
|
||||
.intValue() + 1;
|
||||
this.counts.put(address, Integer.valueOf(next));
|
||||
return next;
|
||||
}
|
||||
|
||||
this.counts.put(address, Integer.valueOf(1));
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrement the count for the given address.
|
||||
*
|
||||
* @param address The IP address
|
||||
*
|
||||
* @return The current count for the given address
|
||||
*/
|
||||
|
||||
public
|
||||
int decrement(final InetAddress address) {
|
||||
Objects.requireNonNull(address, "address");
|
||||
|
||||
if (this.counts.containsKey(address)) {
|
||||
final int next = this.counts.get(address)
|
||||
.intValue() - 1;
|
||||
if (next <= 0) {
|
||||
this.counts.remove(address);
|
||||
return 0;
|
||||
}
|
||||
|
||||
this.counts.put(address, Integer.valueOf(next));
|
||||
return next;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
|
@ -1,310 +0,0 @@
|
|||
package dorkbox.network.aeron;
|
||||
|
||||
import static dorkbox.network.connection.EndPoint.UDP_STREAM_ID;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.agrona.BufferUtil;
|
||||
import org.agrona.DirectBuffer;
|
||||
import org.agrona.concurrent.UnsafeBuffer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.aeron.Aeron;
|
||||
import io.aeron.ConcurrentPublication;
|
||||
import io.aeron.FragmentAssembler;
|
||||
import io.aeron.Image;
|
||||
import io.aeron.Publication;
|
||||
import io.aeron.Subscription;
|
||||
import io.aeron.logbuffer.Header;
|
||||
|
||||
/**
|
||||
* A conversation between the server and a single client.
|
||||
*/
|
||||
|
||||
public final
|
||||
class EchoServerDuologue implements AutoCloseable {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(EchoServerDuologue.class);
|
||||
|
||||
private static final Pattern PATTERN_ECHO = Pattern.compile("^ECHO (.*)$");
|
||||
|
||||
private final UnsafeBuffer send_buffer;
|
||||
private final EchoServerExecutorService exec;
|
||||
private final Instant initial_expire;
|
||||
private final InetAddress owner;
|
||||
private final int port_data;
|
||||
private final int port_control;
|
||||
private final int session;
|
||||
private final FragmentAssembler handler;
|
||||
|
||||
private volatile boolean closed;
|
||||
|
||||
private Publication publication;
|
||||
private Subscription subscription;
|
||||
|
||||
private
|
||||
EchoServerDuologue(final EchoServerExecutorService in_exec,
|
||||
final Instant in_initial_expire,
|
||||
final InetAddress in_owner_address,
|
||||
final int in_session,
|
||||
final int in_port_data,
|
||||
final int in_port_control) {
|
||||
this.exec = Objects.requireNonNull(in_exec, "executor");
|
||||
this.initial_expire = Objects.requireNonNull(in_initial_expire, "initial_expire");
|
||||
this.owner = Objects.requireNonNull(in_owner_address, "owner");
|
||||
|
||||
this.send_buffer = new UnsafeBuffer(BufferUtil.allocateDirectAligned(1024, 16));
|
||||
|
||||
this.session = in_session;
|
||||
this.port_data = in_port_data;
|
||||
this.port_control = in_port_control;
|
||||
this.closed = false;
|
||||
|
||||
this.handler = new FragmentAssembler((data, offset, length, header)->{
|
||||
try {
|
||||
this.onMessageReceived(data, offset, length, header);
|
||||
} catch (final IOException e) {
|
||||
LOG.error("failed to send message: ", e);
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new duologue. This will create a new publication and subscription
|
||||
* pair using a specific session ID and intended only for a single client
|
||||
* at a given address.
|
||||
*
|
||||
* @param aeron The Aeron instance
|
||||
* @param clock A clock used for time-related operations
|
||||
* @param exec An executor
|
||||
* @param local_address The local address of the server ports
|
||||
* @param owner_address The address of the client
|
||||
* @param session The session ID
|
||||
* @param port_data The data port
|
||||
* @param port_control The control port
|
||||
*
|
||||
* @return A new duologue
|
||||
*/
|
||||
|
||||
public static
|
||||
EchoServerDuologue create(final Aeron aeron,
|
||||
final Clock clock,
|
||||
final EchoServerExecutorService exec,
|
||||
final String local_address,
|
||||
final InetAddress owner_address,
|
||||
final int session,
|
||||
final int port_data,
|
||||
final int port_control) {
|
||||
Objects.requireNonNull(aeron, "aeron");
|
||||
Objects.requireNonNull(clock, "clock");
|
||||
Objects.requireNonNull(exec, "exec");
|
||||
Objects.requireNonNull(local_address, "local_address");
|
||||
Objects.requireNonNull(owner_address, "owner_address");
|
||||
|
||||
LOG.debug("creating new duologue at {} ({},{}) session {} for {}",
|
||||
local_address,
|
||||
Integer.valueOf(port_data),
|
||||
Integer.valueOf(port_control),
|
||||
Integer.toString(session),
|
||||
owner_address);
|
||||
|
||||
final Instant initial_expire = clock.instant()
|
||||
.plus(10L, ChronoUnit.SECONDS);
|
||||
|
||||
final ConcurrentPublication pub = EchoChannels.createPublicationDynamicMDCWithSession(aeron,
|
||||
local_address,
|
||||
port_control,
|
||||
UDP_STREAM_ID,
|
||||
session);
|
||||
|
||||
try {
|
||||
final EchoServerDuologue duologue = new EchoServerDuologue(exec,
|
||||
initial_expire,
|
||||
owner_address,
|
||||
session,
|
||||
port_data,
|
||||
port_control);
|
||||
|
||||
final Subscription sub = EchoChannels.createSubscriptionWithHandlersAndSession(aeron,
|
||||
local_address,
|
||||
port_data,
|
||||
UDP_STREAM_ID,
|
||||
duologue::onClientConnected,
|
||||
duologue::onClientDisconnected,
|
||||
session);
|
||||
|
||||
duologue.setPublicationSubscription(pub, sub);
|
||||
return duologue;
|
||||
} catch (final Exception e) {
|
||||
try {
|
||||
pub.close();
|
||||
} catch (final Exception pe) {
|
||||
e.addSuppressed(pe);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll the duologue for activity.
|
||||
*/
|
||||
public
|
||||
void poll() {
|
||||
this.exec.assertIsExecutorThread();
|
||||
this.subscription.poll(this.handler, 10);
|
||||
}
|
||||
|
||||
private
|
||||
void onMessageReceived(final DirectBuffer buffer, final int offset, final int length, final Header header) throws IOException {
|
||||
this.exec.assertIsExecutorThread();
|
||||
|
||||
final String session_name = Integer.toString(header.sessionId());
|
||||
final String message = EchoMessages.parseMessageUTF8(buffer, offset, length);
|
||||
|
||||
/*
|
||||
* Try to parse an ECHO message.
|
||||
*/
|
||||
|
||||
LOG.debug("[{}] received: {}", session_name, message);
|
||||
final Matcher echo_matcher = PATTERN_ECHO.matcher(message);
|
||||
if (echo_matcher.matches()) {
|
||||
EchoMessages.sendMessage(this.publication, this.send_buffer, "ECHO " + echo_matcher.group(1));
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* Otherwise, fail and close this duologue.
|
||||
*/
|
||||
try {
|
||||
EchoMessages.sendMessage(this.publication, this.send_buffer, "ERROR bad message");
|
||||
} finally {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
private
|
||||
void setPublicationSubscription(final Publication in_publication, final Subscription in_subscription) {
|
||||
this.publication = Objects.requireNonNull(in_publication, "Publication");
|
||||
this.subscription = Objects.requireNonNull(in_subscription, "Subscription");
|
||||
}
|
||||
|
||||
private
|
||||
void onClientDisconnected(final Image image) {
|
||||
this.exec.execute(()->{
|
||||
final int image_session = image.sessionId();
|
||||
final String session_name = Integer.toString(image_session);
|
||||
final InetAddress address = EchoAddresses.extractAddress(image.sourceIdentity());
|
||||
|
||||
if (this.subscription.imageCount() == 0) {
|
||||
LOG.debug("[{}] last client ({}) disconnected", session_name, address);
|
||||
this.close();
|
||||
}
|
||||
else {
|
||||
LOG.debug("[{}] client {} disconnected", session_name, address);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private
|
||||
void onClientConnected(final Image image) {
|
||||
this.exec.execute(()->{
|
||||
final InetAddress remote_address = EchoAddresses.extractAddress(image.sourceIdentity());
|
||||
|
||||
if (Objects.equals(remote_address, this.owner)) {
|
||||
LOG.debug("[{}] client with correct IP connected", Integer.toString(image.sessionId()));
|
||||
}
|
||||
else {
|
||||
LOG.error("connecting client has wrong address: {}", remote_address);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param now The current time
|
||||
*
|
||||
* @return {@code true} if this duologue has no subscribers and the current
|
||||
* time {@code now} is after the intended expiry date of the duologue
|
||||
*/
|
||||
|
||||
public
|
||||
boolean isExpired(final Instant now) {
|
||||
Objects.requireNonNull(now, "now");
|
||||
|
||||
this.exec.assertIsExecutorThread();
|
||||
|
||||
return this.subscription.imageCount() == 0 && now.isAfter(this.initial_expire);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@code true} iff {@link #close()} has been called
|
||||
*/
|
||||
|
||||
public
|
||||
boolean isClosed() {
|
||||
this.exec.assertIsExecutorThread();
|
||||
|
||||
return this.closed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void close() {
|
||||
this.exec.assertIsExecutorThread();
|
||||
|
||||
if (!this.closed) {
|
||||
try {
|
||||
try {
|
||||
this.publication.close();
|
||||
} finally {
|
||||
this.subscription.close();
|
||||
}
|
||||
} finally {
|
||||
this.closed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The data port
|
||||
*/
|
||||
|
||||
public
|
||||
int portData() {
|
||||
return this.port_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The control port
|
||||
*/
|
||||
|
||||
public
|
||||
int portControl() {
|
||||
return this.port_control;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The IP address that is permitted to participate in this duologue
|
||||
*/
|
||||
|
||||
public
|
||||
InetAddress ownerAddress() {
|
||||
return this.owner;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The session ID of the duologue
|
||||
*/
|
||||
|
||||
public
|
||||
int session() {
|
||||
return this.session;
|
||||
}
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
package dorkbox.network.aeron;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The default implementation of the {@link EchoServerExecutorService} interface.
|
||||
*/
|
||||
|
||||
public final
|
||||
class EchoServerExecutor implements EchoServerExecutorService {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(EchoServerExecutor.class);
|
||||
|
||||
private final ExecutorService executor;
|
||||
|
||||
private
|
||||
EchoServerExecutor(final ExecutorService in_exec) {
|
||||
this.executor = Objects.requireNonNull(in_exec, "exec");
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
boolean isExecutorThread() {
|
||||
return Thread.currentThread() instanceof EchoServerThread;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void execute(final Runnable runnable) {
|
||||
Objects.requireNonNull(runnable, "runnable");
|
||||
|
||||
this.executor.submit(()->{
|
||||
try {
|
||||
runnable.run();
|
||||
} catch (final Throwable e) {
|
||||
LOG.error("uncaught exception: ", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void close() {
|
||||
this.executor.shutdown();
|
||||
}
|
||||
|
||||
private static final
|
||||
class EchoServerThread extends Thread {
|
||||
EchoServerThread(final Runnable target) {
|
||||
super(Objects.requireNonNull(target, "target"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A new executor
|
||||
*/
|
||||
|
||||
public static
|
||||
EchoServerExecutor create(Class<?> type) {
|
||||
final ThreadFactory factory = r->{
|
||||
final EchoServerThread t = new EchoServerThread(r);
|
||||
t.setName(new StringBuilder(64).append("network-")
|
||||
.append(type.getSimpleName())
|
||||
.append("[")
|
||||
.append(Long.toUnsignedString(t.getId()))
|
||||
.append("]")
|
||||
.toString());
|
||||
return t;
|
||||
};
|
||||
|
||||
return new EchoServerExecutor(Executors.newSingleThreadExecutor(factory));
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
package dorkbox.network.aeron;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* A simple executor service. {@link Runnable} values are executed in the order
|
||||
* that they are submitted on a single <i>executor thread</i>.
|
||||
*/
|
||||
|
||||
public interface EchoServerExecutorService extends AutoCloseable, Executor
|
||||
{
|
||||
/**
|
||||
* @return {@code true} if the caller of this method is running on the executor thread
|
||||
*/
|
||||
|
||||
boolean isExecutorThread();
|
||||
|
||||
/**
|
||||
* Raise {@link IllegalStateException} iff {@link #isExecutorThread()} would
|
||||
* currently return {@code false}.
|
||||
*/
|
||||
|
||||
default void assertIsExecutorThread()
|
||||
{
|
||||
if (!this.isExecutorThread()) {
|
||||
throw new IllegalStateException(
|
||||
"The current thread is not a server executor thread");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
package dorkbox.network.aeron;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.agrona.collections.IntHashSet;
|
||||
|
||||
import dorkbox.network.aeron.exceptions.EchoServerSessionAllocationException;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* An allocator for session IDs. The allocator randomly selects values from
|
||||
* the given range {@code [min, max]} and will not return a previously-returned value {@code x}
|
||||
* until {@code x} has been freed with {@code {@link EchoServerSessionAllocator#free(int)}.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* This implementation uses storage proportional to the number of currently-allocated
|
||||
* values. Allocation time is bounded by {@code max - min}, will be {@code O(1)}
|
||||
* with no allocated values, and will increase to {@code O(n)} as the number
|
||||
* of allocated values approached {@code max - min}.
|
||||
* </p>
|
||||
*/
|
||||
public final
|
||||
class EchoServerSessionAllocator {
|
||||
private final IntHashSet used;
|
||||
private final SecureRandom random;
|
||||
|
||||
private final int min;
|
||||
private final int max_count;
|
||||
|
||||
private
|
||||
EchoServerSessionAllocator(final int in_min, final int in_max, final SecureRandom in_random) {
|
||||
if (in_max < in_min) {
|
||||
throw new IllegalArgumentException(String.format("Maximum value %d must be >= minimum value %d",
|
||||
Integer.valueOf(in_max),
|
||||
Integer.valueOf(in_min)));
|
||||
}
|
||||
|
||||
this.used = new IntHashSet();
|
||||
this.min = in_min;
|
||||
this.max_count = Math.max(in_max - in_min, 1);
|
||||
this.random = Objects.requireNonNull(in_random, "random");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new session allocator.
|
||||
*
|
||||
* @param in_min The minimum session ID (inclusive)
|
||||
* @param in_max The maximum session ID (exclusive)
|
||||
* @param in_random A random number generator
|
||||
*
|
||||
* @return A new allocator
|
||||
*/
|
||||
public static
|
||||
EchoServerSessionAllocator create(final int in_min, final int in_max, final SecureRandom in_random) {
|
||||
return new EchoServerSessionAllocator(in_min, in_max, in_random);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocate a new session.
|
||||
*
|
||||
* @return A new session ID
|
||||
*
|
||||
* @throws EchoServerSessionAllocationException If there are no non-allocated sessions left
|
||||
*/
|
||||
public
|
||||
int allocate() throws EchoServerSessionAllocationException {
|
||||
if (this.used.size() == this.max_count) {
|
||||
throw new EchoServerSessionAllocationException("No session IDs left to allocate");
|
||||
}
|
||||
|
||||
for (int index = 0; index < this.max_count; ++index) {
|
||||
final int session = this.random.nextInt(this.max_count) + this.min;
|
||||
if (!this.used.contains(session)) {
|
||||
this.used.add(session);
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
throw new EchoServerSessionAllocationException(String.format("Unable to allocate a session ID after %d attempts (%d values in use)",
|
||||
Integer.valueOf(this.max_count),
|
||||
Integer.valueOf(this.used.size()),
|
||||
Integer.valueOf(this.max_count)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Free a session. After this method returns, {@code session} becomes eligible
|
||||
* for allocation by future calls to {@link #allocate()}.
|
||||
*
|
||||
* @param session The session to free
|
||||
*/
|
||||
public
|
||||
void free(final int session) {
|
||||
this.used.remove(session);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package dorkbox.network.aeron.client
|
||||
|
||||
/**
|
||||
* The type of exceptions raised by the client.
|
||||
*/
|
||||
open class ClientException : Exception {
|
||||
/**
|
||||
* Create an exception.
|
||||
*
|
||||
* @param message The message
|
||||
*/
|
||||
constructor(message: String) : super(message)
|
||||
|
||||
/**
|
||||
* Create an exception.
|
||||
*
|
||||
* @param cause The cause
|
||||
*/
|
||||
constructor(cause: Throwable) : super(cause)
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package dorkbox.network.aeron.client
|
||||
|
||||
/**
|
||||
* The server rejected this client when it tried to connect.
|
||||
*/
|
||||
class ClientRejectedException(message: String) : ClientException(message)
|
|
@ -0,0 +1,20 @@
|
|||
package dorkbox.network.aeron.client
|
||||
|
||||
/**
|
||||
* The client timed out when it attempted to connect to the server.
|
||||
*/
|
||||
class ClientTimedOutException : ClientException {
|
||||
/**
|
||||
* Create an exception.
|
||||
*
|
||||
* @param message The message
|
||||
*/
|
||||
constructor(message: String) : super(message)
|
||||
|
||||
/**
|
||||
* Create an exception.
|
||||
*
|
||||
* @param cause The cause
|
||||
*/
|
||||
constructor(cause: Throwable) : super(cause)
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
package dorkbox.network.aeron.exceptions;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public final class ClientIOException extends EchoClientException
|
||||
{
|
||||
public
|
||||
ClientIOException(final IOException cause)
|
||||
{
|
||||
super(cause);
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
package dorkbox.network.aeron.exceptions;
|
||||
|
||||
/**
|
||||
* An exception occurred whilst trying to create the client.
|
||||
*/
|
||||
|
||||
public final class EchoClientCreationException extends EchoClientException
|
||||
{
|
||||
/**
|
||||
* Create an exception.
|
||||
*
|
||||
* @param cause The cause
|
||||
*/
|
||||
|
||||
public EchoClientCreationException(final Exception cause)
|
||||
{
|
||||
super(cause);
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
package dorkbox.network.aeron.exceptions;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* The type of exceptions raised by the client.
|
||||
*/
|
||||
|
||||
public abstract class EchoClientException extends Exception
|
||||
{
|
||||
/**
|
||||
* Create an exception.
|
||||
*
|
||||
* @param message The message
|
||||
*/
|
||||
|
||||
public EchoClientException(final String message)
|
||||
{
|
||||
super(Objects.requireNonNull(message, "message"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an exception.
|
||||
*
|
||||
* @param cause The cause
|
||||
*/
|
||||
|
||||
public EchoClientException(final Throwable cause)
|
||||
{
|
||||
super(Objects.requireNonNull(cause, "cause"));
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
package dorkbox.network.aeron.exceptions;
|
||||
|
||||
/**
|
||||
* The server rejected this client when it tried to connect.
|
||||
*/
|
||||
|
||||
public final class EchoClientRejectedException extends EchoClientException
|
||||
{
|
||||
/**
|
||||
* Create an exception.
|
||||
*
|
||||
* @param message The message
|
||||
*/
|
||||
|
||||
public EchoClientRejectedException(final String message)
|
||||
{
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
package dorkbox.network.aeron.exceptions;
|
||||
|
||||
/**
|
||||
* The client timed out when it attempted to connect to the server.
|
||||
*/
|
||||
|
||||
public final class EchoClientTimedOutException extends EchoClientException
|
||||
{
|
||||
/**
|
||||
* Create an exception.
|
||||
*
|
||||
* @param message The message
|
||||
*/
|
||||
|
||||
public EchoClientTimedOutException(final String message)
|
||||
{
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
package dorkbox.network.aeron.exceptions;
|
||||
|
||||
/**
|
||||
* An exception occurred whilst trying to create the server.
|
||||
*/
|
||||
|
||||
public final
|
||||
class EchoServerCreationException extends EchoServerException {
|
||||
/**
|
||||
* Create an exception.
|
||||
*
|
||||
* @param cause The cause
|
||||
*/
|
||||
|
||||
public
|
||||
EchoServerCreationException(final Exception cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
package dorkbox.network.aeron.exceptions;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* The type of exceptions raised by the server.
|
||||
*/
|
||||
|
||||
public abstract class EchoServerException extends Exception
|
||||
{
|
||||
/**
|
||||
* Create an exception.
|
||||
*
|
||||
* @param message The message
|
||||
*/
|
||||
|
||||
public EchoServerException(final String message)
|
||||
{
|
||||
super(Objects.requireNonNull(message, "message"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an exception.
|
||||
*
|
||||
* @param cause The cause
|
||||
*/
|
||||
|
||||
public EchoServerException(final Throwable cause)
|
||||
{
|
||||
super(Objects.requireNonNull(cause, "cause"));
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
package dorkbox.network.aeron.exceptions;
|
||||
|
||||
/**
|
||||
* A port could not be allocated.
|
||||
*/
|
||||
|
||||
public final class EchoServerPortAllocationException extends EchoServerException
|
||||
{
|
||||
/**
|
||||
* Create an exception.
|
||||
*
|
||||
* @param message The message
|
||||
*/
|
||||
|
||||
public EchoServerPortAllocationException(
|
||||
final String message)
|
||||
{
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
package dorkbox.network.aeron.exceptions;
|
||||
|
||||
/**
|
||||
* A session could not be allocated.
|
||||
*/
|
||||
|
||||
public final class EchoServerSessionAllocationException
|
||||
extends EchoServerException
|
||||
{
|
||||
/**
|
||||
* Create an exception.
|
||||
*
|
||||
* @param message The message
|
||||
*/
|
||||
|
||||
public EchoServerSessionAllocationException(
|
||||
final String message)
|
||||
{
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package dorkbox.network.aeron.server
|
||||
|
||||
/**
|
||||
* A session/stream could not be allocated.
|
||||
*/
|
||||
class AllocationException(message: String) : ServerException(message)
|
|
@ -0,0 +1,6 @@
|
|||
package dorkbox.network.aeron.server
|
||||
|
||||
/**
|
||||
* A port could not be allocated.
|
||||
*/
|
||||
class PortAllocationException(message: String) : ServerException(message)
|
|
@ -1,102 +0,0 @@
|
|||
package dorkbox.network.aeron.server;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.agrona.collections.IntArrayList;
|
||||
import org.agrona.collections.IntHashSet;
|
||||
|
||||
import dorkbox.network.aeron.exceptions.EchoServerPortAllocationException;
|
||||
|
||||
/**
|
||||
* <p>An allocator for port numbers.</p>
|
||||
*
|
||||
* <p>
|
||||
* The allocator accepts a base number {@code p} and a maximum count {@code n | n > 0}, and will allocate
|
||||
* up to {@code n} numbers, in a random order, in the range {@code [p, p + n - 1}.
|
||||
* </p>
|
||||
*/
|
||||
public final
|
||||
class PortAllocator {
|
||||
private final int port_lo;
|
||||
private final int port_hi;
|
||||
|
||||
private final IntHashSet ports_used;
|
||||
private final IntArrayList ports_free;
|
||||
|
||||
/**
|
||||
* Create a new port allocator.
|
||||
*
|
||||
* @param port_base The base port
|
||||
* @param max_ports The maximum number of ports that will be allocated
|
||||
*
|
||||
* @return A new port allocator
|
||||
*/
|
||||
|
||||
public static
|
||||
PortAllocator create(final int port_base, final int max_ports) {
|
||||
return new PortAllocator(port_base, max_ports);
|
||||
}
|
||||
|
||||
private
|
||||
PortAllocator(final int in_port_lo, final int in_max_ports) {
|
||||
if (in_port_lo <= 0 || in_port_lo >= 65536) {
|
||||
throw new IllegalArgumentException(String.format("Base port %d must be in the range [1, 65535]", Integer.valueOf(in_port_lo)));
|
||||
}
|
||||
|
||||
this.port_lo = in_port_lo;
|
||||
this.port_hi = in_port_lo + (in_max_ports - 1);
|
||||
|
||||
if (this.port_hi < 0 || this.port_hi >= 65536) {
|
||||
throw new IllegalArgumentException(String.format("Uppermost port %d must be in the range [1, 65535]",
|
||||
Integer.valueOf(this.port_hi)));
|
||||
}
|
||||
|
||||
this.ports_used = new IntHashSet(in_max_ports);
|
||||
this.ports_free = new IntArrayList();
|
||||
|
||||
for (int port = in_port_lo; port <= this.port_hi; ++port) {
|
||||
this.ports_free.addInt(port);
|
||||
}
|
||||
Collections.shuffle(this.ports_free);
|
||||
}
|
||||
|
||||
/**
|
||||
* Free a given port. Has no effect if the given port is outside of the range
|
||||
* considered by the allocator.
|
||||
*
|
||||
* @param port The port
|
||||
*/
|
||||
public
|
||||
void free(final int port) {
|
||||
if (port >= this.port_lo && port <= this.port_hi) {
|
||||
this.ports_used.remove(port);
|
||||
this.ports_free.addInt(port);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocate {@code count} ports.
|
||||
*
|
||||
* @param count The number of ports that will be allocated
|
||||
*
|
||||
* @return An array of allocated ports
|
||||
*
|
||||
* @throws EchoServerPortAllocationException If there are fewer than {@code count} ports available to allocate
|
||||
*/
|
||||
public
|
||||
int[] allocate(final int count) throws EchoServerPortAllocationException {
|
||||
if (this.ports_free.size() < count) {
|
||||
throw new EchoServerPortAllocationException(String.format("Too few ports available to allocate %d ports",
|
||||
Integer.valueOf(count)));
|
||||
}
|
||||
|
||||
final int[] result = new int[count];
|
||||
for (int index = 0; index < count; ++index) {
|
||||
result[index] = this.ports_free.remove(0)
|
||||
.intValue();
|
||||
this.ports_used.add(result[index]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
package dorkbox.network.aeron.server
|
||||
|
||||
import org.agrona.collections.IntArrayList
|
||||
|
||||
/**
|
||||
* An allocator for port numbers.
|
||||
*
|
||||
* The allocator accepts a base number `p` and a maximum count `n | n > 0`, and will allocate
|
||||
* up to `n` numbers, in a random order, in the range `[p, p + n - 1`.
|
||||
*
|
||||
* @param basePort The base port
|
||||
* @param numberOfPortsToAllocate The maximum number of ports that will be allocated
|
||||
*
|
||||
* @throws IllegalArgumentException If the port range is not valid
|
||||
*/
|
||||
class PortAllocator(basePort: Int, numberOfPortsToAllocate: Int) {
|
||||
private val minPort: Int
|
||||
private val maxPort: Int
|
||||
|
||||
private val portShuffleReset: Int
|
||||
private var portShuffleCount: Int
|
||||
private val freePorts: IntArrayList
|
||||
|
||||
init {
|
||||
if (basePort !in 1..65535) {
|
||||
throw IllegalArgumentException("Base port $basePort must be in the range [1, 65535]")
|
||||
}
|
||||
|
||||
minPort = basePort
|
||||
maxPort = basePort + (numberOfPortsToAllocate - 1)
|
||||
|
||||
if (maxPort !in (basePort + 1)..65535) {
|
||||
throw IllegalArgumentException("Uppermost port $maxPort must be in the range [$basePort, 65535]")
|
||||
}
|
||||
|
||||
// every time we add 25% of ports back (via 'free'), reshuffle the ports
|
||||
portShuffleReset = numberOfPortsToAllocate/4
|
||||
portShuffleCount = portShuffleReset
|
||||
|
||||
freePorts = IntArrayList()
|
||||
|
||||
for (port in basePort..maxPort) {
|
||||
freePorts.addInt(port)
|
||||
}
|
||||
|
||||
freePorts.shuffle()
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocate `count` number of ports.
|
||||
*
|
||||
* @param count The number of ports that will be allocated
|
||||
*
|
||||
* @return An array of allocated ports
|
||||
*
|
||||
* @throws PortAllocationException If there are fewer than `count` ports available to allocate
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun allocate(count: Int): IntArray {
|
||||
if (freePorts.size < count) {
|
||||
throw IllegalArgumentException("Too few ports available to allocate $count ports")
|
||||
}
|
||||
|
||||
// reshuffle the ports once we need to re-allocate a new port
|
||||
if (portShuffleCount <= 0) {
|
||||
portShuffleCount = portShuffleReset
|
||||
freePorts.shuffle()
|
||||
}
|
||||
|
||||
val result = IntArray(count)
|
||||
for (index in 0 until count) {
|
||||
val lastValue = freePorts.size - 1
|
||||
val removed = freePorts.removeAt(lastValue)
|
||||
result[index] = removed
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees the given ports. Has no effect if the given port is outside of the range considered by the allocator.
|
||||
*
|
||||
* @param ports The array of ports to free
|
||||
*/
|
||||
fun free(ports: IntArray) {
|
||||
ports.forEach {
|
||||
free(it)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Free a given port.
|
||||
* <p>
|
||||
* Has no effect if the given port is outside of the range considered by the allocator.
|
||||
*
|
||||
* @param port The port
|
||||
*/
|
||||
fun free(port: Int) {
|
||||
if (port in minPort..maxPort) {
|
||||
// add at the end (so we don't have unnecessary array resizes)
|
||||
freePorts.addInt(freePorts.size, port)
|
||||
|
||||
portShuffleCount--
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package dorkbox.network.aeron.server
|
||||
|
||||
import org.agrona.collections.IntHashSet
|
||||
import java.security.SecureRandom
|
||||
|
||||
/**
|
||||
* An allocator for session IDs. The allocator randomly selects values from
|
||||
* the given range `[min, max]` and will not return a previously-returned value `x`
|
||||
* until `x` has been freed with `{ SessionAllocator#free(int)}.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* This implementation uses storage proportional to the number of currently-allocated
|
||||
* values. Allocation time is bounded by { max - min}, will be { O(1)}
|
||||
* with no allocated values, and will increase to { O(n)} as the number
|
||||
* of allocated values approached { max - min}.
|
||||
* </p>`
|
||||
*
|
||||
* @param min The minimum session ID (inclusive)
|
||||
* @param max The maximum session ID (exclusive)
|
||||
*/
|
||||
class RandomIdAllocator(private val min: Int, max: Int) {
|
||||
private val used = IntHashSet()
|
||||
private val random = SecureRandom()
|
||||
private val maxAssignments: Int
|
||||
|
||||
init {
|
||||
// IllegalArgumentException
|
||||
require(max >= min) {
|
||||
"Maximum value $max must be >= minimum value $min"
|
||||
}
|
||||
|
||||
maxAssignments = (max - min).coerceAtLeast(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocate a new session. Will never allocate session ID '0'
|
||||
*
|
||||
* @return A new session ID
|
||||
*
|
||||
* @throws AllocationException If there are no non-allocated sessions left
|
||||
*/
|
||||
@Throws(AllocationException::class)
|
||||
fun allocate(): Int {
|
||||
if (used.size == maxAssignments) {
|
||||
throw AllocationException("No session IDs left to allocate")
|
||||
}
|
||||
|
||||
for (index in 0 until maxAssignments) {
|
||||
val session = random.nextInt(maxAssignments) + min
|
||||
if (!used.contains(session)) {
|
||||
used.add(session)
|
||||
return session
|
||||
}
|
||||
}
|
||||
|
||||
throw AllocationException("Unable to allocate a session ID after $maxAssignments attempts (${used.size} values in use")
|
||||
}
|
||||
|
||||
/**
|
||||
* Free a session. After this method returns, `session` becomes eligible
|
||||
* for allocation by future calls to [.allocate].
|
||||
*
|
||||
* @param session The session to free
|
||||
*/
|
||||
fun free(session: Int) {
|
||||
used.remove(session)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package dorkbox.network.aeron.server
|
||||
|
||||
/**
|
||||
* The type of exceptions raised by the server.
|
||||
*/
|
||||
open class ServerException : Exception {
|
||||
/**
|
||||
* Create an exception.
|
||||
*
|
||||
* @param message The message
|
||||
*/
|
||||
constructor(message: String) : super(message)
|
||||
|
||||
/**
|
||||
* Create an exception.
|
||||
*
|
||||
* @param cause The cause
|
||||
*/
|
||||
constructor(cause: Throwable) : super(cause)
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection;
|
||||
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
|
||||
public
|
||||
class BootstrapWrapper {
|
||||
public final String type;
|
||||
public final Bootstrap bootstrap;
|
||||
public final String address;
|
||||
public final int port;
|
||||
|
||||
public
|
||||
BootstrapWrapper(String type, String address, int port, Bootstrap bootstrap) {
|
||||
this.type = type;
|
||||
this.address = address;
|
||||
this.port = port;
|
||||
this.bootstrap = bootstrap;
|
||||
}
|
||||
|
||||
public
|
||||
BootstrapWrapper clone(EventLoopGroup group) {
|
||||
return new BootstrapWrapper(type, address, port, bootstrap.clone(group));
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
String toString() {
|
||||
return "BootstrapWrapper{" + "type='" + type + '\'' + ", address='" + address + '\'' + ", port=" + port + '}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package dorkbox.network.connection
|
||||
|
||||
import org.slf4j.Logger
|
||||
|
||||
data class ClientConnectionInfo(val subscriptionPort: Int,
|
||||
val publicationPort: Int,
|
||||
val sessionId: Int,
|
||||
val streamId: Int,
|
||||
val publicKey: ByteArray) {
|
||||
|
||||
fun log(handshakeSessionId: Int, logger: Logger) {
|
||||
logger.debug("[{}] connect {} {} (encrypted {})", handshakeSessionId, subscriptionPort, publicationPort, sessionId)
|
||||
}
|
||||
}
|
|
@ -13,99 +13,114 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection;
|
||||
package dorkbox.network.connection
|
||||
|
||||
import dorkbox.network.rmi.RemoteObject;
|
||||
import dorkbox.network.rmi.RemoteObjectCallback;
|
||||
import dorkbox.network.rmi.TimeoutException;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public
|
||||
interface Connection {
|
||||
import dorkbox.network.rmi.RemoteObjectCallback
|
||||
|
||||
interface Connection : AutoCloseable {
|
||||
/**
|
||||
* Has the remote ECC public key changed. This can be useful if specific actions are necessary when the key has changed.
|
||||
*/
|
||||
boolean hasRemoteKeyChanged();
|
||||
fun hasRemoteKeyChanged(): Boolean
|
||||
|
||||
/**
|
||||
* @return the remote address, as a string.
|
||||
* The publication port (used by aeron) for this connection. This is from the perspective of the server!
|
||||
*/
|
||||
String getRemoteHost();
|
||||
val subscriptionPort: Int
|
||||
val publicationPort: Int
|
||||
|
||||
/**
|
||||
* the remote address, as a string.
|
||||
*/
|
||||
val remoteAddress: String
|
||||
|
||||
/**
|
||||
* the remote address, as an integer.
|
||||
*/
|
||||
val remoteAddressInt: Int
|
||||
|
||||
/**
|
||||
* @return true if this connection is established on the loopback interface
|
||||
*/
|
||||
boolean isLoopback();
|
||||
val isLoopback: Boolean
|
||||
|
||||
/**
|
||||
* @return true if this connection is an IPC connection
|
||||
*/
|
||||
boolean isIPC();
|
||||
val isIPC: Boolean
|
||||
|
||||
/**
|
||||
* @return true if this connection is a network connection
|
||||
*/
|
||||
boolean isNetwork();
|
||||
val isNetwork: Boolean
|
||||
|
||||
/**
|
||||
* @return the connection id of this connection.
|
||||
* the stream id of this connection.
|
||||
*/
|
||||
int id();
|
||||
val streamId: Int
|
||||
|
||||
/**
|
||||
* @return the connection id of this connection as a HEX string.
|
||||
* the session id of this connection.
|
||||
*/
|
||||
String idAsHex();
|
||||
val sessionId: Int
|
||||
|
||||
/**
|
||||
* Polls the AERON media driver subscription channel for incoming messages
|
||||
*/
|
||||
fun pollSubscriptions(): Int
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Safely sends objects to a destination.
|
||||
*/
|
||||
ConnectionPoint send(Object message);
|
||||
suspend fun send(message: Any)
|
||||
|
||||
/**
|
||||
* Safely sends objects to a destination with the specified priority.
|
||||
* <p>
|
||||
*
|
||||
*
|
||||
* A priority of 255 (highest) will always be sent immediately.
|
||||
* <p>
|
||||
*
|
||||
*
|
||||
* A priority of 0-254 will be sent (0, the lowest, will be last) if there is no backpressure from the MediaDriver.
|
||||
*/
|
||||
ConnectionPoint send(Object message, byte priority);
|
||||
suspend fun send(message: Any, priority: Byte)
|
||||
|
||||
/**
|
||||
* Safely sends objects to a destination, but does not guarantee delivery
|
||||
*/
|
||||
ConnectionPoint sendUnreliable(Object message);
|
||||
|
||||
|
||||
/**
|
||||
* Safely sends objects to a destination, but does not guarantee delivery.
|
||||
* <p>
|
||||
* A priority of 255 (highest) will always be sent immediately.
|
||||
* <p>
|
||||
* A priority of 0-254 will be sent (0, the lowest, will be last) if there is no backpressure from the MediaDriver.
|
||||
*/
|
||||
ConnectionPoint sendUnreliable(Object message, byte priority);
|
||||
|
||||
/**
|
||||
* Sends a "ping" packet, trying UDP then TCP (in that order) to measure <b>ROUND TRIP</b> time to the remote connection.
|
||||
* Sends a "ping" packet, trying UDP then TCP (in that order) to measure **ROUND TRIP** time to the remote connection.
|
||||
*
|
||||
* @return Ping can have a listener attached, which will get called when the ping returns.
|
||||
*/
|
||||
Ping ping(); // TODO: USE AERON FOR THIS
|
||||
|
||||
suspend fun ping(): Ping // TODO: USE AERON FOR THIS
|
||||
|
||||
// /**
|
||||
// * Expose methods to modify the connection-specific listeners.
|
||||
// */
|
||||
// fun listeners(): Listeners<Connection>
|
||||
|
||||
/**
|
||||
* Expose methods to modify the connection-specific listeners.
|
||||
* @param now The current time
|
||||
*
|
||||
* @return `true` if this connection has no subscribers and the current time `now` is after the expiration date
|
||||
*/
|
||||
Listeners listeners();
|
||||
fun isExpired(now: Long): Boolean
|
||||
|
||||
/**
|
||||
* @param now The current time
|
||||
*
|
||||
* @return `true` if this connection has no subscribers and the current time `now` is after the expiration date
|
||||
*/
|
||||
fun isClosed(): Boolean
|
||||
|
||||
|
||||
/**
|
||||
* Closes the connection and removes all listeners
|
||||
*/
|
||||
void close();
|
||||
@Throws(Exception::class)
|
||||
override fun close()
|
||||
|
||||
|
||||
|
||||
// TODO: below should just be "new()" to create a new object, to mirror "new Object()"
|
||||
// // RMI
|
||||
|
@ -114,20 +129,27 @@ interface Connection {
|
|||
// // using some tricks, we can make it so that it DOESN'T matter the order in which objects are created,
|
||||
// // and can specify, if we want, the object created.
|
||||
// // Once created though, as NEW ONE with the same ID cannot be created until the old one is removed!
|
||||
|
||||
|
||||
/**
|
||||
* Tells the remote connection to create a new proxy object that implements the specified interface. The methods on this object "map"
|
||||
* to an object that is created remotely.
|
||||
* <p>
|
||||
*
|
||||
*
|
||||
* The callback will be notified when the remote object has been created.
|
||||
* <p>
|
||||
* <p>
|
||||
* Methods that return a value will throw {@link TimeoutException} if the response is not received with the
|
||||
* {@link RemoteObject#setResponseTimeout(int) response timeout}.
|
||||
* <p/>
|
||||
* If {@link RemoteObject#setAsync(boolean) non-blocking} is false (the default), then methods that return a value must
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
* Methods that return a value will throw [TimeoutException] if the response is not received with the
|
||||
* [response timeout][RemoteObject.setResponseTimeout].
|
||||
*
|
||||
*
|
||||
* If [non-blocking][RemoteObject.setAsync] is false (the default), then methods that return a value must
|
||||
* not be called from the update thread for the connection. An exception will be thrown if this occurs. Methods with a
|
||||
* void return value can be called on the update thread.
|
||||
* <p/>
|
||||
*
|
||||
*
|
||||
* If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side
|
||||
* will have the proxy object replaced with the registered (non-proxy) object.
|
||||
*
|
||||
|
@ -136,29 +158,35 @@ interface Connection {
|
|||
*
|
||||
* @see RemoteObject
|
||||
*/
|
||||
<Iface> void createRemoteObject(final Class<Iface> interfaceClass, final RemoteObjectCallback<Iface> callback);
|
||||
suspend fun <Iface> createRemoteObject(interfaceClass: Class<Iface>, callback: RemoteObjectCallback<Iface>)
|
||||
|
||||
/**
|
||||
* Tells the remote connection to access an already created proxy object that implements the specified interface. The methods on this object "map"
|
||||
* to an object that is created remotely.
|
||||
* <p>
|
||||
*
|
||||
*
|
||||
* The callback will be notified when the remote object has been created.
|
||||
* <p>
|
||||
* <p>
|
||||
* Methods that return a value will throw {@link TimeoutException} if the response is not received with the
|
||||
* {@link RemoteObject#setResponseTimeout(int) response timeout}.
|
||||
* <p/>
|
||||
* If {@link RemoteObject#setAsync(boolean) non-blocking} is false (the default), then methods that return a value must
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
* Methods that return a value will throw [TimeoutException] if the response is not received with the
|
||||
* [response timeout][RemoteObject.setResponseTimeout].
|
||||
*
|
||||
*
|
||||
* If [non-blocking][RemoteObject.setAsync] is false (the default), then methods that return a value must
|
||||
* not be called from the update thread for the connection. An exception will be thrown if this occurs. Methods with a
|
||||
* void return value can be called on the update thread.
|
||||
* <p/>
|
||||
*
|
||||
*
|
||||
* If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side
|
||||
* will have the proxy object replaced with the registered (non-proxy) object.
|
||||
* <p>
|
||||
*
|
||||
*
|
||||
* If one wishes to change the default behavior, cast the object to access the different methods.
|
||||
* ie: `RemoteObject remoteObject = (RemoteObject) test;`
|
||||
*
|
||||
* @see RemoteObject
|
||||
*/
|
||||
<Iface> void getRemoteObject(final int objectId, final RemoteObjectCallback<Iface> callback);
|
||||
suspend fun <Iface> getRemoteObject(objectId: Int, callback: RemoteObjectCallback<Iface>)
|
||||
}
|
|
@ -1,974 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import dorkbox.network.Client;
|
||||
import dorkbox.network.connection.bridge.ConnectionBridge;
|
||||
import dorkbox.network.connection.ping.PingFuture;
|
||||
import dorkbox.network.connection.ping.PingMessage;
|
||||
import dorkbox.network.connection.ping.PingTuple;
|
||||
import dorkbox.network.connection.wrapper.ChannelNetworkWrapper;
|
||||
import dorkbox.network.connection.wrapper.ChannelNull;
|
||||
import dorkbox.network.connection.wrapper.ChannelWrapper;
|
||||
import dorkbox.network.rmi.ConnectionNoOpSupport;
|
||||
import dorkbox.network.rmi.ConnectionRmiLocalSupport;
|
||||
import dorkbox.network.rmi.ConnectionRmiNetworkSupport;
|
||||
import dorkbox.network.rmi.ConnectionRmiSupport;
|
||||
import dorkbox.network.rmi.RemoteObjectCallback;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandler.Sharable;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.channel.epoll.EpollSocketChannel;
|
||||
import io.netty.channel.kqueue.KQueueSocketChannel;
|
||||
import io.netty.channel.local.LocalChannel;
|
||||
import io.netty.channel.socket.nio.NioSocketChannel;
|
||||
import io.netty.channel.socket.oio.OioSocketChannel;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
import io.netty.util.concurrent.Promise;
|
||||
|
||||
|
||||
/**
|
||||
* The "network connection" is established once the registration is validated for TCP/UDP
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
@Sharable
|
||||
public
|
||||
class ConnectionImpl extends ChannelInboundHandlerAdapter implements Connection_, Listeners, ConnectionBridge {
|
||||
public static
|
||||
boolean isTcpChannel(Class<? extends Channel> channelClass) {
|
||||
return channelClass == OioSocketChannel.class ||
|
||||
channelClass == NioSocketChannel.class ||
|
||||
channelClass == KQueueSocketChannel.class ||
|
||||
channelClass == EpollSocketChannel.class;
|
||||
}
|
||||
|
||||
public static
|
||||
boolean isUdpChannel(Class<? extends Channel> channelClass) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static
|
||||
boolean isLocalChannel(Class<? extends Channel> channelClass) {
|
||||
return channelClass == LocalChannel.class;
|
||||
}
|
||||
|
||||
|
||||
private final org.slf4j.Logger logger;
|
||||
|
||||
private final AtomicBoolean needsLock = new AtomicBoolean(false);
|
||||
private final AtomicBoolean writeSignalNeeded = new AtomicBoolean(false);
|
||||
private final Object writeLock = new Object();
|
||||
|
||||
private final AtomicBoolean closeInProgress = new AtomicBoolean(false);
|
||||
private final AtomicBoolean channelIsClosed = new AtomicBoolean(false);
|
||||
|
||||
private final Object messageInProgressLock = new Object();
|
||||
private final AtomicBoolean messageInProgress = new AtomicBoolean(false);
|
||||
|
||||
private final ISessionManager sessionManager;
|
||||
private final ChannelWrapper channelWrapper;
|
||||
|
||||
private volatile PingFuture pingFuture = null;
|
||||
|
||||
// used to store connection local listeners (instead of global listeners). Only possible on the server.
|
||||
private volatile ConnectionManager localListenerManager;
|
||||
|
||||
// while on the CLIENT, if the SERVER's ecc key has changed, the client will abort and show an error.
|
||||
private boolean remoteKeyChanged;
|
||||
|
||||
private final EndPoint endPoint;
|
||||
|
||||
|
||||
// The IV for AES-GCM must be 12 bytes, since it's 4 (salt) + 8 (external counter) + 4 (GCM counter)
|
||||
// The 12 bytes IV is created during connection registration, and during the AES-GCM crypto, we override the last 8 with this
|
||||
// counter, which is also transmitted as an optimized int. (which is why it starts at 0, so the transmitted bytes are small)
|
||||
private final AtomicLong aes_gcm_iv = new AtomicLong(0);
|
||||
|
||||
|
||||
// when closing this connection, HOW MANY endpoints need to be closed?
|
||||
private CountDownLatch closeLatch;
|
||||
|
||||
|
||||
// RMI support for this connection
|
||||
final ConnectionRmiSupport rmiSupport;
|
||||
|
||||
/**
|
||||
* All of the parameters can be null, when metaChannel wants to get the base class type
|
||||
*/
|
||||
public
|
||||
ConnectionImpl(final EndPoint endPoint, final ChannelWrapper channelWrapper) {
|
||||
this.endPoint = endPoint;
|
||||
|
||||
if (endPoint != null) {
|
||||
this.channelWrapper = channelWrapper;
|
||||
this.logger = endPoint.logger;
|
||||
this.sessionManager = endPoint.connectionManager;
|
||||
|
||||
boolean isNetworkChannel = this.channelWrapper instanceof ChannelNetworkWrapper;
|
||||
|
||||
if (endPoint.rmiEnabled) {
|
||||
if (isNetworkChannel) {
|
||||
// because this is PER CONNECTION, there is no need for synchronize(), since there will not be any issues with concurrent access, but
|
||||
// there WILL be issues with thread visibility because a different worker thread can be called for different connections
|
||||
this.rmiSupport = new ConnectionRmiNetworkSupport(this, endPoint.rmiGlobalBridge);
|
||||
}
|
||||
else {
|
||||
// because this is PER CONNECTION, there is no need for synchronize(), since there will not be any issues with concurrent access, but
|
||||
// there WILL be issues with thread visibility because a different worker thread can be called for different connections
|
||||
this.rmiSupport = new ConnectionRmiLocalSupport(this, endPoint.rmiGlobalBridge);
|
||||
}
|
||||
} else {
|
||||
this.rmiSupport = new ConnectionNoOpSupport();
|
||||
}
|
||||
|
||||
|
||||
if (isNetworkChannel) {
|
||||
this.remoteKeyChanged = ((ChannelNetworkWrapper) channelWrapper).remoteKeyChanged();
|
||||
|
||||
int count = 0;
|
||||
if (channelWrapper.tcp() != null) {
|
||||
count++;
|
||||
}
|
||||
|
||||
if (channelWrapper.udp() != null) {
|
||||
count++;
|
||||
}
|
||||
|
||||
// when closing this connection, HOW MANY endpoints need to be closed?
|
||||
this.closeLatch = new CountDownLatch(count);
|
||||
}
|
||||
else {
|
||||
this.remoteKeyChanged = false;
|
||||
|
||||
// when closing this connection, HOW MANY endpoints need to be closed?
|
||||
this.closeLatch = new CountDownLatch(1);
|
||||
}
|
||||
|
||||
} else {
|
||||
this.logger = null;
|
||||
this.sessionManager = null;
|
||||
this.channelWrapper = null;
|
||||
this.rmiSupport = new ConnectionNoOpSupport();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the AES key. key=32 byte, iv=12 bytes (AES-GCM implementation).
|
||||
*/
|
||||
@Override
|
||||
public final
|
||||
SecretKey cryptoKey() {
|
||||
return this.channelWrapper.cryptoKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the per-message sequence number.
|
||||
*
|
||||
* The IV for AES-GCM must be 12 bytes, since it's 4 (salt) + 4 (external counter) + 4 (GCM counter)
|
||||
* The 12 bytes IV is created during connection registration, and during the AES-GCM crypto, we override the last 8 with this
|
||||
* counter, which is also transmitted as an optimized int. (which is why it starts at 0, so the transmitted bytes are small)
|
||||
*/
|
||||
@Override
|
||||
public final
|
||||
long nextGcmSequence() {
|
||||
return aes_gcm_iv.getAndIncrement();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Has the remote ECC public key changed. This can be useful if specific actions are necessary when the key has changed.
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
boolean hasRemoteKeyChanged() {
|
||||
return this.remoteKeyChanged;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the remote address, as a string.
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
String getRemoteHost() {
|
||||
return this.channelWrapper.getRemoteHost();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if this connection is established on the loopback interface
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
boolean isLoopback() {
|
||||
return channelWrapper.isLoopback();
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
boolean isIPC() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
boolean isNetwork() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the endpoint associated with this connection
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
EndPoint getEndPoint() {
|
||||
return this.endPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the connection (TCP or LOCAL) id of this connection.
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
int id() {
|
||||
return this.channelWrapper.id();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the connection (TCP or LOCAL) id of this connection as a HEX string.
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
String idAsHex() {
|
||||
return Integer.toHexString(id());
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the ping times for this connection (called when this connection gets a REPLY ping message).
|
||||
*/
|
||||
public final
|
||||
void updatePingResponse(PingMessage ping) {
|
||||
if (this.pingFuture != null) {
|
||||
this.pingFuture.setSuccess(this, ping);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a "ping" packet, trying UDP then TCP (in that order) to measure <b>ROUND TRIP</b> time to the remote connection.
|
||||
*
|
||||
* @return Ping can have a listener attached, which will get called when the ping returns.
|
||||
*/
|
||||
@Override
|
||||
public final
|
||||
Ping ping() {
|
||||
PingFuture pingFuture2 = this.pingFuture;
|
||||
if (pingFuture2 != null && !pingFuture2.isSuccess()) {
|
||||
pingFuture2.cancel();
|
||||
}
|
||||
|
||||
Promise<PingTuple<? extends Connection>> newPromise;
|
||||
if (this.channelWrapper.udp() != null) {
|
||||
newPromise = this.channelWrapper.udp()
|
||||
.newPromise();
|
||||
}
|
||||
else {
|
||||
newPromise = this.channelWrapper.tcp()
|
||||
.newPromise();
|
||||
}
|
||||
|
||||
this.pingFuture = new PingFuture(newPromise);
|
||||
|
||||
PingMessage ping = new PingMessage();
|
||||
ping.id = this.pingFuture.getId();
|
||||
ping0(ping);
|
||||
|
||||
return this.pingFuture;
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL USE ONLY. Used to initiate a ping, and to return a ping.
|
||||
*
|
||||
* Sends a ping message attempted in the following order: UDP, TCP,LOCAL
|
||||
*/
|
||||
public final
|
||||
void ping0(PingMessage ping) {
|
||||
if (this.channelWrapper.udp() != null) {
|
||||
UDP(ping);
|
||||
}
|
||||
else if (this.channelWrapper.tcp() != null) {
|
||||
TCP(ping);
|
||||
}
|
||||
else {
|
||||
self(ping);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last calculated TCP return trip time, or -1 if or the {@link PingMessage} response has not yet been received.
|
||||
*/
|
||||
public final
|
||||
int getLastRoundTripTime() {
|
||||
PingFuture pingFuture2 = this.pingFuture;
|
||||
if (pingFuture2 != null) {
|
||||
return pingFuture2.getResponse();
|
||||
}
|
||||
else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void channelWritabilityChanged(final ChannelHandlerContext context) throws Exception {
|
||||
super.channelWritabilityChanged(context);
|
||||
|
||||
// needed to place back-pressure when writing too much data to the connection
|
||||
if (writeSignalNeeded.getAndSet(false)) {
|
||||
synchronized (writeLock) {
|
||||
needsLock.set(false);
|
||||
writeLock.notifyAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* needed to place back-pressure when writing too much data to the connection.
|
||||
*
|
||||
* This blocks until we are writable again
|
||||
*/
|
||||
final
|
||||
void controlBackPressure(ConnectionPoint c) {
|
||||
while (!closeInProgress.get() && !c.isWritable()) {
|
||||
needsLock.set(true);
|
||||
writeSignalNeeded.set(true);
|
||||
|
||||
synchronized (writeLock) {
|
||||
if (needsLock.get()) {
|
||||
try {
|
||||
// waits 1 second maximum per check. This is to guarantee that eventually (in the case of deadlocks, which i've seen)
|
||||
// it will get released. The while loop makes sure it will exit when the channel is writable
|
||||
writeLock.wait(1000);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely sends objects to a destination (such as a custom object or a standard ping). This will automatically choose which protocol
|
||||
* is available to use. If you want specify the protocol, use {@link #TCP(Object)}, {@link #UDP(Object)}, or {@link #self(Object)}.
|
||||
*
|
||||
* By default, this will try in the following order:
|
||||
* - TCP (if available)
|
||||
* - UDP (if available)
|
||||
* - LOCAL (sending a message to itself)
|
||||
*/
|
||||
@Override
|
||||
public final
|
||||
ConnectionPoint send(final Object message) {
|
||||
if (this.channelWrapper.tcp() != null) {
|
||||
return TCP(message);
|
||||
}
|
||||
else if (this.channelWrapper.udp() != null) {
|
||||
return UDP(message);
|
||||
}
|
||||
else {
|
||||
self(message);
|
||||
|
||||
// we have to return something, otherwise dependent code will throw a null pointer exception
|
||||
return ChannelNull.get();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ConnectionPoint send(final Object message, final byte priority) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ConnectionPoint sendUnreliable(final Object message) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ConnectionPoint sendUnreliable(final Object message, final byte priority) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the object to other listeners INSIDE this endpoint. It does not send it to a remote address.
|
||||
*/
|
||||
@Override
|
||||
public final
|
||||
ConnectionPoint self(Object message) {
|
||||
logger.trace("Sending LOCAL {}", message);
|
||||
this.sessionManager.onMessage(this, message);
|
||||
|
||||
// THIS IS REALLY A LOCAL CONNECTION!
|
||||
return this.channelWrapper.tcp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the object over the network using TCP. (LOCAL channels do not care if its TCP or UDP)
|
||||
*/
|
||||
@Override
|
||||
public final
|
||||
ConnectionPoint TCP(final Object message) {
|
||||
if (!closeInProgress.get()) {
|
||||
logger.trace("Sending TCP {}", message);
|
||||
|
||||
ConnectionPoint tcp = this.channelWrapper.tcp();
|
||||
try {
|
||||
tcp.write(message);
|
||||
} catch (Exception e) {
|
||||
logger.error("Unable to write TCP object {}", message.getClass(), e);
|
||||
}
|
||||
return tcp;
|
||||
}
|
||||
else {
|
||||
logger.debug("writing TCP while closed: {}", message);
|
||||
|
||||
// we have to return something, otherwise dependent code will throw a null pointer exception
|
||||
return ChannelNull.get();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the object over the network using UDP (LOCAL channels do not care if its TCP or UDP)
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
ConnectionPoint UDP(Object message) {
|
||||
if (!closeInProgress.get()) {
|
||||
logger.trace("Sending UDP {}", message);
|
||||
|
||||
ConnectionPoint udp = this.channelWrapper.udp();
|
||||
try {
|
||||
udp.write(message);
|
||||
} catch (Exception e) {
|
||||
logger.error("Unable to write UDP object {}", message.getClass(), e);
|
||||
}
|
||||
return udp;
|
||||
}
|
||||
else {
|
||||
logger.debug("writing UDP while closed: {}", message);
|
||||
// we have to return something, otherwise dependent code will throw a null pointer exception
|
||||
return ChannelNull.get();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param context can be NULL when running deferred messages from registration process.
|
||||
* @param message the received message
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
void channelRead(ChannelHandlerContext context, Object message) throws Exception {
|
||||
channelRead(message);
|
||||
ReferenceCountUtil.release(message);
|
||||
}
|
||||
|
||||
private
|
||||
void channelRead(Object object) {
|
||||
// prevent close from occurring SMACK in the middle of a message in progress.
|
||||
// delay close until it's finished.
|
||||
this.messageInProgress.set(true);
|
||||
|
||||
// will auto-flush if necessary
|
||||
this.sessionManager.onMessage(this, object);
|
||||
|
||||
this.messageInProgress.set(false);
|
||||
|
||||
// if we are in the middle of closing, and waiting for the message, it's safe to notify it to continue.
|
||||
if (this.closeInProgress.get()) {
|
||||
synchronized (this.messageInProgressLock) {
|
||||
this.messageInProgressLock.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
// in some cases, we want to close the current connection -- and given the way the system is designed, we cannot always close it before
|
||||
// we return. This will let us close the connection when our business logic is finished.
|
||||
// if (closeAsap) {
|
||||
// close();
|
||||
// }
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void channelInactive(ChannelHandlerContext context) throws Exception {
|
||||
// if we are in the middle of a message, hold off.
|
||||
if (this.messageInProgress.get()) {
|
||||
synchronized (this.messageInProgressLock) {
|
||||
try {
|
||||
this.messageInProgressLock.wait();
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Channel channel = context.channel();
|
||||
Class<? extends Channel> channelClass = channel.getClass();
|
||||
|
||||
boolean isTCP = isTcpChannel(channelClass);
|
||||
boolean isUDP = false;
|
||||
boolean isLocal = isLocalChannel(channelClass);
|
||||
|
||||
if (this.logger.isInfoEnabled()) {
|
||||
String type;
|
||||
|
||||
if (isTCP) {
|
||||
type = "TCP";
|
||||
}
|
||||
else {
|
||||
isUDP = isUdpChannel(channelClass);
|
||||
if (isUDP) {
|
||||
type = "UDP";
|
||||
}
|
||||
else if (isLocal) {
|
||||
type = "LOCAL";
|
||||
}
|
||||
else {
|
||||
type = "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.info("Closed {} connection [{}]",
|
||||
type,
|
||||
EndPoint.getHostDetails(channel.remoteAddress()));
|
||||
}
|
||||
|
||||
// TODO: tell the remote endpoint that it needs to close (via a message, which might get there...).
|
||||
|
||||
|
||||
if (this.endPoint instanceof EndPointClient) {
|
||||
((EndPointClient) this.endPoint).abortRegistration();
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Only close if we are:
|
||||
* - local (mutually exclusive to TCP/UDP)
|
||||
* - TCP (and TCP+UDP)
|
||||
* - UDP (and not part of TCP+UDP)
|
||||
*
|
||||
* DO NOT call close if we are:
|
||||
* - UDP (part of TCP+UDP)
|
||||
*/
|
||||
if (isLocal ||
|
||||
isTCP ||
|
||||
(isUDP && this.channelWrapper.tcp() == null)) {
|
||||
|
||||
// we can get to this point in two ways. We only want this to happen once
|
||||
// - remote endpoint disconnects (and so closes us)
|
||||
// - local endpoint calls close(), and netty will call this.
|
||||
|
||||
|
||||
// this must happen first, because client.close() depends on it!
|
||||
// onDisconnected() must happen last.
|
||||
boolean doClose = channelIsClosed.compareAndSet(false, true);
|
||||
|
||||
if (!closeInProgress.get()) {
|
||||
if (endPoint instanceof EndPointClient) {
|
||||
// client closes single connection
|
||||
((Client) endPoint).close();
|
||||
} else {
|
||||
// server only closes this connection.
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
if (doClose) {
|
||||
// this is because channelInactive can ONLY happen when netty shuts down the channel.
|
||||
// and connection.close() can be called by the user.
|
||||
// will auto-flush if necessary
|
||||
this.sessionManager.onDisconnected(this);
|
||||
}
|
||||
}
|
||||
|
||||
closeLatch.countDown();
|
||||
}
|
||||
|
||||
final void
|
||||
forceClose() {
|
||||
this.channelWrapper.close(this, this.sessionManager, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the connection, but does not remove any listeners
|
||||
*/
|
||||
@Override
|
||||
public final
|
||||
void close() {
|
||||
close(true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* we can get to this point in two ways. We only want this to happen once
|
||||
* - remote endpoint disconnects (and so netty calls us)
|
||||
* - local endpoint calls close() directly
|
||||
*
|
||||
* NOTE: If we remove all listeners and we are the client, then we remove ALL logic from the client!
|
||||
*/
|
||||
final
|
||||
void close(final boolean keepListeners) {
|
||||
// if we are in the same thread as netty, run in a new thread to prevent deadlocks with messageInProgress
|
||||
// if (!this.closeInProgress.get() && this.messageInProgress.get() && Shutdownable.isNettyThread()) {
|
||||
// Shutdownable.runNewThread("Close connection Thread", new Runnable() {
|
||||
// @Override
|
||||
// public
|
||||
// void run() {
|
||||
// close(keepListeners);
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// return;
|
||||
// }
|
||||
|
||||
|
||||
// only close if we aren't already in the middle of closing.
|
||||
if (this.closeInProgress.compareAndSet(false, true)) {
|
||||
int idleTimeoutMs = 2000;
|
||||
|
||||
// if we are in the middle of a message, hold off.
|
||||
synchronized (this.messageInProgressLock) {
|
||||
// while loop is to prevent spurious wakeups!
|
||||
while (this.messageInProgress.get()) {
|
||||
try {
|
||||
this.messageInProgressLock.wait(idleTimeoutMs);
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// close out the ping future
|
||||
PingFuture pingFuture2 = this.pingFuture;
|
||||
if (pingFuture2 != null) {
|
||||
pingFuture2.cancel();
|
||||
}
|
||||
this.pingFuture = null;
|
||||
|
||||
|
||||
|
||||
synchronized (this.channelIsClosed) {
|
||||
if (!this.channelIsClosed.get()) {
|
||||
// this will have netty call "channelInactive()"
|
||||
this.channelWrapper.close(this, this.sessionManager, false);
|
||||
|
||||
// want to wait for the "channelInactive()" method to FINISH ALL TYPES before allowing our current thread to continue!
|
||||
try {
|
||||
closeLatch.await(idleTimeoutMs, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove all listeners AFTER we close the channel.
|
||||
if (!keepListeners) {
|
||||
removeAll();
|
||||
}
|
||||
|
||||
|
||||
// remove all RMI listeners
|
||||
rmiSupport.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void exceptionCaught(ChannelHandlerContext context, Throwable cause) throws Exception {
|
||||
final Channel channel = context.channel();
|
||||
|
||||
if (!(cause instanceof IOException)) {
|
||||
// safe to ignore, since it's thrown when we try to interact with a closed socket. Race conditions cause this, and
|
||||
// it is still safe to ignore.
|
||||
this.logger.error("Unexpected exception while receiving data from {}", channel.remoteAddress(), cause);
|
||||
|
||||
// the ONLY sockets that can call this are:
|
||||
// CLIENT TCP or UDP
|
||||
// SERVER TCP
|
||||
|
||||
if (channel.isOpen()) {
|
||||
channel.close();
|
||||
}
|
||||
} else {
|
||||
// it's an IOException, just log it!
|
||||
this.logger.error("Unexpected exception while communicating with {}!", channel.remoteAddress(), cause);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose methods to modify the connection listeners.
|
||||
*/
|
||||
@Override
|
||||
public final
|
||||
Listeners listeners() {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener to this connection/endpoint to be notified of
|
||||
* connect/disconnect/idle/receive(object) events.
|
||||
* <p/>
|
||||
* If the listener already exists, it is not added again.
|
||||
* <p/>
|
||||
* When called by a server, NORMALLY listeners are added at the GLOBAL level
|
||||
* (meaning, I add one listener, and ALL connections are notified of that
|
||||
* listener.
|
||||
* <p/>
|
||||
* It is POSSIBLE to add a server connection ONLY (ie, not global) listener
|
||||
* (via connection.addListener), meaning that ONLY that listener attached to
|
||||
* the connection is notified on that event (ie, admin type listeners)
|
||||
*/
|
||||
@Override
|
||||
public final
|
||||
Listeners add(Listener listener) {
|
||||
if (this.endPoint instanceof EndPointServer) {
|
||||
// when we are a server, NORMALLY listeners are added at the GLOBAL level
|
||||
// meaning --
|
||||
// I add one listener, and ALL connections are notified of that listener.
|
||||
//
|
||||
// HOWEVER, it is also POSSIBLE to add a local listener (via connection.addListener), meaning that ONLY
|
||||
// that listener is notified on that event (ie, admin type listeners)
|
||||
|
||||
// synchronized because this should be VERY uncommon, and we want to make sure that when the manager
|
||||
// is empty, we can remove it from this connection.
|
||||
synchronized (this) {
|
||||
if (this.localListenerManager == null) {
|
||||
this.localListenerManager = ((EndPointServer) this.endPoint).addListenerManager(this);
|
||||
}
|
||||
this.localListenerManager.add(listener);
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
this.endPoint.listeners()
|
||||
.add(listener);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a listener from this connection/endpoint to NO LONGER be notified
|
||||
* of connect/disconnect/idle/receive(object) events.
|
||||
* <p/>
|
||||
* When called by a server, NORMALLY listeners are added at the GLOBAL level
|
||||
* (meaning, I add one listener, and ALL connections are notified of that
|
||||
* listener.
|
||||
* <p/>
|
||||
* It is POSSIBLE to remove a server-connection 'non-global' listener (via
|
||||
* connection.removeListener), meaning that ONLY that listener attached to
|
||||
* the connection is removed
|
||||
*/
|
||||
@Override
|
||||
public final
|
||||
Listeners remove(Listener listener) {
|
||||
if (this.endPoint instanceof EndPointServer) {
|
||||
// when we are a server, NORMALLY listeners are added at the GLOBAL level
|
||||
// meaning --
|
||||
// I add one listener, and ALL connections are notified of that listener.
|
||||
//
|
||||
// HOWEVER, it is also POSSIBLE to add a local listener (via connection.addListener), meaning that ONLY
|
||||
// that listener is notified on that event (ie, admin type listeners)
|
||||
|
||||
// synchronized because this should be uncommon, and we want to make sure that when the manager
|
||||
// is empty, we can remove it from this connection.
|
||||
synchronized (this) {
|
||||
if (this.localListenerManager != null) {
|
||||
this.localListenerManager.remove(listener);
|
||||
|
||||
if (!this.localListenerManager.hasListeners()) {
|
||||
((EndPointServer) this.endPoint).removeListenerManager(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.endPoint.listeners()
|
||||
.remove(listener);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all registered listeners from this connection/endpoint to NO
|
||||
* LONGER be notified of connect/disconnect/idle/receive(object) events.
|
||||
*
|
||||
* This includes all proxy listeners
|
||||
*/
|
||||
@Override
|
||||
public final
|
||||
Listeners removeAll() {
|
||||
rmiSupport.removeAllListeners();
|
||||
|
||||
if (this.endPoint instanceof EndPointServer) {
|
||||
// when we are a server, NORMALLY listeners are added at the GLOBAL level
|
||||
// meaning --
|
||||
// I add one listener, and ALL connections are notified of that listener.
|
||||
//
|
||||
// HOWEVER, it is also POSSIBLE to add a local listener (via connection.addListener), meaning that ONLY
|
||||
// that listener is notified on that event (ie, admin type listeners)
|
||||
|
||||
// synchronized because this should be uncommon, and we want to make sure that when the manager
|
||||
// is empty, we can remove it from this connection.
|
||||
synchronized (this) {
|
||||
if (this.localListenerManager != null) {
|
||||
this.localListenerManager.removeAll();
|
||||
this.localListenerManager = null;
|
||||
|
||||
((EndPointServer) this.endPoint).removeListenerManager(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.endPoint.listeners()
|
||||
.removeAll();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all registered listeners (of the object type) from this connection/endpoint to NO LONGER be notified of
|
||||
* connect/disconnect/idle/receive(object) events.
|
||||
*/
|
||||
@Override
|
||||
public final
|
||||
Listeners removeAll(Class<?> classType) {
|
||||
if (this.endPoint instanceof EndPointServer) {
|
||||
// when we are a server, NORMALLY listeners are added at the GLOBAL level
|
||||
// meaning --
|
||||
// I add one listener, and ALL connections are notified of that listener.
|
||||
//
|
||||
// HOWEVER, it is also POSSIBLE to add a local listener (via connection.addListener), meaning that ONLY
|
||||
// that listener is notified on that event (ie, admin type listeners)
|
||||
|
||||
// synchronized because this should be uncommon, and we want to make sure that when the manager
|
||||
// is empty, we can remove it from this connection.
|
||||
synchronized (this) {
|
||||
if (this.localListenerManager != null) {
|
||||
this.localListenerManager.removeAll(classType);
|
||||
|
||||
if (!this.localListenerManager.hasListeners()) {
|
||||
this.localListenerManager = null;
|
||||
((EndPointServer) this.endPoint).removeListenerManager(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.endPoint.listeners()
|
||||
.removeAll(classType);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
String toString() {
|
||||
return this.channelWrapper.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
int hashCode() {
|
||||
return id();
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ConnectionImpl other = (ConnectionImpl) obj;
|
||||
if (this.channelWrapper == null) {
|
||||
if (other.channelWrapper != null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (!this.channelWrapper.equals(other.channelWrapper)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
//
|
||||
// RMI methods
|
||||
//
|
||||
//
|
||||
|
||||
@Override
|
||||
public
|
||||
ConnectionRmiSupport rmiSupport() {
|
||||
return rmiSupport;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final
|
||||
<Iface> void createRemoteObject(final Class<Iface> interfaceClass, final RemoteObjectCallback<Iface> callback) {
|
||||
rmiSupport.createRemoteObject(this, interfaceClass, callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final
|
||||
<Iface> void getRemoteObject(final int objectId, final RemoteObjectCallback<Iface> callback) {
|
||||
rmiSupport.getRemoteObject(this, objectId, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages the RMI stuff for a connection.
|
||||
*
|
||||
* @return true if there was RMI stuff done, false if the message was "normal" and nothing was done
|
||||
*/
|
||||
boolean manageRmi(final Object message) {
|
||||
return rmiSupport.manage(this, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Objects that are on the "local" in-jvm connection have fixup their objects. For "network" connections, this is automatically done.
|
||||
*/
|
||||
Object fixupRmi(final Object message) {
|
||||
// "local RMI" objects have to be modified, this part does that
|
||||
return rmiSupport.fixupRmi(message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,614 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection
|
||||
|
||||
import dorkbox.network.NetworkUtil
|
||||
import dorkbox.network.Server
|
||||
import dorkbox.network.connection.ping.PingFuture
|
||||
import dorkbox.network.connection.ping.PingMessage
|
||||
import dorkbox.network.rmi.ConnectionRmiSupport
|
||||
import dorkbox.network.rmi.RemoteObjectCallback
|
||||
import io.aeron.FragmentAssembler
|
||||
import io.aeron.Publication
|
||||
import io.aeron.Subscription
|
||||
import io.aeron.logbuffer.FragmentHandler
|
||||
import io.aeron.logbuffer.Header
|
||||
import kotlinx.atomicfu.atomic
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.agrona.BitUtil
|
||||
import org.agrona.BufferUtil
|
||||
import org.agrona.DirectBuffer
|
||||
import org.agrona.concurrent.UnsafeBuffer
|
||||
import org.slf4j.Logger
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import javax.crypto.SecretKey
|
||||
|
||||
/**
|
||||
* The "network connection" is established once the registration is validated for TCP/UDP
|
||||
*/
|
||||
open class ConnectionImpl(val endPoint: EndPoint<*>, mediaDriverConnection: MediaDriverConnection)
|
||||
: Connection_, Listeners<Connection> {
|
||||
|
||||
private val subscription: Subscription
|
||||
private val publication: Publication
|
||||
|
||||
final override val subscriptionPort: Int
|
||||
final override val publicationPort: Int
|
||||
final override val remoteAddressInt: Int
|
||||
final override val remoteAddress: String
|
||||
final override val streamId: Int
|
||||
final override val sessionId: Int
|
||||
|
||||
|
||||
|
||||
val expirationTime = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(endPoint.config.connectionCleanupTimeoutInSeconds.toLong())
|
||||
|
||||
private val logger: Logger = endPoint.logger
|
||||
|
||||
// private val needsLock = AtomicBoolean(false)
|
||||
// private val writeSignalNeeded = AtomicBoolean(false)
|
||||
// private val writeLock = Any()
|
||||
// private val closeInProgress = AtomicBoolean(false)
|
||||
private val isClosed = atomic(false)
|
||||
// private val channelIsClosed = AtomicBoolean(false)
|
||||
// private val messageInProgressLock = Any()
|
||||
// private val messageInProgress = AtomicBoolean(false)
|
||||
|
||||
|
||||
@Volatile
|
||||
private var pingFuture: PingFuture? = null
|
||||
|
||||
// used to store connection local listeners (instead of global listeners). Only possible on the server.
|
||||
@Volatile
|
||||
private var localListenerManager: ConnectionManager<*>? = null
|
||||
|
||||
// while on the CLIENT, if the SERVER's ecc key has changed, the client will abort and show an error.
|
||||
private var remoteKeyChanged = false
|
||||
|
||||
// The IV for AES-GCM must be 12 bytes, since it's 4 (salt) + 8 (external counter) + 4 (GCM counter)
|
||||
// The 12 bytes IV is created during connection registration, and during the AES-GCM crypto, we override the last 8 with this
|
||||
// counter, which is also transmitted as an optimized int. (which is why it starts at 0, so the transmitted bytes are small)
|
||||
private val aes_gcm_iv = AtomicLong(0)
|
||||
|
||||
// when closing this connection, HOW MANY endpoints need to be closed?
|
||||
private var closeLatch: CountDownLatch? = null
|
||||
|
||||
// RMI support for this connection
|
||||
var rmiSupport: ConnectionRmiSupport
|
||||
|
||||
|
||||
var messageHandler: FragmentAssembler
|
||||
|
||||
val buffer = UnsafeBuffer(BufferUtil.allocateDirectAligned(10000, BitUtil.CACHE_LINE_LENGTH))
|
||||
|
||||
|
||||
init {
|
||||
// we have to construct how the connection will communicate!
|
||||
if (endPoint is Server) {
|
||||
mediaDriverConnection.buildServer(endPoint.aeron)
|
||||
} else {
|
||||
runBlocking {
|
||||
mediaDriverConnection.buildClient(endPoint.aeron)
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("creating new connection $mediaDriverConnection")
|
||||
|
||||
// can only get this AFTER we have built the sub/pub
|
||||
subscription = mediaDriverConnection.subscription
|
||||
publication = mediaDriverConnection.publication
|
||||
|
||||
subscriptionPort = mediaDriverConnection.subscriptionPort
|
||||
publicationPort = mediaDriverConnection.publicationPort
|
||||
remoteAddress = mediaDriverConnection.address
|
||||
remoteAddressInt = NetworkUtil.IP.toInt(remoteAddress)
|
||||
streamId = mediaDriverConnection.streamId
|
||||
sessionId = mediaDriverConnection.sessionId
|
||||
|
||||
messageHandler = FragmentAssembler(FragmentHandler { buffer: DirectBuffer, offset: Int, length: Int, header: Header ->
|
||||
// small problem... If we expect IN ORDER messages (ie: setting a value, then later reading the value), multiple threads
|
||||
// don't work.
|
||||
endPoint.actionDispatch.launch {
|
||||
endPoint.readMessage(buffer, offset, length, header, this@ConnectionImpl)
|
||||
}
|
||||
})
|
||||
|
||||
rmiSupport = ConnectionRmiSupport(endPoint.rmiGlobalBridge)
|
||||
|
||||
// when closing this connection, HOW MANY endpoints need to be closed?
|
||||
closeLatch = CountDownLatch(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param now The current time
|
||||
*
|
||||
* @return `true` if this duologue has no subscribers and the current
|
||||
* time `now` is after the intended expiry date of the duologue
|
||||
*/
|
||||
override fun isExpired(now: Long): Boolean {
|
||||
return subscription.imageCount() == 0 && now > expirationTime
|
||||
}
|
||||
|
||||
override fun pollSubscriptions(): Int {
|
||||
return subscription.poll(messageHandler, 1024)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the AES key. key=32 byte, iv=12 bytes (AES-GCM implementation).
|
||||
*/
|
||||
override fun cryptoKey(): SecretKey {
|
||||
TODO()
|
||||
// return channelWrapper.cryptoKey()
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the per-message sequence number.
|
||||
*
|
||||
* The IV for AES-GCM must be 12 bytes, since it's 4 (salt) + 4 (external counter) + 4 (GCM counter)
|
||||
* The 12 bytes IV is created during connection registration, and during the AES-GCM crypto, we override the last 8 with this
|
||||
* counter, which is also transmitted as an optimized int. (which is why it starts at 0, so the transmitted bytes are small)
|
||||
*/
|
||||
override fun nextGcmSequence(): Long {
|
||||
return aes_gcm_iv.getAndIncrement()
|
||||
}
|
||||
|
||||
/**
|
||||
* Has the remote ECC public key changed. This can be useful if specific actions are necessary when the key has changed.
|
||||
*/
|
||||
override fun hasRemoteKeyChanged(): Boolean {
|
||||
return remoteKeyChanged
|
||||
}
|
||||
|
||||
override val isLoopback: Boolean
|
||||
get() = TODO("Not yet implemented")
|
||||
override val isIPC: Boolean
|
||||
get() = TODO("Not yet implemented")
|
||||
override val isNetwork: Boolean
|
||||
get() = TODO("Not yet implemented")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Updates the ping times for this connection (called when this connection gets a REPLY ping message).
|
||||
*/
|
||||
fun updatePingResponse(ping: PingMessage?) {
|
||||
if (pingFuture != null) {
|
||||
pingFuture!!.setSuccess(this, ping)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a "ping" packet, trying UDP then TCP (in that order) to measure **ROUND TRIP** time to the remote connection.
|
||||
*
|
||||
* @return Ping can have a listener attached, which will get called when the ping returns.
|
||||
*/
|
||||
override suspend fun ping(): Ping {
|
||||
// val pingFuture2 = pingFuture
|
||||
// if (pingFuture2 != null && !pingFuture2.isSuccess) {
|
||||
// pingFuture2.cancel()
|
||||
// }
|
||||
// val newPromise: Promise<PingTuple<out Connection?>>
|
||||
// newPromise = if (channelWrapper.udp() != null) {
|
||||
// channelWrapper.udp()
|
||||
// .newPromise()
|
||||
// } else {
|
||||
// channelWrapper.tcp()
|
||||
// .newPromise()
|
||||
// }
|
||||
// pingFuture = PingFuture(newPromise)
|
||||
// val ping = PingMessage()
|
||||
// ping.id = pingFuture!!.id
|
||||
// ping0(ping)
|
||||
// return pingFuture!!
|
||||
TODO()
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL USE ONLY. Used to initiate a ping, and to return a ping.
|
||||
*
|
||||
* Sends a ping message attempted in the following order: UDP, TCP,LOCAL
|
||||
*/
|
||||
fun ping0(ping: PingMessage) {
|
||||
// if (channelWrapper.udp() != null) {
|
||||
// UDP(ping)
|
||||
// } else if (channelWrapper.tcp() != null) {
|
||||
// TCP(ping)
|
||||
// } else {
|
||||
// self(ping)
|
||||
// }
|
||||
TODO()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last calculated TCP return trip time, or -1 if or the [PingMessage] response has not yet been received.
|
||||
*/
|
||||
val lastRoundTripTime: Int
|
||||
get() {
|
||||
val pingFuture2 = pingFuture
|
||||
return pingFuture2?.response ?: -1
|
||||
}
|
||||
|
||||
/**
|
||||
* needed to place back-pressure when writing too much data to the connection.
|
||||
*
|
||||
* This blocks until we are writable again
|
||||
*/
|
||||
// TODO: remove this!?!? use idle backoff strategy?!?
|
||||
// fun controlBackPressure(c: ConnectionPoint) {
|
||||
// while (!closeInProgress.get() && !c.isWritable) {
|
||||
// needsLock.set(true)
|
||||
// writeSignalNeeded.set(true)
|
||||
// synchronized(writeLock) {
|
||||
// if (needsLock.get()) {
|
||||
// try {
|
||||
// // waits 1 second maximum per check. This is to guarantee that eventually (in the case of deadlocks, which i've seen)
|
||||
// // it will get released. The while loop makes sure it will exit when the channel is writable
|
||||
//// writeLock.wait(1000)
|
||||
// } catch (e: InterruptedException) {
|
||||
// e.printStackTrace()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Send the given message to the given publication. If the publication fails to accept the message, the method will retry `5` times,
|
||||
* waiting `100` milliseconds each time, before throwing an exception.
|
||||
*
|
||||
* @param pub The publication
|
||||
* @param buffer A buffer that will hold the message for sending
|
||||
* @param message The message
|
||||
*
|
||||
* @return The new publication stream position
|
||||
*
|
||||
* @throws IOException If the message cannot be sent
|
||||
*/
|
||||
|
||||
/**
|
||||
* Safely sends objects to a destination (such as a custom object or a standard ping).
|
||||
*/
|
||||
override suspend fun send(message: Any) {
|
||||
endPoint.writeMessage(publication, this, message)
|
||||
}
|
||||
|
||||
|
||||
override suspend fun send(message: Any, priority: Byte) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Closes the connection, and removes all connection specific listeners
|
||||
*/
|
||||
override fun close() {
|
||||
if (isClosed.compareAndSet(expect = false, update = true)) {
|
||||
subscription.close()
|
||||
publication.close()
|
||||
}
|
||||
|
||||
// only close if we aren't already in the middle of closing.
|
||||
// if (closeInProgress.compareAndSet(false, true)) {
|
||||
// val idleTimeoutMs = 2000
|
||||
//
|
||||
// // if we are in the middle of a message, hold off.
|
||||
//// synchronized(messageInProgressLock) {
|
||||
//// // while loop is to prevent spurious wakeups!
|
||||
//// while (messageInProgress.get()) {
|
||||
//// try {
|
||||
////// messageInProgressLock.wait(idleTimeoutMs.toLong())
|
||||
//// } catch (ignored: InterruptedException) {
|
||||
//// }
|
||||
//// }
|
||||
//// }
|
||||
//
|
||||
//
|
||||
// // close out the ping future
|
||||
// val pingFuture2 = pingFuture
|
||||
// pingFuture2?.cancel()
|
||||
// pingFuture = null
|
||||
//
|
||||
//// synchronized(channelIsClosed) {
|
||||
//// if (!channelIsClosed.get()) {
|
||||
//// // this will have netty call "channelInactive()"
|
||||
////// channelWrapper.close(this, sessionManager, false)
|
||||
////
|
||||
//// // want to wait for the "channelInactive()" method to FINISH ALL TYPES before allowing our current thread to continue!
|
||||
//// try {
|
||||
//// closeLatch!!.await(idleTimeoutMs.toLong(), TimeUnit.MILLISECONDS)
|
||||
//// } catch (ignored: InterruptedException) {
|
||||
//// }
|
||||
//// }
|
||||
//// }
|
||||
//
|
||||
// // remove all listeners AFTER we close the channel.
|
||||
// if (!keepListeners) {
|
||||
// removeAll()
|
||||
// }
|
||||
//
|
||||
//
|
||||
// // remove all RMI listeners
|
||||
// rmiSupport!!.close()
|
||||
// }
|
||||
|
||||
// remove all listeners AFTER we close the channel.
|
||||
// if (!keepListeners) {
|
||||
removeAll()
|
||||
// }
|
||||
|
||||
|
||||
// remove all RMI listeners
|
||||
rmiSupport.close()
|
||||
}
|
||||
|
||||
// @Throws(Exception::class)
|
||||
// override fun exceptionCaught(context: ChannelHandlerContext, cause: Throwable) {
|
||||
// val channel = context.channel()
|
||||
// if (cause !is IOException) {
|
||||
// // safe to ignore, since it's thrown when we try to interact with a closed socket. Race conditions cause this, and
|
||||
// // it is still safe to ignore.
|
||||
// logger!!.error("Unexpected exception while receiving data from {}", channel.remoteAddress(), cause)
|
||||
//
|
||||
// // the ONLY sockets that can call this are:
|
||||
// // CLIENT TCP or UDP
|
||||
// // SERVER TCP
|
||||
// if (channel.isOpen) {
|
||||
// channel.close()
|
||||
// }
|
||||
// } else {
|
||||
// // it's an IOException, just log it!
|
||||
// logger!!.error("Unexpected exception while communicating with {}!", channel.remoteAddress(), cause)
|
||||
// }
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Expose methods to modify the connection listeners.
|
||||
// */
|
||||
// override fun listeners(): Listeners<Connection> {
|
||||
// return this
|
||||
// }
|
||||
|
||||
/**
|
||||
* Adds a listener to this connection/endpoint to be notified of
|
||||
* connect/disconnect/idle/receive(object) events.
|
||||
*
|
||||
*
|
||||
* If the listener already exists, it is not added again.
|
||||
*
|
||||
*
|
||||
* When called by a server, NORMALLY listeners are added at the GLOBAL level
|
||||
* (meaning, I add one listener, and ALL connections are notified of that
|
||||
* listener.
|
||||
*
|
||||
*
|
||||
* It is POSSIBLE to add a server connection ONLY (ie, not global) listener
|
||||
* (via connection.addListener), meaning that ONLY that listener attached to
|
||||
* the connection is notified on that event (ie, admin type listeners)
|
||||
*/
|
||||
|
||||
|
||||
override fun filter(function: (Connection) -> Boolean): Listeners<Connection> {
|
||||
return this
|
||||
}
|
||||
|
||||
override fun onConnect(function: (Connection) -> Unit): Listeners<Connection> {
|
||||
return this
|
||||
}
|
||||
|
||||
override fun onDisconnect(function: (Connection) -> Unit): Listeners<Connection> {
|
||||
return this
|
||||
}
|
||||
|
||||
override fun onError(function: (Connection, throwable: Throwable) -> Unit): Listeners<Connection> {
|
||||
return this
|
||||
}
|
||||
|
||||
override fun <M : Any> onMessage(function: (Connection, M) -> Unit): Listeners<Connection> {
|
||||
return this
|
||||
}
|
||||
|
||||
// override fun add(listener: OnConnected<Connection>): Listeners<Connection> {
|
||||
// if (endPoint is EndPointServer) {
|
||||
// // when we are a server, NORMALLY listeners are added at the GLOBAL level
|
||||
// // meaning --
|
||||
// // I add one listener, and ALL connections are notified of that listener.
|
||||
// //
|
||||
// // HOWEVER, it is also POSSIBLE to add a local listener (via connection.addListener), meaning that ONLY
|
||||
// // that listener is notified on that event (ie, admin type listeners)
|
||||
//
|
||||
// // synchronized because this should be VERY uncommon, and we want to make sure that when the manager
|
||||
// // is empty, we can remove it from this connection.
|
||||
//// synchronized(this) {
|
||||
//// if (localListenerManager == null) {
|
||||
//// localListenerManager = endPoint.addListenerManager(this)
|
||||
//// }
|
||||
//// localListenerManager!!.add(listener)
|
||||
//// }
|
||||
// } else {
|
||||
//// endPoint.listeners()
|
||||
//// .add(listener)
|
||||
// }
|
||||
// return this
|
||||
// }
|
||||
|
||||
/**
|
||||
* Removes a listener from this connection/endpoint to NO LONGER be notified
|
||||
* of connect/disconnect/idle/receive(object) events.
|
||||
*
|
||||
*
|
||||
* When called by a server, NORMALLY listeners are added at the GLOBAL level
|
||||
* (meaning, I add one listener, and ALL connections are notified of that
|
||||
* listener.
|
||||
*
|
||||
*
|
||||
* It is POSSIBLE to remove a server-connection 'non-global' listener (via
|
||||
* connection.removeListener), meaning that ONLY that listener attached to
|
||||
* the connection is removed
|
||||
*/
|
||||
override fun remove(listener: OnConnected<Connection>): Listeners<Connection> {
|
||||
if (endPoint is Server) {
|
||||
// when we are a server, NORMALLY listeners are added at the GLOBAL level
|
||||
// meaning --
|
||||
// I add one listener, and ALL connections are notified of that listener.
|
||||
//
|
||||
// HOWEVER, it is also POSSIBLE to add a local listener (via connection.addListener), meaning that ONLY
|
||||
// that listener is notified on that event (ie, admin type listeners)
|
||||
|
||||
// synchronized because this should be uncommon, and we want to make sure that when the manager
|
||||
// is empty, we can remove it from this connection.
|
||||
// synchronized(this) {
|
||||
// val local = localListenerManager
|
||||
// if (local != null) {
|
||||
// local.remove(listener)
|
||||
// if (!local.hasListeners()) {
|
||||
// endPoint.removeListenerManager(this)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
} else {
|
||||
// endPoint.listeners()
|
||||
// .remove(listener)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all registered listeners from this connection/endpoint to NO
|
||||
* LONGER be notified of connect/disconnect/idle/receive(object) events.
|
||||
*
|
||||
* This includes all proxy listeners
|
||||
*/
|
||||
override fun removeAll(): Listeners<Connection> {
|
||||
rmiSupport.removeAllListeners()
|
||||
|
||||
if (endPoint is Server) {
|
||||
// when we are a server, NORMALLY listeners are added at the GLOBAL level
|
||||
// meaning --
|
||||
// I add one listener, and ALL connections are notified of that listener.
|
||||
//
|
||||
// HOWEVER, it is also POSSIBLE to add a local listener (via connection.addListener), meaning that ONLY
|
||||
// that listener is notified on that event (ie, admin type listeners)
|
||||
|
||||
// synchronized because this should be uncommon, and we want to make sure that when the manager
|
||||
// is empty, we can remove it from this connection.
|
||||
// synchronized(this) {
|
||||
// if (localListenerManager != null) {
|
||||
// localListenerManager?.removeAll()
|
||||
// localListenerManager = null
|
||||
// endPoint.removeListenerManager(this)
|
||||
// }
|
||||
// }
|
||||
} else {
|
||||
// endPoint.listeners()
|
||||
// .removeAll()
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all registered listeners (of the object type) from this connection/endpoint to NO LONGER be notified of
|
||||
* connect/disconnect/idle/receive(object) events.
|
||||
*/
|
||||
override fun removeAll(classType: Class<*>): Listeners<Connection> {
|
||||
if (endPoint is Server) {
|
||||
// when we are a server, NORMALLY listeners are added at the GLOBAL level
|
||||
// meaning --
|
||||
// I add one listener, and ALL connections are notified of that listener.
|
||||
//
|
||||
// HOWEVER, it is also POSSIBLE to add a local listener (via connection.addListener), meaning that ONLY
|
||||
// that listener is notified on that event (ie, admin type listeners)
|
||||
|
||||
// synchronized because this should be uncommon, and we want to make sure that when the manager
|
||||
// is empty, we can remove it from this connection.
|
||||
// synchronized(this) {
|
||||
// val local = localListenerManager
|
||||
// if (local != null) {
|
||||
// local.removeAll(classType)
|
||||
// if (!local.hasListeners()) {
|
||||
// localListenerManager = null
|
||||
// endPoint.removeListenerManager(this)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
} else {
|
||||
// endPoint.listeners()
|
||||
// .removeAll(classType)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
|
||||
override fun isClosed(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun endPoint(): EndPoint<*> {
|
||||
return endPoint
|
||||
}
|
||||
|
||||
|
||||
override fun toString(): String {
|
||||
// return channelWrapper.toString()
|
||||
return "TODO"
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return sessionId
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
if (other == null) {
|
||||
return false
|
||||
}
|
||||
if (javaClass != other.javaClass) {
|
||||
return false
|
||||
}
|
||||
// val other1 = other as ConnectionImpl
|
||||
// if (channelWrapper != other1.channelWrapper) {
|
||||
// return false
|
||||
// }
|
||||
return true
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
// RMI methods
|
||||
//
|
||||
//
|
||||
override fun rmiSupport(): ConnectionRmiSupport {
|
||||
return rmiSupport
|
||||
}
|
||||
|
||||
override suspend fun <Iface> createRemoteObject(interfaceClass: Class<Iface>, callback: RemoteObjectCallback<Iface>) {
|
||||
rmiSupport.createRemoteObject(this, interfaceClass, callback)
|
||||
}
|
||||
|
||||
override suspend fun <Iface> getRemoteObject(objectId: Int, callback: RemoteObjectCallback<Iface>) {
|
||||
rmiSupport.getRemoteObject(this, objectId, callback)
|
||||
}
|
||||
}
|
|
@ -1,784 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import com.esotericsoftware.kryo.util.IdentityMap;
|
||||
|
||||
import dorkbox.network.connection.Listener.OnConnected;
|
||||
import dorkbox.network.connection.bridge.ConnectionBridgeServer;
|
||||
import dorkbox.network.connection.bridge.ConnectionExceptSpecifiedBridgeServer;
|
||||
import dorkbox.network.connection.listenerManagement.OnConnectedManager;
|
||||
import dorkbox.network.connection.listenerManagement.OnDisconnectedManager;
|
||||
import dorkbox.network.connection.listenerManagement.OnMessageReceivedManager;
|
||||
import dorkbox.network.connection.ping.PingMessage;
|
||||
import dorkbox.util.Property;
|
||||
import dorkbox.util.collections.ConcurrentEntry;
|
||||
import dorkbox.util.generics.ClassHelper;
|
||||
import io.netty.util.concurrent.ImmediateEventExecutor;
|
||||
import io.netty.util.concurrent.Promise;
|
||||
import net.jodah.typetools.TypeResolver;
|
||||
|
||||
// .equals() compares the identity on purpose,this because we cannot create two separate objects that are somehow equal to each other.
|
||||
@SuppressWarnings("unchecked")
|
||||
public
|
||||
class ConnectionManager<C extends Connection> implements Listeners, ISessionManager, ConnectionPoint, ConnectionBridgeServer,
|
||||
ConnectionExceptSpecifiedBridgeServer {
|
||||
/**
|
||||
* Specifies the load-factor for the IdentityMap used to manage keeping track of the number of connections + listeners
|
||||
*/
|
||||
@Property
|
||||
public static final float LOAD_FACTOR = 0.8F;
|
||||
|
||||
// Recommended for best performance while adhering to the "single writer principle". Must be static-final
|
||||
private static final AtomicReferenceFieldUpdater<ConnectionManager, IdentityMap> localManagersREF = AtomicReferenceFieldUpdater.newUpdater(
|
||||
ConnectionManager.class,
|
||||
IdentityMap.class,
|
||||
"localManagers");
|
||||
|
||||
|
||||
|
||||
// Recommended for best performance while adhering to the "single writer principle". Must be static-final
|
||||
private static final AtomicReferenceFieldUpdater<ConnectionManager, ConcurrentEntry> connectionsREF = AtomicReferenceFieldUpdater.newUpdater(
|
||||
ConnectionManager.class,
|
||||
ConcurrentEntry.class,
|
||||
"connectionsHead");
|
||||
|
||||
|
||||
private final String loggerName;
|
||||
|
||||
private final OnConnectedManager<C> onConnectedManager;
|
||||
private final OnDisconnectedManager<C> onDisconnectedManager;
|
||||
private final OnMessageReceivedManager<C> onMessageReceivedManager;
|
||||
|
||||
@SuppressWarnings({"FieldCanBeLocal", "unused"})
|
||||
private volatile ConcurrentEntry<Connection> connectionsHead = null; // reference to the first element
|
||||
|
||||
// This is ONLY touched by a single thread, maintains a map of entries for FAST lookup during connection remove.
|
||||
private final IdentityMap<Connection, ConcurrentEntry> connectionEntries = new IdentityMap<Connection, ConcurrentEntry>(32, ConnectionManager.LOAD_FACTOR);
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private volatile IdentityMap<Connection, ConnectionManager> localManagers = new IdentityMap<Connection, ConnectionManager>(8, ConnectionManager.LOAD_FACTOR);
|
||||
|
||||
|
||||
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
|
||||
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
|
||||
// use-case 99% of the time)
|
||||
private final Object singleWriterConnectionsLock = new Object();
|
||||
private final Object singleWriterLocalManagerLock = new Object();
|
||||
|
||||
|
||||
/**
|
||||
* Used by the listener subsystem to determine types.
|
||||
*/
|
||||
private final Class<?> baseClass;
|
||||
protected final org.slf4j.Logger logger;
|
||||
private final AtomicBoolean hasAtLeastOneListener = new AtomicBoolean(false);
|
||||
final AtomicBoolean shutdown = new AtomicBoolean(false);
|
||||
|
||||
|
||||
ConnectionManager(final String loggerName, final Class<?> baseClass) {
|
||||
this.loggerName = loggerName;
|
||||
this.logger = org.slf4j.LoggerFactory.getLogger(loggerName);
|
||||
this.baseClass = baseClass;
|
||||
|
||||
onConnectedManager = new OnConnectedManager<C>(logger);
|
||||
onDisconnectedManager = new OnDisconnectedManager<C>(logger);
|
||||
onMessageReceivedManager = new OnMessageReceivedManager<C>(logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener to this connection/endpoint to be notified of connect/disconnect/idle/receive(object) events.
|
||||
* <p/>
|
||||
* When called by a server, NORMALLY listeners are added at the GLOBAL level (meaning, I add one listener, and ALL connections are
|
||||
* notified of that listener.
|
||||
* <p/>
|
||||
* It is POSSIBLE to add a server connection ONLY (ie, not global) listener (via connection.addListener), meaning that ONLY that
|
||||
* listener attached to the connection is notified on that event (ie, admin type listeners)
|
||||
*/
|
||||
@Override
|
||||
public final
|
||||
Listeners add(final Listener listener) {
|
||||
if (listener == null) {
|
||||
throw new IllegalArgumentException("listener cannot be null.");
|
||||
}
|
||||
|
||||
// this is the connection generic parameter for the listener, works for lambda expressions as well
|
||||
Class<?> genericClass = ClassHelper.getGenericParameterAsClassForSuperClass(Listener.class, listener.getClass(), 0);
|
||||
|
||||
// if we are null, it means that we have no generics specified for our listener!
|
||||
//noinspection IfStatementWithIdenticalBranches
|
||||
if (genericClass == this.baseClass || genericClass == TypeResolver.Unknown.class || genericClass == null) {
|
||||
// we are the base class, so we are fine.
|
||||
addListener0(listener);
|
||||
return this;
|
||||
|
||||
}
|
||||
else if (ClassHelper.hasInterface(Connection.class, genericClass) && !ClassHelper.hasParentClass(this.baseClass, genericClass)) {
|
||||
// now we must make sure that the PARENT class is NOT the base class. ONLY the base class is allowed!
|
||||
addListener0(listener);
|
||||
return this;
|
||||
}
|
||||
|
||||
// didn't successfully add the listener.
|
||||
throw new IllegalArgumentException("Unable to add incompatible connection type as a listener! : " + this.baseClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL USE ONLY
|
||||
*/
|
||||
private
|
||||
void addListener0(final Listener listener) {
|
||||
boolean found = false;
|
||||
if (listener instanceof Listener.OnConnected) {
|
||||
onConnectedManager.add((Listener.OnConnected) listener);
|
||||
found = true;
|
||||
}
|
||||
if (listener instanceof Listener.OnDisconnected) {
|
||||
onDisconnectedManager.add((Listener.OnDisconnected) listener);
|
||||
found = true;
|
||||
}
|
||||
|
||||
if (listener instanceof Listener.OnMessageReceived) {
|
||||
onMessageReceivedManager.add((Listener.OnMessageReceived) listener);
|
||||
found = true;
|
||||
}
|
||||
|
||||
if (found) {
|
||||
hasAtLeastOneListener.set(true);
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("listener added: {}",
|
||||
listener.getClass()
|
||||
.getName());
|
||||
}
|
||||
}
|
||||
else {
|
||||
logger.error("No matching listener types. Unable to add listener: {}",
|
||||
listener.getClass()
|
||||
.getName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a listener from this connection/endpoint to NO LONGER be notified of connect/disconnect/idle/receive(object) events.
|
||||
* <p/>
|
||||
* When called by a server, NORMALLY listeners are added at the GLOBAL level (meaning, I add one listener, and ALL connections are
|
||||
* notified of that listener.
|
||||
* <p/>
|
||||
* It is POSSIBLE to remove a server-connection 'non-global' listener (via connection.removeListener), meaning that ONLY that listener
|
||||
* attached to the connection is removed
|
||||
*/
|
||||
@Override
|
||||
public final
|
||||
Listeners remove(final Listener listener) {
|
||||
if (listener == null) {
|
||||
throw new IllegalArgumentException("listener cannot be null.");
|
||||
}
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("listener removed: {}",
|
||||
listener.getClass()
|
||||
.getName());
|
||||
}
|
||||
|
||||
boolean found = false;
|
||||
int remainingListeners = 0;
|
||||
|
||||
if (listener instanceof Listener.OnConnected) {
|
||||
int size = onConnectedManager.removeWithSize((OnConnected) listener);
|
||||
if (size >= 0) {
|
||||
remainingListeners += size;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if (listener instanceof Listener.OnDisconnected) {
|
||||
int size = onDisconnectedManager.removeWithSize((Listener.OnDisconnected) listener);
|
||||
if (size >= 0) {
|
||||
remainingListeners += size;
|
||||
found |= true;
|
||||
}
|
||||
}
|
||||
if (listener instanceof Listener.OnMessageReceived) {
|
||||
int size = onMessageReceivedManager.removeWithSize((Listener.OnMessageReceived) listener);
|
||||
if (size >= 0) {
|
||||
remainingListeners += size;
|
||||
found |= true;
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
if (remainingListeners == 0) {
|
||||
hasAtLeastOneListener.set(false);
|
||||
}
|
||||
}
|
||||
else {
|
||||
logger.error("No matching listener types. Unable to remove listener: {}",
|
||||
listener.getClass()
|
||||
.getName());
|
||||
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all registered listeners from this connection/endpoint to NO LONGER be notified of connect/disconnect/idle/receive(object)
|
||||
* events.
|
||||
*/
|
||||
@Override
|
||||
public final
|
||||
Listeners removeAll() {
|
||||
onConnectedManager.clear();
|
||||
onDisconnectedManager.clear();
|
||||
onMessageReceivedManager.clear();
|
||||
|
||||
logger.trace("ALL listeners removed !!");
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all registered listeners (of the object type) from this
|
||||
* connection/endpoint to NO LONGER be notified of
|
||||
* connect/disconnect/idle/receive(object) events.
|
||||
*/
|
||||
@Override
|
||||
public final
|
||||
Listeners removeAll(final Class<?> classType) {
|
||||
if (classType == null) {
|
||||
throw new IllegalArgumentException("classType cannot be null.");
|
||||
}
|
||||
|
||||
final Logger logger2 = this.logger;
|
||||
if (onMessageReceivedManager.removeAll(classType)) {
|
||||
if (logger2.isTraceEnabled()) {
|
||||
logger2.trace("All listeners removed for type: {}",
|
||||
classType.getClass()
|
||||
.getName());
|
||||
}
|
||||
} else {
|
||||
logger2.warn("No listeners found to remove for type: {}",
|
||||
classType.getClass()
|
||||
.getName());
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Invoked when a message object was received from a remote peer.
|
||||
* <p/>
|
||||
* If data is sent in response to this event, the connection data is automatically flushed to the wire.
|
||||
* <p/>
|
||||
* {@link ISessionManager}
|
||||
*/
|
||||
@Override
|
||||
public final
|
||||
void onMessage(final ConnectionImpl connection, final Object message) {
|
||||
// add the ping listener (internal use only!)
|
||||
Class<?> messageClass = message.getClass();
|
||||
if (messageClass == PingMessage.class) {
|
||||
PingMessage ping = (PingMessage) message;
|
||||
if (ping.isReply) {
|
||||
connection.updatePingResponse(ping);
|
||||
}
|
||||
else {
|
||||
// return the ping from whence it came
|
||||
ping.isReply = true;
|
||||
|
||||
connection.ping0(ping);
|
||||
}
|
||||
}
|
||||
|
||||
// add the UDP "close hint" to close remote connections (internal use only!)
|
||||
// else if (messageClass == DatagramCloseMessage.class) {
|
||||
// connection.forceClose();
|
||||
// }
|
||||
|
||||
else {
|
||||
notifyOnMessage0(connection, message, false);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("Duplicates")
|
||||
private
|
||||
boolean notifyOnMessage0(final ConnectionImpl connection, Object message, boolean foundListener) {
|
||||
if (connection.manageRmi(message)) {
|
||||
// if we are an RMI message/registration, we have very specific, defined behavior. We do not use the "normal" listener callback pattern
|
||||
// because these methods are rare, and require special functionality
|
||||
return true;
|
||||
}
|
||||
|
||||
message = connection.fixupRmi(message);
|
||||
|
||||
|
||||
foundListener |= onMessageReceivedManager.notifyReceived((C) connection, message, shutdown);
|
||||
|
||||
// now have to account for additional connection listener managers (non-global).
|
||||
// access a snapshot of the managers (single-writer-principle)
|
||||
final IdentityMap<Connection, ConnectionManager<C>> localManagers = localManagersREF.get(this);
|
||||
ConnectionManager<C> localManager = localManagers.get(connection);
|
||||
if (localManager != null) {
|
||||
// if we found a listener during THIS method call, we need to let the NEXT method call know,
|
||||
// so it doesn't spit out error for not handling a message (since that message MIGHT have
|
||||
// been found in this method).
|
||||
foundListener |= localManager.notifyOnMessage0(connection, message, foundListener);
|
||||
}
|
||||
|
||||
// only run a flush once
|
||||
if (!foundListener) {
|
||||
this.logger.warn("----------- LISTENER NOT REGISTERED FOR TYPE: {}",
|
||||
message.getClass()
|
||||
.getSimpleName());
|
||||
}
|
||||
return foundListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a Channel is open, bound to a local address, and connected to a remote address.
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
void onConnected(final ConnectionImpl connection) {
|
||||
// we add the connection in a different step!
|
||||
|
||||
boolean foundListener = onConnectedManager.notifyConnected((C) connection, shutdown);
|
||||
|
||||
// now have to account for additional (local) listener managers.
|
||||
// access a snapshot of the managers (single-writer-principle)
|
||||
final IdentityMap<Connection, ConnectionManager> localManagers = localManagersREF.get(this);
|
||||
ConnectionManager localManager = localManagers.get(connection);
|
||||
if (localManager != null) {
|
||||
localManager.onConnected(connection);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a Channel was disconnected from its remote peer.
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
void onDisconnected(final ConnectionImpl connection) {
|
||||
logger.trace("onDisconnected({})", connection.id());
|
||||
|
||||
boolean foundListener = onDisconnectedManager.notifyDisconnected((C) connection);
|
||||
|
||||
// now have to account for additional (local) listener managers.
|
||||
|
||||
// access a snapshot of the managers (single-writer-principle)
|
||||
final IdentityMap<Connection, ConnectionManager> localManagers = localManagersREF.get(this);
|
||||
ConnectionManager localManager = localManagers.get(connection);
|
||||
if (localManager != null) {
|
||||
localManager.onDisconnected(connection);
|
||||
|
||||
// remove myself from the "global" listeners so we can have our memory cleaned up.
|
||||
removeListenerManager(connection);
|
||||
}
|
||||
|
||||
removeConnection(connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a Channel is open, bound to a local address, and connected to a remote address.
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
void addConnection(ConnectionImpl connection) {
|
||||
addConnection0(connection);
|
||||
|
||||
// now have to account for additional (local) listener managers.
|
||||
// access a snapshot of the managers (single-writer-principle)
|
||||
final IdentityMap<Connection, ConnectionManager> localManagers = localManagersREF.get(this);
|
||||
ConnectionManager localManager = localManagers.get(connection);
|
||||
if (localManager != null) {
|
||||
localManager.addConnection(connection);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a custom connection to the server.
|
||||
* <p>
|
||||
* This should only be used in situations where there can be DIFFERENT types of connections (such as a 'web-based' connection) and
|
||||
* you want *this* server instance to manage listeners + message dispatch
|
||||
*
|
||||
* @param connection the connection to add
|
||||
*/
|
||||
void addConnection0(final Connection connection) {
|
||||
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
|
||||
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
|
||||
// use-case 99% of the time)
|
||||
synchronized (singleWriterConnectionsLock) {
|
||||
// access a snapshot of the connections (single-writer-principle)
|
||||
ConcurrentEntry head = connectionsREF.get(this);
|
||||
|
||||
if (!connectionEntries.containsKey(connection)) {
|
||||
head = new ConcurrentEntry<Object>(connection, head);
|
||||
|
||||
connectionEntries.put(connection, head);
|
||||
|
||||
// save this snapshot back to the original (single writer principle)
|
||||
connectionsREF.lazySet(this, head);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a custom connection to the server.
|
||||
* <p>
|
||||
* This should only be used in situations where there can be DIFFERENT types of connections (such as a 'web-based' connection) and
|
||||
* you want *this* server instance to manage listeners + message dispatch
|
||||
*
|
||||
* @param connection the connection to remove
|
||||
*/
|
||||
void removeConnection(Connection connection) {
|
||||
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
|
||||
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
|
||||
// use-case 99% of the time)
|
||||
synchronized (singleWriterConnectionsLock) {
|
||||
// access a snapshot of the connections (single-writer-principle)
|
||||
ConcurrentEntry concurrentEntry = connectionEntries.get(connection);
|
||||
|
||||
if (concurrentEntry != null) {
|
||||
ConcurrentEntry head1 = connectionsREF.get(this);
|
||||
|
||||
if (concurrentEntry == head1) {
|
||||
// if it was second, now it's first
|
||||
head1 = head1.next();
|
||||
//oldHead.clear(); // optimize for GC not possible because of potentially running iterators
|
||||
}
|
||||
else {
|
||||
concurrentEntry.remove();
|
||||
}
|
||||
|
||||
// save this snapshot back to the original (single writer principle)
|
||||
connectionsREF.lazySet(this, head1);
|
||||
this.connectionEntries.remove(connection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a non-modifiable list of active connections. This is extremely slow, and not recommended!
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
List<C> getConnections() {
|
||||
synchronized (singleWriterConnectionsLock) {
|
||||
final IdentityMap.Keys<Connection> keys = this.connectionEntries.keys();
|
||||
return (List<C>) keys.toArray();
|
||||
}
|
||||
}
|
||||
|
||||
final
|
||||
ConnectionManager addListenerManager(final Connection connection) {
|
||||
// when we are a server, NORMALLY listeners are added at the GLOBAL level (meaning, I add one listener, and ALL connections
|
||||
// are notified of that listener.
|
||||
// it is POSSIBLE to add a connection-specific listener (via connection.addListener), meaning that ONLY
|
||||
// that listener is notified on that event (ie, admin type listeners)
|
||||
|
||||
ConnectionManager manager;
|
||||
boolean created = false;
|
||||
|
||||
|
||||
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
|
||||
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
|
||||
// use-case 99% of the time)
|
||||
synchronized (singleWriterLocalManagerLock) {
|
||||
// access a snapshot of the managers (single-writer-principle)
|
||||
final IdentityMap<Connection, ConnectionManager> localManagers = localManagersREF.get(this);
|
||||
|
||||
manager = localManagers.get(connection);
|
||||
if (manager == null) {
|
||||
created = true;
|
||||
manager = new ConnectionManager(loggerName + "-" + connection.toString() + " Specific", ConnectionManager.this.baseClass);
|
||||
localManagers.put(connection, manager);
|
||||
|
||||
// save this snapshot back to the original (single writer principle)
|
||||
localManagersREF.lazySet(this, localManagers);
|
||||
}
|
||||
}
|
||||
|
||||
if (created) {
|
||||
Logger logger2 = this.logger;
|
||||
if (logger2.isTraceEnabled()) {
|
||||
logger2.trace("Connection specific Listener Manager added for connection: {}", connection);
|
||||
}
|
||||
}
|
||||
|
||||
return manager;
|
||||
}
|
||||
|
||||
final
|
||||
void removeListenerManager(final Connection connection) {
|
||||
boolean wasRemoved = false;
|
||||
|
||||
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
|
||||
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
|
||||
// use-case 99% of the time)
|
||||
synchronized (singleWriterLocalManagerLock) {
|
||||
// access a snapshot of the managers (single-writer-principle)
|
||||
final IdentityMap<Connection, ConnectionManager> localManagers = localManagersREF.get(this);
|
||||
|
||||
final ConnectionManager removed = localManagers.remove(connection);
|
||||
if (removed != null) {
|
||||
wasRemoved = true;
|
||||
|
||||
// save this snapshot back to the original (single writer principle)
|
||||
localManagersREF.lazySet(this, localManagers);
|
||||
}
|
||||
}
|
||||
|
||||
if (wasRemoved) {
|
||||
Logger logger2 = this.logger;
|
||||
if (logger2.isTraceEnabled()) {
|
||||
logger2.trace("Connection specific Listener Manager removed for connection: {}", connection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* BE CAREFUL! Only for internal use!
|
||||
*
|
||||
* @return a boolean indicating if there are any listeners registered with this manager.
|
||||
*/
|
||||
final
|
||||
boolean hasListeners() {
|
||||
return hasAtLeastOneListener.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes all associated resources/threads/connections
|
||||
*/
|
||||
final
|
||||
void stop() {
|
||||
this.shutdown.set(true);
|
||||
|
||||
// disconnect the sessions
|
||||
closeConnections(false);
|
||||
|
||||
onConnectedManager.clear();
|
||||
onDisconnectedManager.clear();
|
||||
onMessageReceivedManager.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close all connections ONLY.
|
||||
*
|
||||
* Only keep the listeners for connections IF we are the client. If we remove listeners as a client, ALL of the client logic will
|
||||
* be lost. The server is reactive, so listeners are added to connections as needed (instead of before startup, which is what the client does).
|
||||
*/
|
||||
final
|
||||
void closeConnections(boolean keepListeners) {
|
||||
LinkedList<ConnectionImpl> closeConnections = new LinkedList<ConnectionImpl>();
|
||||
|
||||
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
|
||||
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
|
||||
// use-case 99% of the time)
|
||||
synchronized (singleWriterConnectionsLock) {
|
||||
// don't need anything fast or fancy here, because this method will only be called once
|
||||
final IdentityMap.Keys<Connection> keys = connectionEntries.keys();
|
||||
for (Connection connection : keys) {
|
||||
// Close the connection. Make sure the close operation ends because
|
||||
// all I/O operations are asynchronous in Netty.
|
||||
// Also necessary otherwise workers won't close.
|
||||
if (connection instanceof ConnectionImpl) {
|
||||
closeConnections.add((ConnectionImpl) connection);
|
||||
}
|
||||
}
|
||||
|
||||
this.connectionEntries.clear();
|
||||
this.connectionsHead = null;
|
||||
}
|
||||
|
||||
// must be outside of the synchronize, otherwise we can potentially deadlock
|
||||
for (ConnectionImpl connection : closeConnections) {
|
||||
connection.close(keepListeners);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Not implemented, since this would cause horrendous problems.
|
||||
*
|
||||
* @see dorkbox.network.connection.ConnectionPoint#isWritable()
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
boolean isWritable() {
|
||||
throw new UnsupportedOperationException("Method not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void write(final Object object) {
|
||||
throw new UnsupportedOperationException("Method not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposes methods to send the object to all server connections (except the specified one) over the network. (or via LOCAL when it's a
|
||||
* local channel).
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
ConnectionExceptSpecifiedBridgeServer except() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
<V> Promise<V> newPromise() {
|
||||
return ImmediateEventExecutor.INSTANCE.newPromise();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the object to all server connections (except the specified one) over the network using TCP. (or via LOCAL when it's a local
|
||||
* channel).
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
ConnectionPoint TCP(final Connection connection, final Object message) {
|
||||
ConcurrentEntry<Connection> current = connectionsREF.get(this);
|
||||
Connection c;
|
||||
while (current != null) {
|
||||
c = current.getValue();
|
||||
current = current.next();
|
||||
|
||||
if (c != connection) {
|
||||
// c.send()
|
||||
// .TCP(message);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the object to all server connections (except the specified one) over the network using UDP (or via LOCAL when it's a local
|
||||
* channel).
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
ConnectionPoint UDP(final Connection connection, final Object message) {
|
||||
ConcurrentEntry<Connection> current = connectionsREF.get(this);
|
||||
Connection c;
|
||||
while (current != null) {
|
||||
c = current.getValue();
|
||||
current = current.next();
|
||||
|
||||
if (c != connection) {
|
||||
// c.send()
|
||||
// .UDP(message);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the message to other listeners INSIDE this endpoint for EVERY connection. It does not send it to a remote address.
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
ConnectionPoint self(final Object message) {
|
||||
ConcurrentEntry<ConnectionImpl> current = connectionsREF.get(this);
|
||||
ConnectionImpl c;
|
||||
while (current != null) {
|
||||
c = current.getValue();
|
||||
current = current.next();
|
||||
|
||||
onMessage(c, message);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the object all server connections over the network using TCP. (or via LOCAL when it's a local channel).
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
ConnectionPoint TCP(final Object message) {
|
||||
ConcurrentEntry<Connection> current = connectionsREF.get(this);
|
||||
Connection c;
|
||||
while (current != null) {
|
||||
c = current.getValue();
|
||||
current = current.next();
|
||||
|
||||
// c.send()
|
||||
// .TCP(message);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the object all server connections over the network using UDP. (or via LOCAL when it's a local channel).
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
ConnectionPoint UDP(final Object message) {
|
||||
ConcurrentEntry<Connection> current = connectionsREF.get(this);
|
||||
Connection c;
|
||||
while (current != null) {
|
||||
c = current.getValue();
|
||||
current = current.next();
|
||||
|
||||
// c.send()
|
||||
// .UDP(message);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely sends objects to a destination (such as a custom object or a standard ping). This will automatically choose which protocol
|
||||
* is available to use. If you want specify the protocol, use {@link ConnectionManager#TCP(Object)}, etc.
|
||||
* <p>
|
||||
* By default, this will try in the following order:
|
||||
* - TCP (if available)
|
||||
* - UDP (if available)
|
||||
* - LOCAL
|
||||
*/
|
||||
protected
|
||||
ConnectionPoint send(final Object message) {
|
||||
ConcurrentEntry<Connection> current = connectionsREF.get(this);
|
||||
Connection c;
|
||||
while (current != null) {
|
||||
c = current.getValue();
|
||||
current = current.next();
|
||||
|
||||
c.send(message);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
boolean equals(final Object o) {
|
||||
return this == o;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
int hashCode() {
|
||||
return 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,666 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection
|
||||
|
||||
import dorkbox.network.Configuration
|
||||
import dorkbox.util.Property
|
||||
import dorkbox.util.classes.ClassHelper
|
||||
import dorkbox.util.classes.ClassHierarchy
|
||||
import dorkbox.util.collections.IdentityMap
|
||||
import kotlinx.atomicfu.atomic
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import net.jodah.typetools.TypeResolver
|
||||
import org.slf4j.Logger
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock
|
||||
import kotlin.concurrent.read
|
||||
import kotlin.concurrent.write
|
||||
|
||||
// Because all of our callbacks are in response to network communication, and there CANNOT be CPU race conditions over a network...
|
||||
// we specifically use atomic references to set/get all of the callbacks. This ensures that these objects are visible when accessed
|
||||
// from different coroutines (because, ultimately, we want to use multiple threads on the box for processing data, and if we use
|
||||
// coroutines, we can ensure maximum thread output)
|
||||
|
||||
// .equals() compares the identity on purpose,this because we cannot create two separate objects that are somehow equal to each other.
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
open class ConnectionManager<C : Connection>(val logger: Logger, val config: Configuration) : AutoCloseable {
|
||||
// used to keep a cache of class hierarchy for distributing messages
|
||||
private val classHierarchyCache = ClassHierarchy(LOAD_FACTOR)
|
||||
|
||||
// initialize an emtpy array
|
||||
private val onConnectFilterList = atomic(Array<(suspend (C) -> Boolean)>(0) { { true } })
|
||||
private val onConnectFilterMutex = Mutex()
|
||||
|
||||
private val onConnectList = atomic(Array<suspend ((C) -> Unit)>(0) { { } })
|
||||
private val onConnectMutex = Mutex()
|
||||
|
||||
private val onDisconnectList = atomic(Array<suspend (C) -> Unit>(0) { { } })
|
||||
private val onDisconnectMutex = Mutex()
|
||||
|
||||
private val onErrorList = atomic(Array<suspend (C, Throwable) -> Unit>(0) { { _, _ -> } })
|
||||
private val onErrorMutex = Mutex()
|
||||
|
||||
private val onErrorGlobalList = atomic(Array<suspend (Throwable) -> Unit>(0) { { _ -> } })
|
||||
private val onErrorGlobalMutex = Mutex()
|
||||
|
||||
private val onMessageMap = atomic(IdentityMap<Class<*>, Array<suspend (C, Any) -> Unit>>(32, LOAD_FACTOR))
|
||||
private val onMessageMutex = Mutex()
|
||||
|
||||
|
||||
private val connectionLock = ReentrantReadWriteLock()
|
||||
private val connections = CopyOnWriteArrayList<C>()
|
||||
|
||||
private val localManagers = IdentityMap<C, ConnectionManager<C>>(8, LOAD_FACTOR)
|
||||
|
||||
private val shutdown = AtomicBoolean(false)
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Specifies the load-factor for the IdentityMap used to manage keeping track of the number of connections + listeners
|
||||
*/
|
||||
@Property
|
||||
val LOAD_FACTOR = 0.8f
|
||||
}
|
||||
|
||||
private inline fun <reified T> add(thing: T, array: Array<T>): Array<T> {
|
||||
val currentLength: Int = array.size
|
||||
|
||||
// add the new subscription to the array
|
||||
val newMessageArray = array.copyOf(currentLength + 1) as Array<T>
|
||||
newMessageArray[currentLength] = thing
|
||||
|
||||
return newMessageArray
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds a function that will be called BEFORE a client/server "connects" with
|
||||
* each other, and used to determine if a connection should be allowed
|
||||
* <p>
|
||||
* If the function returns TRUE, then the connection will continue to connect.
|
||||
* If the function returns FALSE, then the other end of the connection will
|
||||
* receive a connection error
|
||||
* <p>
|
||||
* For a server, this function will be called for ALL clients.
|
||||
*/
|
||||
suspend fun filter(function: suspend (C) -> Boolean) {
|
||||
onConnectFilterMutex.withLock {
|
||||
// we have to follow the single-writer principle!
|
||||
onConnectFilterList.lazySet(add(function, onConnectFilterList.value))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a function that will be called when a client/server "connects" with each other
|
||||
* <p>
|
||||
* For a server, this function will be called for ALL clients.
|
||||
*/
|
||||
suspend fun onConnect(function: suspend (C) -> Unit) {
|
||||
onConnectMutex.withLock {
|
||||
// we have to follow the single-writer principle!
|
||||
onConnectList.lazySet(add(function, onConnectList.value))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the remote end is no longer connected.
|
||||
* <p>
|
||||
* Do not try to send messages! The connection will already be closed, resulting in an error if you attempt to do so.
|
||||
*/
|
||||
suspend fun onDisconnect(function: suspend (C) -> Unit) {
|
||||
onDisconnectMutex.withLock {
|
||||
// we have to follow the single-writer principle!
|
||||
onDisconnectList.lazySet(add(function, onDisconnectList.value))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when there is an error for a specific connection
|
||||
* <p>
|
||||
* The error is also sent to an error log before this method is called.
|
||||
*/
|
||||
suspend fun onError(function: suspend (C, throwable: Throwable) -> Unit) {
|
||||
onErrorMutex.withLock {
|
||||
// we have to follow the single-writer principle!
|
||||
onErrorList.lazySet(add(function, onErrorList.value))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when there is an error in general
|
||||
* <p>
|
||||
* The error is also sent to an error log before this method is called.
|
||||
*/
|
||||
suspend fun onError(function: suspend (throwable: Throwable) -> Unit) {
|
||||
onErrorGlobalMutex.withLock {
|
||||
// we have to follow the single-writer principle!
|
||||
onErrorGlobalList.lazySet(add(function, onErrorGlobalList.value))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an object has been received from the remote end of the connection.
|
||||
* <p>
|
||||
* This method should not block for long periods as other network activity will not be processed until it returns.
|
||||
*/
|
||||
suspend fun <M : Any> onMessage(function: suspend (C, M) -> Unit) {
|
||||
onMessageMutex.withLock {
|
||||
// we have to follow the single-writer principle!
|
||||
|
||||
// this is the connection generic parameter for the listener, works for lambda expressions as well
|
||||
val connectionClass = ClassHelper.getGenericParameterAsClassForSuperClass(Connection::class.java, function.javaClass, 0)
|
||||
var messageClass = ClassHelper.getGenericParameterAsClassForSuperClass(Object::class.java, function.javaClass, 1)
|
||||
|
||||
var success = false
|
||||
|
||||
if (ClassHelper.hasInterface(Connection::class.java, connectionClass)) {
|
||||
// our connection class has "connection" as an interface.
|
||||
success = true
|
||||
}
|
||||
|
||||
// if we are null, it means that we have no generics specified for our listener, so it accepts everything!
|
||||
else if (messageClass == null || messageClass == TypeResolver.Unknown::class.java) {
|
||||
messageClass = Object::class.java
|
||||
success = true
|
||||
}
|
||||
|
||||
if (success) {
|
||||
// NOTE: https://github.com/Kotlin/kotlinx.atomicfu
|
||||
// this is EXPLICITLY listed as a "Don't" via the documentation. The ****ONLY**** reason this is actually OK is because
|
||||
// we are following the "single-writer principle", so only ONE THREAD can modify this at a time.
|
||||
val tempMap = onMessageMap.value
|
||||
|
||||
val func = function as suspend (C, Any) -> Unit
|
||||
|
||||
val newMessageArray: Array<suspend (C, Any) -> Unit>
|
||||
val onMessageArray: Array<suspend (C, Any) -> Unit>? = tempMap.get(messageClass)
|
||||
|
||||
if (onMessageArray != null) {
|
||||
newMessageArray = add(function, onMessageArray)
|
||||
} else {
|
||||
@Suppress("RemoveExplicitTypeArguments")
|
||||
newMessageArray = Array<suspend (C, Any) -> Unit>(1) { { _, _ -> } }
|
||||
newMessageArray[0] = func
|
||||
}
|
||||
|
||||
tempMap.put(messageClass, newMessageArray)
|
||||
onMessageMap.lazySet(tempMap)
|
||||
} else {
|
||||
throw IllegalArgumentException("Unable to add incompatible types! Detected connection/message classes: $connectionClass, $messageClass")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Invoked just after a connection is created, but before it is connected.
|
||||
*
|
||||
* @return true if the connection will be allowed to connect. False if we should terminate this connection
|
||||
*/
|
||||
suspend fun notifyFilter(connection: C): Boolean {
|
||||
onConnectFilterList.value.forEach {
|
||||
if (!it(connection)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a connection is connected to a remote address.
|
||||
*/
|
||||
suspend fun notifyConnect(connection: C) {
|
||||
onConnectList.value.forEach {
|
||||
it(connection)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a connection is disconnected to a remote address.
|
||||
*/
|
||||
suspend fun notifyDisconnect(connection: C) {
|
||||
onDisconnectList.value.forEach {
|
||||
it(connection)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when there is an error for a specific connection
|
||||
* <p>
|
||||
* The error is also sent to an error log before notifying callbacks
|
||||
*/
|
||||
suspend fun notifyError(connection: C, exception: Throwable) {
|
||||
logger.error("Error on connection: $connection", exception)
|
||||
onErrorList.value.forEach {
|
||||
it(connection, exception)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when there is an error in general
|
||||
* <p>
|
||||
* The error is also sent to an error log before notifying callbacks
|
||||
*/
|
||||
suspend fun notifyError(exception: Throwable) {
|
||||
logger.error("General error", exception)
|
||||
onErrorGlobalList.value.forEach {
|
||||
it(exception)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Invoked when a message object was received from a remote peer.
|
||||
*/
|
||||
suspend fun notifyOnMessage(connection: Connection_, message: Any) {
|
||||
connection as C // note: this is necessary because of how connection calls it!
|
||||
|
||||
val messageClass: Class<*> = message.javaClass
|
||||
|
||||
// have to save the types + hierarchy (note: duplicates are OK, since they will just be overwritten)
|
||||
// this is used to 'pre-populate' the cache, so additional lookups are fast
|
||||
val hierarchy = classHierarchyCache.getClassAndSuperClasses(messageClass)
|
||||
|
||||
// we march through the class hierarchy for this message, and call ALL callback that are registered
|
||||
// NOTICE: we CALL ALL TYPES -- meaning, if we have Object->Foo->Bar
|
||||
// we have registered for 'Object' and 'Foo'
|
||||
// we will call Foo (from this code)
|
||||
// we will ALSO call Object (since we called Foo).
|
||||
|
||||
|
||||
// NOTE: https://github.com/Kotlin/kotlinx.atomicfu
|
||||
// this is EXPLICITLY listed as a "Don't" via the documentation. The ****ONLY**** reason this is actually OK is because
|
||||
// we are following the "single-writer principle", so only ONE THREAD can modify this at a time.
|
||||
|
||||
// cache the lookup (because we don't care about race conditions, since the object hierarchy will be ALREADY established at this
|
||||
// exact moment
|
||||
val tempMap = onMessageMap.value
|
||||
var hasListeners = false
|
||||
hierarchy.forEach { clazz ->
|
||||
val onMessageArray: Array<suspend (C, Any) -> Unit>? = tempMap.get(clazz)
|
||||
if (onMessageArray != null) {
|
||||
hasListeners = true
|
||||
|
||||
onMessageArray.forEach { func ->
|
||||
func(connection, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// foundListener |= onMessageReceivedManager.notifyReceived((C) connection, message, shutdown);
|
||||
|
||||
// now have to account for additional connection listener managers (non-global).
|
||||
// access a snapshot of the managers (single-writer-principle)
|
||||
// val localManager = localManagers[connection as C]
|
||||
// if (localManager != null) {
|
||||
// // if we found a listener during THIS method call, we need to let the NEXT method call know,
|
||||
// // so it doesn't spit out error for not handling a message (since that message MIGHT have
|
||||
// // been found in this method).
|
||||
// foundListener = foundListener or localManager.notifyOnMessage0(connection, message, foundListener)
|
||||
// }
|
||||
|
||||
if (!hasListeners) {
|
||||
logger.error("----------- MESSAGE CALLBACK NOT REGISTERED FOR {}", messageClass.simpleName)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// override fun remove(listener: OnConnected<C>): Listeners<C> {
|
||||
// return this
|
||||
// }
|
||||
|
||||
|
||||
// /**
|
||||
// * Adds a listener to this connection/endpoint to be notified of connect/disconnect/idle/receive(object) events.
|
||||
// * <p/>
|
||||
// * When called by a server, NORMALLY listeners are added at the GLOBAL level (meaning, I add one listener, and ALL connections are
|
||||
// * notified of that listener.
|
||||
// * <p/>
|
||||
// * It is POSSIBLE to add a server connection ONLY (ie, not global) listener (via connection.addListener), meaning that ONLY that
|
||||
// * listener attached to the connection is notified on that event (ie, admin type listeners)
|
||||
// *
|
||||
// *
|
||||
// * // TODO: When converting to kotlin, use reified! to get the listener types
|
||||
// * https://kotlinlang.org/docs/reference/inline-functions.html
|
||||
// */
|
||||
// @Override
|
||||
// public final
|
||||
// Listeners add(final Listener listener) {
|
||||
// if (listener == null) {
|
||||
// throw new IllegalArgumentException("listener cannot be null.");
|
||||
// }
|
||||
//
|
||||
// // this is the connection generic parameter for the listener, works for lambda expressions as well
|
||||
// Class<?> genericClass = ClassHelper.getGenericParameterAsClassForSuperClass(Listener.class, listener.getClass(), 0);
|
||||
//
|
||||
// // if we are null, it means that we have no generics specified for our listener!
|
||||
// if (genericClass == this.baseClass || genericClass == TypeResolver.Unknown.class || genericClass == null) {
|
||||
// // we are the base class, so we are fine.
|
||||
// addListener0(listener);
|
||||
// return this;
|
||||
//
|
||||
// }
|
||||
// else if (ClassHelper.hasInterface(Connection.class, genericClass) && !ClassHelper.hasParentClass(this.baseClass, genericClass)) {
|
||||
// // now we must make sure that the PARENT class is NOT the base class. ONLY the base class is allowed!
|
||||
// addListener0(listener);
|
||||
// return this;
|
||||
// }
|
||||
//
|
||||
// // didn't successfully add the listener.
|
||||
// throw new IllegalArgumentException("Unable to add incompatible connection type as a listener! : " + this.baseClass);
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * INTERNAL USE ONLY
|
||||
// */
|
||||
// private
|
||||
// void addListener0(final Listener listener) {
|
||||
// boolean found = false;
|
||||
// if (listener instanceof OnConnected) {
|
||||
// onConnectedManager.add((Listener.OnConnected<C>) listener);
|
||||
// found = true;
|
||||
// }
|
||||
// if (listener instanceof Listener.OnDisconnected) {
|
||||
// onDisconnectedManager.add((Listener.OnDisconnected<C>) listener);
|
||||
// found = true;
|
||||
// }
|
||||
//
|
||||
// if (listener instanceof Listener.OnMessageReceived) {
|
||||
// onMessageReceivedManager.add((Listener.OnMessageReceived) listener);
|
||||
// found = true;
|
||||
// }
|
||||
//
|
||||
// if (found) {
|
||||
// hasAtLeastOneListener.set(true);
|
||||
//
|
||||
// if (logger.isTraceEnabled()) {
|
||||
// logger.trace("listener added: {}",
|
||||
// listener.getClass()
|
||||
// .getName());
|
||||
// }
|
||||
// }
|
||||
// else {
|
||||
// logger.error("No matching listener types. Unable to add listener: {}",
|
||||
// listener.getClass()
|
||||
// .getName());
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Removes a listener from this connection/endpoint to NO LONGER be notified of connect/disconnect/idle/receive(object) events.
|
||||
// * <p/>
|
||||
// * When called by a server, NORMALLY listeners are added at the GLOBAL level (meaning, I add one listener, and ALL connections are
|
||||
// * notified of that listener.
|
||||
// * <p/>
|
||||
// * It is POSSIBLE to remove a server-connection 'non-global' listener (via connection.removeListener), meaning that ONLY that listener
|
||||
// * attached to the connection is removed
|
||||
// */
|
||||
// @Override
|
||||
// public final
|
||||
// Listeners remove(final Listener listener) {
|
||||
// if (listener == null) {
|
||||
// throw new IllegalArgumentException("listener cannot be null.");
|
||||
// }
|
||||
//
|
||||
// if (logger.isTraceEnabled()) {
|
||||
// logger.trace("listener removed: {}",
|
||||
// listener.getClass()
|
||||
// .getName());
|
||||
// }
|
||||
//
|
||||
// boolean found = false;
|
||||
// int remainingListeners = 0;
|
||||
//
|
||||
// if (listener instanceof Listener.OnConnected) {
|
||||
// int size = onConnectedManager.removeWithSize((OnConnected<C>) listener);
|
||||
// if (size >= 0) {
|
||||
// remainingListeners += size;
|
||||
// found = true;
|
||||
// }
|
||||
// }
|
||||
// if (listener instanceof Listener.OnDisconnected) {
|
||||
// int size = onDisconnectedManager.removeWithSize((Listener.OnDisconnected<C>) listener);
|
||||
// if (size >= 0) {
|
||||
// remainingListeners += size;
|
||||
// found |= true;
|
||||
// }
|
||||
// }
|
||||
// if (listener instanceof Listener.OnMessageReceived) {
|
||||
// int size = onMessageReceivedManager.removeWithSize((Listener.OnMessageReceived) listener);
|
||||
// if (size >= 0) {
|
||||
// remainingListeners += size;
|
||||
// found |= true;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if (found) {
|
||||
// if (remainingListeners == 0) {
|
||||
// hasAtLeastOneListener.set(false);
|
||||
// }
|
||||
// }
|
||||
// else {
|
||||
// logger.error("No matching listener types. Unable to remove listener: {}",
|
||||
// listener.getClass()
|
||||
// .getName());
|
||||
//
|
||||
// }
|
||||
//
|
||||
// return this;
|
||||
// }
|
||||
// /**
|
||||
// * Removes all registered listeners from this connection/endpoint to NO LONGER be notified of connect/disconnect/idle/receive(object)
|
||||
// * events.
|
||||
// */
|
||||
// override fun removeAll(): Listeners<C> {
|
||||
// // onConnectedManager.clear();
|
||||
// // onDisconnectedManager.clear();
|
||||
// // onMessageReceivedManager.clear();
|
||||
// logger.error("ALL listeners removed !!")
|
||||
// return this
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Removes all registered listeners (of the object type) from this
|
||||
// * connection/endpoint to NO LONGER be notified of
|
||||
// * connect/disconnect/idle/receive(object) events.
|
||||
// */
|
||||
// override fun removeAll(classType: Class<*>): Listeners<C> {
|
||||
// val logger2 = logger
|
||||
// // if (onMessageReceivedManager.removeAll(classType)) {
|
||||
// // if (logger2.isTraceEnabled()) {
|
||||
// // logger2.trace("All listeners removed for type: {}",
|
||||
// // classType.getClass()
|
||||
// // .getName());
|
||||
// // }
|
||||
// // } else {
|
||||
// // logger2.warn("No listeners found to remove for type: {}",
|
||||
// // classType.getClass()
|
||||
// // .getName());
|
||||
// // }
|
||||
// return this
|
||||
// }
|
||||
|
||||
|
||||
/**
|
||||
* Invoked when aeron successfully connects to a remote address.
|
||||
*
|
||||
* @param connection the connection to add
|
||||
*/
|
||||
fun addConnection(connection: C) {
|
||||
connectionLock.write {
|
||||
connections.add(connection)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a custom connection to the server.
|
||||
*
|
||||
*
|
||||
* This should only be used in situations where there can be DIFFERENT types of connections (such as a 'web-based' connection) and
|
||||
* you want *this* server instance to manage listeners + message dispatch
|
||||
*
|
||||
* @param connection the connection to remove
|
||||
*/
|
||||
fun removeConnection(connection: C) {
|
||||
connectionLock.write {
|
||||
connections.remove(connection)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an action on each connection in the list inside a read lock
|
||||
*/
|
||||
suspend fun forEachConnectionDoRead(function: suspend (connection: C) -> Unit) {
|
||||
connectionLock.read {
|
||||
connections.forEach {
|
||||
function(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an action on each connection in the list.
|
||||
*/
|
||||
private val connectionsToRemove = mutableListOf<C>()
|
||||
internal suspend fun forEachConnectionCleanup(function: suspend (connection: C) -> Boolean, cleanup: suspend (connection: C) -> Unit) {
|
||||
connectionLock.write {
|
||||
connections.forEach {
|
||||
if (function(it)) {
|
||||
try {
|
||||
it.close()
|
||||
} finally {
|
||||
connectionsToRemove.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (connectionsToRemove.size > 0) {
|
||||
connectionsToRemove.forEach {
|
||||
cleanup(it)
|
||||
}
|
||||
connectionsToRemove.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun connectionCount(): Int {
|
||||
return connections.size
|
||||
}
|
||||
|
||||
|
||||
// fun addListenerManager(connection: C): ConnectionManager<C> {
|
||||
// // when we are a server, NORMALLY listeners are added at the GLOBAL level (meaning, I add one listener, and ALL connections
|
||||
// // are notified of that listener.
|
||||
//
|
||||
// // it is POSSIBLE to add a connection-specific listener (via connection.addListener), meaning that ONLY
|
||||
// // that listener is notified on that event (ie, admin type listeners)
|
||||
// var created = false
|
||||
// var manager = localManagers[connection]
|
||||
// if (manager == null) {
|
||||
// created = true
|
||||
// manager = ConnectionManager<C>("$loggerName-$connection Specific", actionDispatchScope)
|
||||
// localManagers.put(connection, manager)
|
||||
// }
|
||||
// if (created) {
|
||||
// val logger2 = logger
|
||||
// if (logger2.isTraceEnabled) {
|
||||
// logger2.trace("Connection specific Listener Manager added for connection: {}", connection)
|
||||
// }
|
||||
// }
|
||||
// return manager
|
||||
// }
|
||||
|
||||
// fun removeListenerManager(connection: C) {
|
||||
// var wasRemoved = false
|
||||
// val removed = localManagers.remove(connection)
|
||||
// if (removed != null) {
|
||||
// wasRemoved = true
|
||||
// }
|
||||
// if (wasRemoved) {
|
||||
// val logger2 = logger
|
||||
// if (logger2.isTraceEnabled) {
|
||||
// logger2.trace("Connection specific Listener Manager removed for connection: {}", connection)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Closes all associated resources/threads/connections
|
||||
*/
|
||||
override fun close() {
|
||||
connectionLock.write {
|
||||
// runBlocking because we don't want to progress until we are 100% done closing all connections
|
||||
runBlocking {
|
||||
// don't need anything fast or fancy here, because this method will only be called once
|
||||
connections.forEach {
|
||||
it.close()
|
||||
}
|
||||
|
||||
connections.forEach {
|
||||
notifyDisconnect(it)
|
||||
}
|
||||
|
||||
connections.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Exposes methods to send the object to all server connections (except the specified one) over the network. (or via LOCAL when it's a
|
||||
* local channel).
|
||||
*/
|
||||
// @Override
|
||||
// public
|
||||
// ConnectionExceptSpecifiedBridgeServer except() {
|
||||
// return this;
|
||||
// }
|
||||
// /**
|
||||
// * Sends the message to other listeners INSIDE this endpoint for EVERY connection. It does not send it to a remote address.
|
||||
// */
|
||||
// @Override
|
||||
// public
|
||||
// ConnectionPoint self(final Object message) {
|
||||
// ConcurrentEntry<ConnectionImpl> current = connectionsREF.get(this);
|
||||
// ConnectionImpl c;
|
||||
// while (current != null) {
|
||||
// c = current.getValue();
|
||||
// current = current.next();
|
||||
//
|
||||
// onMessage(c, message);
|
||||
// }
|
||||
// return this;
|
||||
// }
|
||||
|
||||
/**
|
||||
* Safely sends objects to a destination (such as a custom object or a standard ping). This will automatically choose which protocol
|
||||
* is available to use.
|
||||
*/
|
||||
suspend fun send(message: Any) {
|
||||
// TODO: USE AERON add.dataPublisher thingy, so it's areon pusing messages out (way, WAY faster than if we are to iterate over
|
||||
// the connections
|
||||
// for (connection in connections) {
|
||||
// connection.send(message)
|
||||
// }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
package dorkbox.network.connection
|
||||
|
||||
import dorkbox.network.Configuration
|
||||
import dorkbox.network.aeron.client.ClientRejectedException
|
||||
import dorkbox.network.aeron.client.ClientTimedOutException
|
||||
import dorkbox.network.connection.registration.Registration
|
||||
import io.aeron.FragmentAssembler
|
||||
import io.aeron.logbuffer.FragmentHandler
|
||||
import io.aeron.logbuffer.Header
|
||||
import kotlinx.coroutines.launch
|
||||
import org.agrona.DirectBuffer
|
||||
import org.slf4j.Logger
|
||||
import java.security.SecureRandom
|
||||
|
||||
class ConnectionManagerClient<C : Connection>(logger: Logger, config: Configuration) : ConnectionManager<C>(logger, config) {
|
||||
// a one-time key for connecting
|
||||
private val oneTimePad = SecureRandom().nextInt()
|
||||
|
||||
|
||||
@Volatile
|
||||
var connectionInfo: ClientConnectionInfo? = null
|
||||
|
||||
private var failed = false
|
||||
|
||||
var sessionId: Int = 0
|
||||
|
||||
@Throws(ClientTimedOutException::class, ClientRejectedException::class)
|
||||
suspend fun initHandshake(mediaConnection: MediaDriverConnection, connectionTimeoutMS: Long, endPoint: EndPoint<C>) : ClientConnectionInfo {
|
||||
// now we have a bi-directional connection with the server on the handshake socket.
|
||||
val handler: FragmentHandler = FragmentAssembler(FragmentHandler { buffer: DirectBuffer, offset: Int, length: Int, header: Header ->
|
||||
endPoint.actionDispatch.launch {
|
||||
val message = endPoint.readHandshakeMessage(buffer, offset, length, header)
|
||||
logger.debug("[{}] response: {}", sessionId, message)
|
||||
|
||||
// it must be a registration message
|
||||
if (message !is Registration) {
|
||||
logger.error("[{}] server returned unrecognized message: {}", sessionId, message)
|
||||
return@launch
|
||||
}
|
||||
|
||||
// it must be the correct state
|
||||
if (message.state != Registration.HELLO_ACK) {
|
||||
logger.error("[{}] ignored message that is not HELLO_ACK", sessionId)
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (message.sessionId != sessionId) {
|
||||
logger.error("[{}] ignored message intended for another client", sessionId)
|
||||
return@launch
|
||||
}
|
||||
|
||||
// The message was intended for this client. Try to parse it as one of the available message types.
|
||||
connectionInfo = ClientConnectionInfo(
|
||||
subscriptionPort = message.publicationPort,
|
||||
publicationPort = message.subscriptionPort,
|
||||
sessionId = oneTimePad xor message.oneTimePad,
|
||||
streamId = oneTimePad xor message.streamId,
|
||||
publicKey = message.publicKey!!)
|
||||
|
||||
connectionInfo!!.log(sessionId, logger)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
val registrationMessage = Registration.hello(oneTimePad, config.settingsStore.getPublicKey()!!)
|
||||
|
||||
|
||||
// Send the one-time pad to the server.
|
||||
endPoint.writeMessage(mediaConnection.publication, registrationMessage)
|
||||
sessionId = mediaConnection.publication.sessionId()
|
||||
|
||||
|
||||
logger.debug("waiting for response")
|
||||
|
||||
// block until we receive the connection information from the server
|
||||
|
||||
var pollCount: Int
|
||||
val subscription = mediaConnection.subscription
|
||||
val pollIdleStrategy = endPoint.config.pollIdleStrategy
|
||||
|
||||
val startTime = System.currentTimeMillis()
|
||||
while (System.currentTimeMillis() - startTime < connectionTimeoutMS) {
|
||||
pollCount = subscription.poll(handler, 1024)
|
||||
|
||||
if (failed) {
|
||||
// no longer necessary to hold this connection open
|
||||
mediaConnection.close()
|
||||
throw ClientRejectedException("Server rejected this client")
|
||||
}
|
||||
|
||||
if (connectionInfo != null) {
|
||||
// no longer necessary to hold this connection open
|
||||
mediaConnection.close()
|
||||
break
|
||||
}
|
||||
|
||||
// 0 means we idle. >0 means reset and don't idle (because there are likely more)
|
||||
pollIdleStrategy.idle(pollCount)
|
||||
}
|
||||
|
||||
if (connectionInfo == null) {
|
||||
// no longer necessary to hold this connection open
|
||||
mediaConnection.close()
|
||||
throw ClientTimedOutException("Waiting for registration response from server")
|
||||
}
|
||||
|
||||
return connectionInfo!!
|
||||
}
|
||||
}
|
|
@ -0,0 +1,263 @@
|
|||
package dorkbox.network.connection
|
||||
|
||||
import dorkbox.network.NetworkUtil
|
||||
import dorkbox.network.ServerConfiguration
|
||||
import dorkbox.network.aeron.client.ClientRejectedException
|
||||
import dorkbox.network.aeron.server.AllocationException
|
||||
import dorkbox.network.aeron.server.PortAllocator
|
||||
import dorkbox.network.aeron.server.RandomIdAllocator
|
||||
import dorkbox.network.aeron.server.ServerException
|
||||
import dorkbox.network.connection.registration.Registration
|
||||
import io.aeron.Image
|
||||
import io.aeron.Publication
|
||||
import io.aeron.logbuffer.Header
|
||||
import org.agrona.DirectBuffer
|
||||
import org.agrona.collections.Int2IntCounterMap
|
||||
import org.slf4j.Logger
|
||||
|
||||
/**
|
||||
* TODO: when adding a "custom" connection, it's super important to not have to worry about the sessionID (which is what we key off of)
|
||||
*
|
||||
* @throws IllegalArgumentException If the port range is not valid
|
||||
*/
|
||||
class ConnectionManagerServer<C : Connection>(logger: Logger,
|
||||
config: ServerConfiguration) : ConnectionManager<C>(logger, config) {
|
||||
|
||||
companion object {
|
||||
// this is the number of ports used per client. Depending on how a client is configured, this number can change
|
||||
const val portsPerClient = 2
|
||||
}
|
||||
|
||||
private val portAllocator: PortAllocator
|
||||
private val connectionsPerIpCounts = Int2IntCounterMap(0)
|
||||
|
||||
// guarantee that session/stream ID's will ALWAYS be unique! (there can NEVER be a collision!)
|
||||
private val sessionIdAllocator = RandomIdAllocator(EndPoint.RESERVED_SESSION_ID_LOW, EndPoint.RESERVED_SESSION_ID_HIGH)
|
||||
private val streamIdAllocator = RandomIdAllocator(1, Integer.MAX_VALUE)
|
||||
|
||||
init {
|
||||
val minPort = config.clientStartPort
|
||||
val maxPortCount = portsPerClient * config.maxClientCount
|
||||
portAllocator = PortAllocator(minPort, maxPortCount)
|
||||
}
|
||||
|
||||
|
||||
@Throws(ServerException::class)
|
||||
suspend fun receiveHandshakeMessageServer(handshakePublication: Publication,
|
||||
buffer: DirectBuffer, offset: Int, length: Int, header: Header,
|
||||
endPoint: EndPoint<C>) {
|
||||
|
||||
// The sessionId is unique within a Subscription and unique across all Publication's from a sourceIdentity.
|
||||
val sessionId = header.sessionId()
|
||||
|
||||
// note: this address will ALWAYS be an IP:PORT combo
|
||||
val remoteIpAndPort = (header.context() as Image).sourceIdentity()
|
||||
|
||||
try {
|
||||
// split
|
||||
val splitPoint = remoteIpAndPort.lastIndexOf(':')
|
||||
val clientAddressString = remoteIpAndPort.substring(0, splitPoint)
|
||||
// val port = remoteIpAndPort.substring(splitPoint+1)
|
||||
|
||||
val clientAddress = NetworkUtil.IP.toInt(clientAddressString)
|
||||
|
||||
// TODO: notify error if there is an exceptoin!
|
||||
val message = endPoint.readHandshakeMessage(buffer, offset, length, header)
|
||||
logger.debug("[{}] received: {}", sessionId, message)
|
||||
|
||||
config as ServerConfiguration
|
||||
|
||||
// VALIDATE:: a Registration object is the only acceptable message during the connection phase
|
||||
if (message !is Registration) {
|
||||
endPoint.writeMessage(handshakePublication, Registration.error("Invalid connection request"))
|
||||
return
|
||||
}
|
||||
|
||||
// VALIDATE:: Check to see if there are already too many clients connected.
|
||||
if (connectionCount() >= config.maxClientCount) {
|
||||
logger.debug("server is full")
|
||||
endPoint.writeMessage(handshakePublication, Registration.error("server full. Max allowed is ${config.maxClientCount}"))
|
||||
return
|
||||
}
|
||||
|
||||
// VALIDATE:: check to see if the remote connection's public key has changed!
|
||||
if (!endPoint.validateRemoteAddress(clientAddress, message.publicKey)) {
|
||||
// TODO: this should provide info to a callback
|
||||
println("connection not allowed! public key mismatch")
|
||||
return
|
||||
}
|
||||
|
||||
// VALIDATE TODO: make sure the serialization matches between the client/server!
|
||||
|
||||
|
||||
// VALIDATE:: we are now connected to the client and are going to create a new connection.
|
||||
val currentCountForIp = connectionsPerIpCounts.getAndIncrement(clientAddress)
|
||||
if (currentCountForIp >= config.maxConnectionsPerIpAddress) {
|
||||
// decrement it now, since we aren't going to permit this connection (take the hit on failure, instead
|
||||
connectionsPerIpCounts.getAndDecrement(clientAddress)
|
||||
|
||||
logger.debug("too many connections for IP address")
|
||||
endPoint.writeMessage(handshakePublication, Registration.error("too many connections for IP address. Max allowed is ${config.maxConnectionsPerIpAddress}"))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// VALIDATE:: TODO: ?? check to see if this session is ALREADY connected??. It should not be!
|
||||
|
||||
|
||||
/////
|
||||
/////
|
||||
///// DONE WITH VALIDATION
|
||||
/////
|
||||
/////
|
||||
|
||||
|
||||
// allocate ports for the client
|
||||
val connectionPorts: IntArray
|
||||
|
||||
try {
|
||||
// throws exception if this is not possible
|
||||
connectionPorts = portAllocator.allocate(portsPerClient)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// have to unwind actions!
|
||||
connectionsPerIpCounts.getAndDecrement(clientAddress)
|
||||
|
||||
logger.error("Unable to allocate $portsPerClient ports for client connection!")
|
||||
return
|
||||
}
|
||||
|
||||
// allocate session/stream id's
|
||||
val connectionSessionId: Int
|
||||
try {
|
||||
connectionSessionId = sessionIdAllocator.allocate()
|
||||
} catch (e: AllocationException) {
|
||||
// have to unwind actions!
|
||||
connectionsPerIpCounts.getAndDecrement(clientAddress)
|
||||
portAllocator.free(connectionPorts)
|
||||
|
||||
logger.error("Unable to allocate a session ID for the client connection!")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
val connectionStreamId: Int
|
||||
try {
|
||||
connectionStreamId = streamIdAllocator.allocate()
|
||||
} catch (e: AllocationException) {
|
||||
// have to unwind actions!
|
||||
connectionsPerIpCounts.getAndDecrement(clientAddress)
|
||||
portAllocator.free(connectionPorts)
|
||||
sessionIdAllocator.free(connectionSessionId)
|
||||
|
||||
logger.error("Unable to allocate a stream ID for the client connection!")
|
||||
return
|
||||
}
|
||||
|
||||
val serverAddress = config.listenIpAddress // TODO :: my IP address?? this should be the IP of the box?
|
||||
val subscriptionPort = connectionPorts[0]
|
||||
val publicationPort = connectionPorts[1]
|
||||
|
||||
|
||||
// create a new connection. The session ID is encrypted.
|
||||
try {
|
||||
// connection timeout of 0 doesn't matter. it is not used by the server
|
||||
val clientConnection = UdpMediaDriverConnection(
|
||||
serverAddress, subscriptionPort, publicationPort,
|
||||
connectionStreamId, connectionSessionId, 0, message.isReliable)
|
||||
|
||||
val connection: Connection = endPoint.newConnection(endPoint, clientConnection)
|
||||
|
||||
// VALIDATE:: are we allowed to connect to this server (now that we have the initial server information)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val permitConnection = notifyFilter(connection as C)
|
||||
if (!permitConnection) {
|
||||
// have to unwind actions!
|
||||
connectionsPerIpCounts.getAndDecrement(clientAddress)
|
||||
portAllocator.free(connectionPorts)
|
||||
sessionIdAllocator.free(connectionSessionId)
|
||||
streamIdAllocator.free(connectionStreamId)
|
||||
|
||||
logger.error("Error creating new duologue")
|
||||
|
||||
notifyError(connection, ClientRejectedException("Connection was not permitted!"))
|
||||
return
|
||||
}
|
||||
|
||||
logger.info("Client connected [$clientAddressString:$subscriptionPort|$publicationPort] (session: $sessionId")
|
||||
logger.debug("[{}] created new client connection", connectionSessionId)
|
||||
|
||||
// The one-time pad is used to encrypt the session ID, so that ONLY the correct client knows what it is!
|
||||
val successMessage = Registration.helloAck(message.oneTimePad xor connectionSessionId)
|
||||
successMessage.sessionId = sessionId // has to be the same as before (the client expects this)
|
||||
successMessage.streamId = message.oneTimePad xor connectionStreamId
|
||||
|
||||
successMessage.subscriptionPort = subscriptionPort
|
||||
successMessage.publicationPort = publicationPort
|
||||
successMessage.publicKey = config.settingsStore.getPublicKey()
|
||||
|
||||
endPoint.writeMessage(handshakePublication, successMessage)
|
||||
|
||||
addConnection(connection)
|
||||
notifyConnect(connection)
|
||||
} catch (e: Exception) {
|
||||
// have to unwind actions!
|
||||
connectionsPerIpCounts.getAndDecrement(clientAddress)
|
||||
portAllocator.free(connectionPorts)
|
||||
sessionIdAllocator.free(connectionSessionId)
|
||||
streamIdAllocator.free(connectionStreamId)
|
||||
|
||||
logger.error("Error creating new duologue")
|
||||
|
||||
logger.error("could not process client message: $message")
|
||||
notifyError(e)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("could not process client message: ", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun poll(): Int {
|
||||
// Get the current time, used to cleanup connections
|
||||
val now = System.currentTimeMillis()
|
||||
var pollCount = 0
|
||||
|
||||
forEachConnectionCleanup({ connection ->
|
||||
// If the connection has either been closed, or has expired, it needs to be cleaned-up/deleted.
|
||||
var shouldCleanupConnection = false
|
||||
|
||||
if (connection.isExpired(now)) {
|
||||
logger.debug("[{}] connection expired", connection.sessionId)
|
||||
shouldCleanupConnection = true
|
||||
}
|
||||
|
||||
if (connection.isClosed()) {
|
||||
logger.debug("[{}] connection closed", connection.sessionId)
|
||||
shouldCleanupConnection = true
|
||||
}
|
||||
if (shouldCleanupConnection) {
|
||||
true
|
||||
}
|
||||
else {
|
||||
// Otherwise, poll the duologue for activity.
|
||||
pollCount += connection.pollSubscriptions()
|
||||
false
|
||||
}
|
||||
}, { connectionToClean ->
|
||||
logger.debug("[{}] deleted connection", connectionToClean.sessionId)
|
||||
|
||||
removeConnection(connectionToClean)
|
||||
|
||||
// have to free up resources!
|
||||
connectionsPerIpCounts.getAndDecrement(connectionToClean.remoteAddressInt)
|
||||
portAllocator.free(connectionToClean.subscriptionPort)
|
||||
portAllocator.free(connectionToClean.publicationPort)
|
||||
sessionIdAllocator.free(connectionToClean.sessionId)
|
||||
streamIdAllocator.free(connectionToClean.streamId)
|
||||
|
||||
notifyDisconnect(connectionToClean)
|
||||
})
|
||||
|
||||
return pollCount
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection;
|
||||
|
||||
import io.netty.util.concurrent.Promise;
|
||||
|
||||
public
|
||||
interface ConnectionPoint {
|
||||
|
||||
/**
|
||||
* @return true if the channel is writable. Useful when sending large amounts of data at once.
|
||||
*/
|
||||
boolean isWritable();
|
||||
|
||||
/**
|
||||
* Writes data to the pipe. <b>DOES NOT FLUSH</b> the pipe to the wire!
|
||||
*/
|
||||
void write(Object object) throws Exception;
|
||||
|
||||
/**
|
||||
* Creates a new promise associated with this connection type
|
||||
*/
|
||||
<V> Promise<V> newPromise();
|
||||
}
|
|
@ -13,41 +13,36 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection;
|
||||
package dorkbox.network.connection
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import dorkbox.network.rmi.ConnectionRmiSupport;
|
||||
import dorkbox.network.rmi.ConnectionRmiSupport
|
||||
import javax.crypto.SecretKey
|
||||
|
||||
/**
|
||||
* Supporting methods that are internal to the network stack
|
||||
*/
|
||||
public
|
||||
interface Connection_ extends Connection {
|
||||
|
||||
interface Connection_ : Connection {
|
||||
/**
|
||||
* @return the RMI support for this connection
|
||||
*/
|
||||
ConnectionRmiSupport rmiSupport();
|
||||
fun rmiSupport(): ConnectionRmiSupport
|
||||
|
||||
/**
|
||||
* This is the per-message sequence number.
|
||||
*
|
||||
* The IV for AES-GCM must be 12 bytes, since it's 4 (salt) + 4 (external counter) + 4 (GCM counter)
|
||||
* The 12 bytes IV is created during connection registration, and during the AES-GCM crypto, we override the last 8 with this
|
||||
* counter, which is also transmitted as an optimized int. (which is why it starts at 0, so the transmitted bytes are small)
|
||||
* The IV for AES-GCM must be 12 bytes, since it's 4 (salt) + 4 (external counter) + 4 (GCM counter)
|
||||
* The 12 bytes IV is created during connection registration, and during the AES-GCM crypto, we override the last 8 with this
|
||||
* counter, which is also transmitted as an optimized int. (which is why it starts at 0, so the transmitted bytes are small)
|
||||
*/
|
||||
long nextGcmSequence();
|
||||
|
||||
fun nextGcmSequence(): Long
|
||||
|
||||
/**
|
||||
* @return the AES key.
|
||||
*/
|
||||
SecretKey cryptoKey();
|
||||
fun cryptoKey(): SecretKey
|
||||
|
||||
/**
|
||||
* @return the endpoint associated with this connection
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
EndPoint getEndPoint();
|
||||
fun endPoint(): EndPoint<*>
|
||||
}
|
|
@ -1,590 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
|
||||
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
|
||||
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import dorkbox.network.Client;
|
||||
import dorkbox.network.ClientConfiguration;
|
||||
import dorkbox.network.Configuration;
|
||||
import dorkbox.network.Server;
|
||||
import dorkbox.network.ServerConfiguration;
|
||||
import dorkbox.network.connection.registration.MetaChannel;
|
||||
import dorkbox.network.connection.wrapper.ChannelLocalWrapper;
|
||||
import dorkbox.network.connection.wrapper.ChannelNetworkWrapper;
|
||||
import dorkbox.network.connection.wrapper.ChannelWrapper;
|
||||
import dorkbox.network.rmi.RmiBridge;
|
||||
import dorkbox.network.serialization.NetworkSerializationManager;
|
||||
import dorkbox.network.serialization.Serialization;
|
||||
import dorkbox.network.store.NullSettingsStore;
|
||||
import dorkbox.network.store.SettingsStore;
|
||||
import dorkbox.util.OS;
|
||||
import dorkbox.util.RandomUtil;
|
||||
import dorkbox.util.crypto.CryptoECC;
|
||||
import dorkbox.util.entropy.Entropy;
|
||||
import dorkbox.util.exceptions.SecurityException;
|
||||
import io.aeron.Aeron;
|
||||
import io.aeron.driver.MediaDriver;
|
||||
import io.netty.channel.local.LocalAddress;
|
||||
import io.netty.util.NetUtil;
|
||||
|
||||
/**
|
||||
* represents the base of a client/server end point
|
||||
*/
|
||||
public abstract
|
||||
class EndPoint<_Configuration extends Configuration> implements Closeable {
|
||||
/**
|
||||
* The inclusive lower bound of the reserved sessions range.
|
||||
*/
|
||||
|
||||
public static final int RESERVED_SESSION_ID_LOW = 1;
|
||||
|
||||
/**
|
||||
* The inclusive upper bound of the reserved sessions range.
|
||||
*/
|
||||
|
||||
public static final int RESERVED_SESSION_ID_HIGH = 2147483647;
|
||||
|
||||
public static final int UDP_STREAM_ID = 0x1337cafe;
|
||||
public static final int IPC_STREAM_ID = 0xdeadbeef;
|
||||
|
||||
|
||||
// If TCP and UDP both fill the pipe, THERE WILL BE FRAGMENTATION and dropped UDP packets!
|
||||
// it results in severe UDP packet loss and contention.
|
||||
//
|
||||
// http://www.isoc.org/INET97/proceedings/F3/F3_1.HTM
|
||||
// also, a google search on just "INET97/proceedings/F3/F3_1.HTM" turns up interesting problems.
|
||||
// Usually it's with ISPs.
|
||||
|
||||
public static
|
||||
String getHostDetails(final SocketAddress socketAddress) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
getHostDetails(builder, socketAddress);
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public static
|
||||
void getHostDetails(StringBuilder stringBuilder, final SocketAddress socketAddress) {
|
||||
if (socketAddress instanceof InetSocketAddress) {
|
||||
InetSocketAddress address = (InetSocketAddress) socketAddress;
|
||||
|
||||
InetAddress address1 = address.getAddress();
|
||||
|
||||
String hostName = address1.getHostName();
|
||||
String hostAddress = address1.getHostAddress();
|
||||
|
||||
if (!hostName.equals(hostAddress)) {
|
||||
stringBuilder.append(hostName)
|
||||
.append('/')
|
||||
.append(hostAddress);
|
||||
}
|
||||
else {
|
||||
stringBuilder.append(hostAddress);
|
||||
}
|
||||
|
||||
stringBuilder.append(':')
|
||||
.append(address.getPort());
|
||||
}
|
||||
else if (socketAddress instanceof LocalAddress) {
|
||||
stringBuilder.append(socketAddress.toString());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
protected final org.slf4j.Logger logger;
|
||||
|
||||
private final Class<?> type;
|
||||
protected final _Configuration config;
|
||||
|
||||
protected MediaDriver mediaDriver = null;
|
||||
protected Aeron aeron = null;
|
||||
|
||||
protected final ConnectionManager connectionManager;
|
||||
protected final NetworkSerializationManager serializationManager;
|
||||
// protected final RegistrationWrapper registrationWrapper;
|
||||
|
||||
final ECPrivateKeyParameters privateKey;
|
||||
final ECPublicKeyParameters publicKey;
|
||||
|
||||
final SecureRandom secureRandom;
|
||||
|
||||
final boolean rmiEnabled;
|
||||
|
||||
// we only want one instance of these created. These will be called appropriately
|
||||
final RmiBridge rmiGlobalBridge;
|
||||
|
||||
|
||||
SettingsStore propertyStore;
|
||||
boolean disableRemoteKeyValidation;
|
||||
|
||||
// the connection status of this endpoint. Once a server has connected to ANY client, it will always return true until server.close() is called
|
||||
protected final AtomicBoolean isConnected = new AtomicBoolean(false);
|
||||
|
||||
|
||||
protected
|
||||
EndPoint(final ClientConfiguration config) throws SecurityException, IOException {
|
||||
this(Client.class, config);
|
||||
}
|
||||
|
||||
protected
|
||||
EndPoint(final ServerConfiguration config) throws SecurityException, IOException {
|
||||
this(Server.class, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param type this is either "Client" or "Server", depending on who is creating this endpoint.
|
||||
* @param config these are the specific connection options
|
||||
*
|
||||
* @throws SecurityException if unable to initialize/generate ECC keys
|
||||
*/
|
||||
private
|
||||
EndPoint(Class<?> type, final Configuration config) throws SecurityException, IOException {
|
||||
this.type = type;
|
||||
|
||||
logger = org.slf4j.LoggerFactory.getLogger(type.getSimpleName());
|
||||
|
||||
//noinspection unchecked
|
||||
this.config = (_Configuration) config;
|
||||
|
||||
|
||||
if (config instanceof ServerConfiguration) {
|
||||
ServerConfiguration sConfig = (ServerConfiguration) config;
|
||||
|
||||
if (sConfig.listenIpAddress == null) {
|
||||
throw new RuntimeException("The listen IP address cannot be null");
|
||||
}
|
||||
|
||||
String listenIpAddress = sConfig.listenIpAddress = sConfig.listenIpAddress.toLowerCase();
|
||||
|
||||
if (listenIpAddress.equals("localhost") || listenIpAddress.equals("loopback") || listenIpAddress.equals("lo") ||
|
||||
listenIpAddress.startsWith("127.") || listenIpAddress.startsWith("::1")) {
|
||||
|
||||
// localhost/loopback IP might not always be 127.0.0.1 or ::1
|
||||
sConfig.listenIpAddress = NetUtil.LOCALHOST.getHostAddress();
|
||||
}
|
||||
else if (listenIpAddress.equals("*")) {
|
||||
// we set this to "0.0.0.0" so that it is clear that we are trying to bind to that address.
|
||||
sConfig.listenIpAddress = "0.0.0.0";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Aeron configuration
|
||||
File aeronLogDirectory = config.aeronLogDirectory;
|
||||
if (aeron == null) {
|
||||
File baseFile;
|
||||
if (OS.isLinux()) {
|
||||
// this is significantly faster for linux than using the temp dir
|
||||
baseFile = new File(System.getProperty("/dev/shm/"));
|
||||
} else {
|
||||
baseFile = new File(System.getProperty("java.io.tmpdir"));
|
||||
}
|
||||
// note: MacOS should create a ram-drive for this
|
||||
/*
|
||||
* Linux
|
||||
Linux normally requires some settings of sysctl values. One is net.core.rmem_max to allow larger SO_RCVBUF and net.core.wmem_max to allow larger SO_SNDBUF values to be set.
|
||||
Windows
|
||||
|
||||
Windows tends to use SO_SNDBUF values that are too small. It is recommended to use values more like 1MB or so.
|
||||
Mac/Darwin
|
||||
|
||||
Mac tends to use SO_SNDBUF values that are too small. It is recommended to use larger values, like 16KB.
|
||||
|
||||
Note: Since Mac OS does not have a built-in support for /dev/shm it is advised to create a RAM disk for the Aeron directory (aeron.dir).
|
||||
|
||||
You can create a RAM disk with the following command:
|
||||
|
||||
$ diskutil erasevolume HFS+ "DISK_NAME" `hdiutil attach -nomount ram://$((2048 * SIZE_IN_MB))`
|
||||
|
||||
where:
|
||||
|
||||
DISK_NAME should be replaced with a name of your choice.
|
||||
SIZE_IN_MB is the size in megabytes for the disk (e.g. 4096 for a 4GB disk).
|
||||
|
||||
For example, the following command creates a RAM disk named DevShm which is 2GB in size:
|
||||
|
||||
$ diskutil erasevolume HFS+ "DevShm" `hdiutil attach -nomount ram://$((2048 * 2048))`
|
||||
|
||||
After this command is executed the new disk will be mounted under /Volumes/DevShm.
|
||||
*/
|
||||
|
||||
String baseName = "aeron-" + type.getSimpleName();
|
||||
aeronLogDirectory = new File(baseFile, baseName);
|
||||
while (aeronLogDirectory.exists()) {
|
||||
logger.error("Aeron log directory already exists! This might not be what you want!");
|
||||
// avoid a collision
|
||||
aeronLogDirectory = new File(baseFile, baseName + RandomUtil.get().nextInt(1000));
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("Aeron log directory: " + aeronLogDirectory);
|
||||
|
||||
|
||||
// LOW-LATENCY SETTINGS
|
||||
// .termBufferSparseFile(false)
|
||||
// .useWindowsHighResTimer(true)
|
||||
// .threadingMode(ThreadingMode.DEDICATED)
|
||||
// .conductorIdleStrategy(BusySpinIdleStrategy.INSTANCE)
|
||||
// .receiverIdleStrategy(NoOpIdleStrategy.INSTANCE)
|
||||
// .senderIdleStrategy(NoOpIdleStrategy.INSTANCE);
|
||||
|
||||
final MediaDriver.Context mediaDriverContext = new MediaDriver.Context()
|
||||
.publicationReservedSessionIdLow(RESERVED_SESSION_ID_LOW)
|
||||
.publicationReservedSessionIdHigh(RESERVED_SESSION_ID_HIGH)
|
||||
.dirDeleteOnShutdown(true)
|
||||
.threadingMode(config.threadingMode)
|
||||
.mtuLength(config.networkMtuSize)
|
||||
.socketSndbufLength(config.sendBufferSize)
|
||||
.socketRcvbufLength(config.receiveBufferSize)
|
||||
.aeronDirectoryName(aeronLogDirectory.getAbsolutePath());
|
||||
|
||||
final Aeron.Context aeronContext = new Aeron.Context().aeronDirectoryName(mediaDriverContext.aeronDirectoryName());
|
||||
|
||||
try {
|
||||
mediaDriver = MediaDriver.launch(mediaDriverContext);
|
||||
aeron = Aeron.connect(aeronContext);
|
||||
} catch (final Exception e) {
|
||||
try {
|
||||
close();
|
||||
} catch (final Exception secondaryException) {
|
||||
e.addSuppressed(secondaryException);
|
||||
}
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
|
||||
// serialization stuff
|
||||
if (config.serialization != null) {
|
||||
serializationManager = config.serialization;
|
||||
} else {
|
||||
serializationManager = Serialization.DEFAULT();
|
||||
}
|
||||
|
||||
// setup our RMI serialization managers. Can only be called once
|
||||
rmiEnabled = serializationManager.initRmiSerialization();
|
||||
|
||||
|
||||
// The registration wrapper permits the registration process to access protected/package fields/methods, that we don't want
|
||||
// to expose to external code. "this" escaping can be ignored, because it is benign.
|
||||
//noinspection ThisEscapedInObjectConstruction
|
||||
// if (type == Server.class) {
|
||||
// registrationWrapper = new RegistrationWrapperServer(this, logger);
|
||||
// } else {
|
||||
// registrationWrapper = new RegistrationWrapperClient(this, logger);
|
||||
// }
|
||||
|
||||
|
||||
// we have to be able to specify WHAT property store we want to use, since it can change!
|
||||
if (config.settingsStore == null) {
|
||||
propertyStore = new PropertyStore();
|
||||
}
|
||||
else {
|
||||
propertyStore = config.settingsStore;
|
||||
}
|
||||
|
||||
propertyStore.init(serializationManager, null);
|
||||
|
||||
// null it out, since it is sensitive!
|
||||
config.settingsStore = null;
|
||||
|
||||
|
||||
if (!(propertyStore instanceof NullSettingsStore)) {
|
||||
// initialize the private/public keys used for negotiating ECC handshakes
|
||||
// these are ONLY used for IP connections. LOCAL connections do not need a handshake!
|
||||
ECPrivateKeyParameters privateKey = propertyStore.getPrivateKey();
|
||||
ECPublicKeyParameters publicKey = propertyStore.getPublicKey();
|
||||
|
||||
if (privateKey == null || publicKey == null) {
|
||||
try {
|
||||
// seed our RNG based off of this and create our ECC keys
|
||||
byte[] seedBytes = Entropy.get("There are no ECC keys for the " + type.getSimpleName() + " yet");
|
||||
SecureRandom secureRandom = new SecureRandom(seedBytes);
|
||||
secureRandom.nextBytes(seedBytes);
|
||||
|
||||
logger.debug("Now generating ECC (" + CryptoECC.curve25519 + ") keys. Please wait!");
|
||||
AsymmetricCipherKeyPair generateKeyPair = CryptoECC.generateKeyPair(CryptoECC.curve25519, secureRandom);
|
||||
|
||||
privateKey = (ECPrivateKeyParameters) generateKeyPair.getPrivate();
|
||||
publicKey = (ECPublicKeyParameters) generateKeyPair.getPublic();
|
||||
|
||||
// save to properties file
|
||||
propertyStore.savePrivateKey(privateKey);
|
||||
propertyStore.savePublicKey(publicKey);
|
||||
|
||||
logger.debug("Done with ECC keys!");
|
||||
} catch (Exception e) {
|
||||
String message = "Unable to initialize/generate ECC keys. FORCED SHUTDOWN.";
|
||||
logger.error(message);
|
||||
throw new SecurityException(message);
|
||||
}
|
||||
}
|
||||
|
||||
this.privateKey = privateKey;
|
||||
this.publicKey = publicKey;
|
||||
}
|
||||
else {
|
||||
this.privateKey = null;
|
||||
this.publicKey = null;
|
||||
}
|
||||
|
||||
|
||||
secureRandom = new SecureRandom(propertyStore.getSalt());
|
||||
|
||||
// we don't care about un-instantiated/constructed members, since the class type is the only interest.
|
||||
//noinspection unchecked
|
||||
connectionManager = new ConnectionManager(type.getSimpleName(), connection0(null, null).getClass());
|
||||
|
||||
if (rmiEnabled) {
|
||||
rmiGlobalBridge = new RmiBridge(logger, true);
|
||||
}
|
||||
else {
|
||||
rmiGlobalBridge = null;
|
||||
}
|
||||
|
||||
Logger readLogger = LoggerFactory.getLogger(type.getSimpleName() + ".READ");
|
||||
Logger writeLogger = LoggerFactory.getLogger(type.getSimpleName() + ".WRITE");
|
||||
serializationManager.finishInit(readLogger, writeLogger);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables remote endpoint public key validation when the connection is established. This is not recommended as it is a security risk
|
||||
*/
|
||||
public
|
||||
void disableRemoteKeyValidation() {
|
||||
if (isConnected.get()) {
|
||||
logger.error("Cannot disable the remote key validation after this endpoint is connected!");
|
||||
}
|
||||
else {
|
||||
logger.info("WARNING: Disabling remote key validation is a security risk!!");
|
||||
disableRemoteKeyValidation = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the property store used by this endpoint. The property store can store via properties,
|
||||
* a database, etc, or can be a "null" property store, which does nothing
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public
|
||||
<S extends SettingsStore> S getPropertyStore() {
|
||||
return (S) propertyStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the serialization wrapper if there is an object type that needs to be added outside of the basics.
|
||||
*/
|
||||
public
|
||||
NetworkSerializationManager getSerialization() {
|
||||
return serializationManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method allows the connections used by the client/server to be subclassed (with custom implementations).
|
||||
* <p/>
|
||||
* As this is for the network stack, the new connection MUST subclass {@link ConnectionImpl}
|
||||
* <p/>
|
||||
* The parameters are ALL NULL when getting the base class, as this instance is just thrown away.
|
||||
*
|
||||
* @return a new network connection
|
||||
*/
|
||||
protected
|
||||
<E extends EndPoint> ConnectionImpl newConnection(final E endPoint, final ChannelWrapper wrapper) {
|
||||
return new ConnectionImpl(endPoint, wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal call by the pipeline when:
|
||||
* - creating a new network connection
|
||||
* - when determining the baseClass for listeners
|
||||
*
|
||||
* @param metaChannel can be NULL (when getting the baseClass)
|
||||
* @param remoteAddress be NULL (when getting the baseClass or when creating a local channel)
|
||||
*/
|
||||
final
|
||||
ConnectionImpl connection0(final MetaChannel metaChannel, final InetSocketAddress remoteAddress) {
|
||||
ConnectionImpl connection;
|
||||
|
||||
// setup the extras needed by the network connection.
|
||||
// These properties are ASSIGNED in the same thread that CREATED the object. Only the AES info needs to be
|
||||
// volatile since it is the only thing that changes.
|
||||
if (metaChannel != null) {
|
||||
ChannelWrapper wrapper;
|
||||
|
||||
if (metaChannel.localChannel != null) {
|
||||
wrapper = new ChannelLocalWrapper(metaChannel);
|
||||
}
|
||||
else {
|
||||
wrapper = new ChannelNetworkWrapper(metaChannel, remoteAddress);
|
||||
}
|
||||
|
||||
connection = newConnection(this, wrapper);
|
||||
|
||||
isConnected.set(true);
|
||||
connectionManager.addConnection(connection);
|
||||
}
|
||||
else {
|
||||
// getting the connection baseClass
|
||||
|
||||
// have to add the networkAssociate to a map of "connected" computers
|
||||
connection = newConnection(null, null);
|
||||
}
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal call by the pipeline to notify the "Connection" object that it has "connected", meaning that modifications
|
||||
* to the pipeline are finished.
|
||||
* <p/>
|
||||
* Only the CLIENT injects in front of this
|
||||
*/
|
||||
void connectionConnected0(ConnectionImpl connection) {
|
||||
connectionManager.onConnected(connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose methods to modify the listeners (connect/disconnect/idle/receive events).
|
||||
*/
|
||||
public final
|
||||
Listeners listeners() {
|
||||
return connectionManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a non-modifiable list of active connections
|
||||
*/
|
||||
public
|
||||
<C extends Connection> List<C> getConnections() {
|
||||
return connectionManager.getConnections();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a "global" RMI object for use by multiple connections.
|
||||
*
|
||||
* @return the ID assigned to this RMI object
|
||||
*/
|
||||
public
|
||||
<T> int createGlobalObject(final T globalObject) {
|
||||
return rmiGlobalBridge.register(globalObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a previously created "global" RMI object
|
||||
*
|
||||
* @param objectRmiId the ID of the RMI object to get
|
||||
*
|
||||
* @return null if the object doesn't exist or the ID is invalid.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public
|
||||
<T> T getGlobalObject(final int objectRmiId) {
|
||||
return (T) rmiGlobalBridge.getRegisteredObject(objectRmiId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
String toString() {
|
||||
return "EndPoint [" + getName() + "]";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the type class of this connection endpoint
|
||||
*/
|
||||
public Class<?> getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the simple name (for the class) of this connection endpoint
|
||||
*/
|
||||
public
|
||||
String getName() {
|
||||
return type.getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + (privateKey == null ? 0 : privateKey.hashCode());
|
||||
result = prime * result + (publicKey == null ? 0 : publicKey.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
EndPoint other = (EndPoint) obj;
|
||||
|
||||
if (privateKey == null) {
|
||||
if (other.privateKey != null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (!CryptoECC.compare(privateKey, other.privateKey)) {
|
||||
return false;
|
||||
}
|
||||
if (publicKey == null) {
|
||||
if (other.publicKey != null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (!CryptoECC.compare(publicKey, other.publicKey)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (aeron != null) {
|
||||
aeron.close();
|
||||
}
|
||||
if (mediaDriver != null) {
|
||||
mediaDriver.close();
|
||||
}
|
||||
|
||||
if (propertyStore != null) {
|
||||
propertyStore.close();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,771 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection
|
||||
|
||||
import dorkbox.network.*
|
||||
import dorkbox.network.aeron.CoroutineIdleStrategy
|
||||
import dorkbox.network.connection.ping.PingMessage
|
||||
import dorkbox.network.other.CryptoEccNative
|
||||
import dorkbox.network.rmi.RmiServer
|
||||
import dorkbox.network.rmi.messages.RmiMessage
|
||||
import dorkbox.network.serialization.NetworkSerializationManager
|
||||
import dorkbox.network.serialization.Serialization
|
||||
import dorkbox.network.store.NullSettingsStore
|
||||
import dorkbox.network.store.SettingsStore
|
||||
import dorkbox.util.NamedThreadFactory
|
||||
import dorkbox.util.OS
|
||||
import dorkbox.util.entropy.Entropy
|
||||
import dorkbox.util.exceptions.SecurityException
|
||||
import io.aeron.Aeron
|
||||
import io.aeron.Publication
|
||||
import io.aeron.driver.MediaDriver
|
||||
import io.aeron.logbuffer.Header
|
||||
import kotlinx.atomicfu.atomic
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.agrona.DirectBuffer
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.File
|
||||
import java.security.KeyFactory
|
||||
import java.security.PrivateKey
|
||||
import java.security.SecureRandom
|
||||
import java.security.interfaces.XECPrivateKey
|
||||
import java.security.spec.PKCS8EncodedKeySpec
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
// If TCP and UDP both fill the pipe, THERE WILL BE FRAGMENTATION and dropped UDP packets!
|
||||
// it results in severe UDP packet loss and contention.
|
||||
//
|
||||
// http://www.isoc.org/INET97/proceedings/F3/F3_1.HTM
|
||||
// also, a google search on just "INET97/proceedings/F3/F3_1.HTM" turns up interesting problems.
|
||||
// Usually it's with ISPs.
|
||||
/**
|
||||
* represents the base of a client/server end point for interacting with aeron
|
||||
*/
|
||||
abstract class EndPoint<C : Connection>
|
||||
internal constructor(val type: Class<*>, internal val config: Configuration) : AutoCloseable {
|
||||
protected constructor(config: Configuration) : this(Client::class.java, config)
|
||||
protected constructor(config: ServerConfiguration) : this(Server::class.java, config)
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Identifier for invalid sessions. This must be < RESERVED_SESSION_ID_LOW
|
||||
*/
|
||||
const val RESERVED_SESSION_ID_INVALID = 0
|
||||
|
||||
/**
|
||||
* The inclusive lower bound of the reserved sessions range. THIS SHOULD NEVER BE <= 0!
|
||||
*/
|
||||
const val RESERVED_SESSION_ID_LOW = 1
|
||||
|
||||
/**
|
||||
* The inclusive upper bound of the reserved sessions range.
|
||||
*/
|
||||
const val RESERVED_SESSION_ID_HIGH = Integer.MAX_VALUE
|
||||
|
||||
const val UDP_HANDSHAKE_STREAM_ID: Int = 0x1337cafe
|
||||
const val IPC_STREAM_ID: Int = 0x1337c0de
|
||||
|
||||
init {
|
||||
println("THIS IS ONLY IPV4 AT THE MOMENT. IPV6 is in progress!")
|
||||
}
|
||||
|
||||
fun errorCodeName(result: Long): String {
|
||||
return when (result) {
|
||||
Publication.NOT_CONNECTED -> "Not connected"
|
||||
Publication.ADMIN_ACTION -> "Administrative action"
|
||||
Publication.BACK_PRESSURED -> "Back pressured"
|
||||
Publication.CLOSED -> "Publication is closed"
|
||||
Publication.MAX_POSITION_EXCEEDED -> "Maximum term position exceeded"
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// the simple name (for the class) of this connection endpoint
|
||||
val name = type.simpleName
|
||||
val logger: Logger = LoggerFactory.getLogger(name)
|
||||
|
||||
internal val closables = CopyOnWriteArrayList<AutoCloseable>()
|
||||
|
||||
internal val actionDispatch = CoroutineScope(Dispatchers.Default)
|
||||
internal abstract val connectionManager: ConnectionManager<C>
|
||||
|
||||
internal lateinit var mediaDriver: MediaDriver
|
||||
internal lateinit var aeron: Aeron
|
||||
|
||||
/**
|
||||
* Returns the serialization wrapper if there is an object type that needs to be added outside of the basics.
|
||||
*/
|
||||
val serialization: NetworkSerializationManager
|
||||
|
||||
private val sendIdleStrategy: CoroutineIdleStrategy
|
||||
|
||||
|
||||
val privateKey: PrivateKey?
|
||||
val publicKey: ByteArray?
|
||||
|
||||
val secureRandom: SecureRandom
|
||||
|
||||
private val shutdown = atomic(false)
|
||||
private val shutdownLatch = CountDownLatch(1)
|
||||
|
||||
// we only want one instance of these created. These will be called appropriately
|
||||
val settingsStore: SettingsStore
|
||||
val rmiGlobalBridge = RmiServer(logger, true)
|
||||
var disableRemoteKeyValidation = false
|
||||
|
||||
/**
|
||||
* Checks to see if this client has connected yet or not.
|
||||
*
|
||||
* Once a server has connected to ANY client, it will always return true until server.close() is called
|
||||
*
|
||||
* @return true if we are connected, false otherwise.
|
||||
*/
|
||||
abstract fun isConnected(): Boolean
|
||||
|
||||
/**
|
||||
* @param type this is either "Client" or "Server", depending on who is creating this endpoint.
|
||||
* @param config these are the specific connection options
|
||||
*
|
||||
* @throws SecurityException if unable to initialize/generate ECC keys
|
||||
* @throws ServerException if unable to validate configuration
|
||||
*
|
||||
*/
|
||||
init {
|
||||
// Aeron configuration
|
||||
|
||||
/*
|
||||
* Linux
|
||||
* Linux normally requires some settings of sysctl values. One is net.core.rmem_max to allow larger SO_RCVBUF and
|
||||
* net.core.wmem_max to allow larger SO_SNDBUF values to be set.
|
||||
*
|
||||
* Windows
|
||||
* Windows tends to use SO_SNDBUF values that are too small. It is recommended to use values more like 1MB or so.
|
||||
*
|
||||
* Mac/Darwin
|
||||
*
|
||||
* Mac tends to use SO_SNDBUF values that are too small. It is recommended to use larger values, like 16KB.
|
||||
*/
|
||||
if (config.receiveBufferSize == 0) {
|
||||
config.receiveBufferSize = io.aeron.driver.Configuration.SOCKET_RCVBUF_LENGTH_DEFAULT
|
||||
// when {
|
||||
// OS.isLinux() ->
|
||||
// OS.isWindows() ->
|
||||
// OS.isMacOsX() ->
|
||||
// }
|
||||
|
||||
// val rmem_max = dorkbox.network.NetUtil.sysctlGetInt("net.core.rmem_max")
|
||||
// val wmem_max = dorkbox.network.NetUtil.sysctlGetInt("net.core.wmem_max")
|
||||
}
|
||||
|
||||
|
||||
if (config.sendBufferSize == 0) {
|
||||
config.receiveBufferSize = io.aeron.driver.Configuration.SOCKET_SNDBUF_LENGTH_DEFAULT
|
||||
// when {
|
||||
// OS.isLinux() ->
|
||||
// OS.isWindows() ->
|
||||
// OS.isMacOsX() ->
|
||||
// }
|
||||
|
||||
// val rmem_max = dorkbox.network.NetUtil.sysctlGetInt("net.core.rmem_max")
|
||||
// val wmem_max = dorkbox.network.NetUtil.sysctlGetInt("net.core.wmem_max")
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Note: Since Mac OS does not have a built-in support for /dev/shm it is advised to create a RAM disk for the Aeron directory (aeron.dir).
|
||||
*
|
||||
* You can create a RAM disk with the following command:
|
||||
*
|
||||
* $ diskutil erasevolume HFS+ "DISK_NAME" `hdiutil attach -nomount ram://$((2048 * SIZE_IN_MB))`
|
||||
*
|
||||
* where:
|
||||
*
|
||||
* DISK_NAME should be replaced with a name of your choice.
|
||||
* SIZE_IN_MB is the size in megabytes for the disk (e.g. 4096 for a 4GB disk).
|
||||
*
|
||||
* For example, the following command creates a RAM disk named DevShm which is 2GB in size:
|
||||
*
|
||||
* $ diskutil erasevolume HFS+ "DevShm" `hdiutil attach -nomount ram://$((2048 * 2048))`
|
||||
*
|
||||
* After this command is executed the new disk will be mounted under /Volumes/DevShm.
|
||||
*/
|
||||
if (config.aeronLogDirectory == null) {
|
||||
val baseFile = when {
|
||||
OS.isMacOsX() -> {
|
||||
logger.info("It is recommended to create a RAM drive for best performance. For example\n" +
|
||||
"\$ diskutil erasevolume HFS+ \"DevShm\" `hdiutil attach -nomount ram://\$((2048 * 2048))`\n" +
|
||||
"\t After this, set config.aeronLogDirectory = \"/Volumes/DevShm\"")
|
||||
File(System.getProperty("java.io.tmpdir"))
|
||||
}
|
||||
OS.isLinux() -> {
|
||||
// this is significantly faster for linux than using the temp dir
|
||||
File(System.getProperty("/dev/shm/"))
|
||||
}
|
||||
else -> {
|
||||
File(System.getProperty("java.io.tmpdir"))
|
||||
}
|
||||
}
|
||||
|
||||
val baseName = "aeron-" + type.simpleName
|
||||
val aeronLogDirectory = File(baseFile, baseName)
|
||||
if (aeronLogDirectory.exists()) {
|
||||
logger.error("Aeron log directory already exists! This might not be what you want!")
|
||||
// avoid a collision
|
||||
// aeronLogDirectory = File(baseFile, baseName + RandomUtil.get().nextInt(1000))
|
||||
}
|
||||
logger.debug("Aeron log directory: $aeronLogDirectory")
|
||||
config.aeronLogDirectory = aeronLogDirectory
|
||||
}
|
||||
|
||||
// the RmiNoOpConnection must have an endpoint, and we DO NOT want it to actually setup/configure aeron!
|
||||
if (config.publicationPort > 0) {
|
||||
val threadFactory = NamedThreadFactory("Aeron", false)
|
||||
|
||||
// LOW-LATENCY SETTINGS
|
||||
// .termBufferSparseFile(false)
|
||||
// .useWindowsHighResTimer(true)
|
||||
// .threadingMode(ThreadingMode.DEDICATED)
|
||||
// .conductorIdleStrategy(BusySpinIdleStrategy.INSTANCE)
|
||||
// .receiverIdleStrategy(NoOpIdleStrategy.INSTANCE)
|
||||
// .senderIdleStrategy(NoOpIdleStrategy.INSTANCE);
|
||||
val mediaDriverContext = MediaDriver.Context()
|
||||
.publicationReservedSessionIdLow(RESERVED_SESSION_ID_LOW)
|
||||
.publicationReservedSessionIdHigh(RESERVED_SESSION_ID_HIGH)
|
||||
.dirDeleteOnStart(true) // TODO: FOR NOW?
|
||||
.dirDeleteOnShutdown(true)
|
||||
.conductorThreadFactory(threadFactory)
|
||||
.receiverThreadFactory(threadFactory)
|
||||
.senderThreadFactory(threadFactory)
|
||||
.sharedNetworkThreadFactory(threadFactory)
|
||||
.sharedThreadFactory(threadFactory)
|
||||
.threadingMode(config.threadingMode)
|
||||
.mtuLength(config.networkMtuSize)
|
||||
.socketSndbufLength(config.sendBufferSize)
|
||||
.socketRcvbufLength(config.receiveBufferSize)
|
||||
.aeronDirectoryName(config.aeronLogDirectory!!.absolutePath)
|
||||
|
||||
val aeronContext = Aeron.Context().aeronDirectoryName(mediaDriverContext.aeronDirectoryName())
|
||||
|
||||
try {
|
||||
mediaDriver = MediaDriver.launch(mediaDriverContext)
|
||||
} catch (e: Exception) {
|
||||
throw e
|
||||
}
|
||||
|
||||
try {
|
||||
aeron = Aeron.connect(aeronContext)
|
||||
} catch (e: Exception) {
|
||||
try {
|
||||
mediaDriver.close()
|
||||
} catch (secondaryException: Exception) {
|
||||
e.addSuppressed(secondaryException)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
|
||||
closables.add(aeron)
|
||||
closables.add(mediaDriver)
|
||||
}
|
||||
|
||||
|
||||
// serialization stuff
|
||||
serialization = config.serialization
|
||||
sendIdleStrategy = config.sendIdleStrategy
|
||||
|
||||
// we have to be able to specify WHAT property store we want to use, since it can change!
|
||||
settingsStore = config.settingsStore
|
||||
settingsStore.init(serialization, config.settingsStorageSystem.build())
|
||||
|
||||
// the storage is closed via this as well
|
||||
closables.add(settingsStore)
|
||||
|
||||
secureRandom = SecureRandom(settingsStore.getSalt())
|
||||
|
||||
if (settingsStore !is NullSettingsStore) {
|
||||
// initialize the private/public keys used for negotiating ECC handshakes
|
||||
// these are ONLY used for IP connections. LOCAL connections do not need a handshake!
|
||||
var privateKey = settingsStore.getPrivateKey()
|
||||
var publicKey = settingsStore.getPublicKey()
|
||||
|
||||
if (privateKey == null || publicKey == null) {
|
||||
try {
|
||||
// seed our RNG based off of this and create our ECC keys
|
||||
val seedBytes = Entropy.get("There are no ECC keys for the " + type.simpleName + " yet")
|
||||
logger.debug("Now generating ECC (" + CryptoEccNative.curve25519 + ") keys. Please wait!")
|
||||
|
||||
secureRandom.nextBytes(seedBytes)
|
||||
val generateKeyPair = CryptoEccNative.createKeyPair(secureRandom)
|
||||
privateKey = generateKeyPair.private.encoded
|
||||
publicKey = generateKeyPair.public.encoded
|
||||
|
||||
// save to properties file
|
||||
settingsStore.savePrivateKey(privateKey)
|
||||
settingsStore.savePublicKey(publicKey)
|
||||
logger.debug("Done with ECC keys!")
|
||||
} catch (e: Exception) {
|
||||
val message = "Unable to initialize/generate ECC keys. FORCED SHUTDOWN."
|
||||
logger.error(message)
|
||||
throw SecurityException(message)
|
||||
}
|
||||
}
|
||||
|
||||
val keyFactory = KeyFactory.getInstance("XDH")
|
||||
|
||||
this.privateKey = keyFactory.generatePrivate(PKCS8EncodedKeySpec(privateKey)) as XECPrivateKey
|
||||
// keyFactory.generatePublic(X509EncodedKeySpec(publicKey)) as XECPublicKey
|
||||
this.publicKey = publicKey
|
||||
} else {
|
||||
privateKey = null
|
||||
publicKey = null
|
||||
}
|
||||
|
||||
// we are done with initial configuration, now finish serialization
|
||||
serialization.finishInit(type)
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables remote endpoint public key validation when the connection is established. This is not recommended as it is a security risk
|
||||
*/
|
||||
fun disableRemoteKeyValidation() {
|
||||
if (isConnected()) {
|
||||
logger.error("Cannot disable the remote key validation after this endpoint is connected!")
|
||||
} else {
|
||||
logger.info("WARNING: Disabling remote key validation is a security risk!!")
|
||||
disableRemoteKeyValidation = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the key does not match AND we have disabled remote key validation, then metachannel.changedRemoteKey = true. OTHERWISE, key validation is REQUIRED!
|
||||
*
|
||||
* @return true if the remote address public key matches the one saved or we disabled remote key validation.
|
||||
*/
|
||||
internal fun validateRemoteAddress(remoteAddress: Int, publicKey: ByteArray?): Boolean {
|
||||
if (publicKey == null) {
|
||||
logger.error("Error validating public key for ${NetworkUtil.IP.toString(remoteAddress)}! It was null (and should not have been)")
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
val savedPublicKey = config.settingsStore.getRegisteredServerKey(remoteAddress)
|
||||
if (savedPublicKey == null) {
|
||||
if (logger.isDebugEnabled) {
|
||||
logger.debug("Adding new remote IP address key for ${NetworkUtil.IP.toString(remoteAddress)}")
|
||||
}
|
||||
|
||||
config.settingsStore.addRegisteredServerKey(remoteAddress, publicKey)
|
||||
} else {
|
||||
// COMPARE!
|
||||
if (!publicKey.contentEquals(savedPublicKey)) {
|
||||
return if (!config.enableRemoteSignatureValidation) {
|
||||
logger.warn("Invalid or non-matching public key from remote connection, their public key has changed. Toggling extra flag in channel to indicate key change. To fix, remove entry for: ${NetworkUtil.IP.toString(remoteAddress)}")
|
||||
true
|
||||
} else {
|
||||
// keys do not match, abort!
|
||||
logger.error("Invalid or non-matching public key from remote connection, their public key has changed. To fix, remove entry for: ${NetworkUtil.IP.toString(remoteAddress)}")
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
// keys do not match, abort!
|
||||
logger.error("Error validating public key for ${NetworkUtil.IP.toString(remoteAddress)}!", e)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the property store used by this endpoint. The property store can store via properties,
|
||||
* a database, etc, or can be a "null" property store, which does nothing
|
||||
*/
|
||||
fun <S : SettingsStore> getPropertyStore(): S {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return settingsStore as S
|
||||
}
|
||||
|
||||
/**
|
||||
* This method allows the connections used by the client/server to be subclassed (with custom implementations).
|
||||
*
|
||||
* As this is for the network stack, the new connection MUST subclass [ConnectionImpl]
|
||||
*
|
||||
* The parameters are ALL NULL when getting the base class, as this instance is just thrown away.
|
||||
*
|
||||
* @return a new network connection
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
open fun newConnection(endPoint: EndPoint<C>, mediaDriverConnection: MediaDriverConnection): C {
|
||||
return ConnectionImpl(endPoint, mediaDriverConnection) as C
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a function that will be called BEFORE a client/server "connects" with
|
||||
* each other, and used to determine if a connection should be allowed
|
||||
* <p>
|
||||
* If the function returns TRUE, then the connection will continue to connect.
|
||||
* If the function returns FALSE, then the other end of the connection will
|
||||
* receive a connection error
|
||||
* <p>
|
||||
* For a server, this function will be called for ALL clients.
|
||||
*/
|
||||
fun filter(function: suspend (C) -> Boolean) {
|
||||
actionDispatch.launch {
|
||||
connectionManager.filter(function)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a function that will be called when a client/server "connects" with each other
|
||||
* <p>
|
||||
* For a server, this function will be called for ALL clients.
|
||||
*/
|
||||
fun onConnect(function: suspend (C) -> Unit) {
|
||||
actionDispatch.launch {
|
||||
connectionManager.onConnect(function)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the remote end is no longer connected.
|
||||
* <p>
|
||||
* Do not try to send messages! The connection will already be closed, resulting in an error if you attempt to do so.
|
||||
*/
|
||||
fun onDisconnect(function: suspend (C) -> Unit) {
|
||||
actionDispatch.launch {
|
||||
connectionManager.onDisconnect(function)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when there is an error for a specific connection
|
||||
* <p>
|
||||
* The error is also sent to an error log before this method is called.
|
||||
*/
|
||||
fun onError(function: suspend (C, Throwable) -> Unit) {
|
||||
actionDispatch.launch {
|
||||
connectionManager.onError(function)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when there is an error in general
|
||||
* <p>
|
||||
* The error is also sent to an error log before this method is called.
|
||||
*/
|
||||
fun onError(function: suspend (Throwable) -> Unit) {
|
||||
actionDispatch.launch {
|
||||
connectionManager.onError(function)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an object has been received from the remote end of the connection.
|
||||
* <p>
|
||||
* This method should not block for long periods as other network activity will not be processed until it returns.
|
||||
*/
|
||||
fun <M : Any> onMessage(function: suspend (C, M) -> Unit) {
|
||||
actionDispatch.launch {
|
||||
connectionManager.onMessage(function)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs an action for each connection inside of a read-lock
|
||||
*/
|
||||
suspend fun forEachConnection(function: suspend (connection: C) -> Unit) {
|
||||
connectionManager.forEachConnectionDoRead(function)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a "global" RMI object for use by multiple connections.
|
||||
*
|
||||
* @return the ID assigned to this RMI object
|
||||
*/
|
||||
fun <T> createGlobalObject(globalObject: T): Int {
|
||||
return rmiGlobalBridge.register(globalObject) ?: 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a previously created "global" RMI object
|
||||
*
|
||||
* @param objectRmiId the ID of the RMI object to get
|
||||
*
|
||||
* @return null if the object doesn't exist or the ID is invalid.
|
||||
*/
|
||||
fun <T> getGlobalObject(objectRmiId: Int): T {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return rmiGlobalBridge.getRegisteredObject(objectRmiId) as T
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
suspend fun writeMessage(publication: Publication, message: Any) {
|
||||
writeMessage(publication, Serialization.NOP_CONNECTION, message)
|
||||
}
|
||||
|
||||
suspend fun writeMessage(publication: Publication, connection: Connection_, message: Any) {
|
||||
sendIdleStrategy.reset()
|
||||
// TODO: WE MIGHT NOT WANT TO USE SESSIONID()!!
|
||||
logger.debug("[{}] send: {}", publication.sessionId(), message)
|
||||
|
||||
val kryo: KryoExtra = serialization.takeKryo()
|
||||
try {
|
||||
kryo.write(connection, message)
|
||||
|
||||
val buffer = kryo.writerBuffer
|
||||
val objectSize = buffer.position()
|
||||
val internalBuffer = buffer.internalBuffer
|
||||
|
||||
var result: Long
|
||||
while (true) {
|
||||
result = publication.offer(internalBuffer, 0, objectSize)
|
||||
// success!
|
||||
if (result > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (result == Publication.BACK_PRESSURED || result == Publication.ADMIN_ACTION) {
|
||||
// we should retry.
|
||||
sendIdleStrategy.idle()
|
||||
continue
|
||||
}
|
||||
|
||||
// more critical error sending the message. we shouldn't retry or anything.
|
||||
logger.error("Error sending message. ${errorCodeName(result)}")
|
||||
return
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("Error serializing message $message", e)
|
||||
} finally {
|
||||
serialization.returnKryo(kryo)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @param buffer The buffer
|
||||
* @param offset The offset from the start of the buffer
|
||||
* @param length The number of bytes to extract
|
||||
*
|
||||
* @return A string
|
||||
*/
|
||||
fun readHandshakeMessage(buffer: DirectBuffer, offset: Int, length: Int, header: Header): Any? {
|
||||
// The sessionId is unique within a Subscription and unique across all Publication's from a sourceIdentity.
|
||||
// val sessionId = header.sessionId()
|
||||
// TODO: WE MIGHT NOT WANT TO USE SESSIONID()!!
|
||||
|
||||
val kryo: KryoExtra = serialization.takeKryo()
|
||||
try {
|
||||
return kryo.read(buffer, offset, length, Serialization.NOP_CONNECTION)
|
||||
} catch (e: Exception) {
|
||||
logger.error("Error de-serializing message on connection ${header.sessionId()}!", e)
|
||||
} finally {
|
||||
serialization.returnKryo(kryo)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun readMessage(buffer: DirectBuffer, offset: Int, length: Int, header: Header, connection: Connection_) {
|
||||
// The sessionId is unique within a Subscription and unique across all Publication's from a sourceIdentity.
|
||||
val sessionId = header.sessionId()
|
||||
|
||||
// note: this address will ALWAYS be an IP:PORT combo
|
||||
// val remoteIpAndPort = (header.context() as Image).sourceIdentity()
|
||||
|
||||
// split
|
||||
// val splitPoint = remoteIpAndPort.lastIndexOf(':')
|
||||
// val ip = remoteIpAndPort.substring(0, splitPoint)
|
||||
// val port = remoteIpAndPort.substring(splitPoint+1)
|
||||
|
||||
// val ipAsInt = NetworkUtil.IP.toInt(ip)
|
||||
|
||||
|
||||
// TODO: WE MIGHT NOT WANT TO USE SESSIONID()!!
|
||||
var message: Any? = null
|
||||
|
||||
val kryo: KryoExtra = serialization.takeKryo()
|
||||
try {
|
||||
message = kryo.read(buffer, offset, length, connection)
|
||||
logger.debug("[{}] received: {}", sessionId, message)
|
||||
} catch (e: Exception) {
|
||||
logger.error("[${header.sessionId()}] Error de-serializing message", e)
|
||||
} finally {
|
||||
serialization.returnKryo(kryo)
|
||||
}
|
||||
|
||||
|
||||
val data = ByteArray(length)
|
||||
buffer.getBytes(offset, data)
|
||||
|
||||
|
||||
when (message) {
|
||||
is PingMessage -> {
|
||||
// the ping listener (internal use only!)
|
||||
|
||||
// val ping = message as PingMessage
|
||||
// if (ping.isReply) {
|
||||
// updatePingResponse(ping)
|
||||
// } else {
|
||||
// // return the ping from whence it came
|
||||
// ping.isReply = true
|
||||
// ping0(ping)
|
||||
// }
|
||||
}
|
||||
is RmiMessage -> {
|
||||
// if we are an RMI message/registration, we have very specific, defined behavior.
|
||||
// We do not use the "normal" listener callback pattern because this require special functionality
|
||||
// note: RMI messages are NEVER subclassed!
|
||||
connection.rmiSupport().manage(connection, message, logger)
|
||||
}
|
||||
is Any -> {
|
||||
connectionManager.notifyOnMessage(connection, message)
|
||||
}
|
||||
else -> {
|
||||
// do nothing, there were problems with the message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
override fun toString(): String {
|
||||
return "EndPoint [$name]"
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
val prime = 31
|
||||
var result = 1
|
||||
result = prime * result + (privateKey?.hashCode() ?: 0)
|
||||
result = prime * result + (publicKey?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
if (other == null) {
|
||||
return false
|
||||
}
|
||||
if (javaClass != other.javaClass) {
|
||||
return false
|
||||
}
|
||||
|
||||
val other1 = other as EndPoint<*>
|
||||
if (privateKey == null) {
|
||||
if (other1.privateKey != null) {
|
||||
return false
|
||||
}
|
||||
} else if (other1.privateKey == null) {
|
||||
return false
|
||||
} else if (!privateKey.encoded.contentEquals(other1.privateKey.encoded)) {
|
||||
return false
|
||||
}
|
||||
if (publicKey == null) {
|
||||
if (other1.publicKey != null) {
|
||||
return false
|
||||
}
|
||||
} else if (other1.publicKey == null) {
|
||||
return false
|
||||
} else if (!publicKey.contentEquals(other1.publicKey)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if this endpoint has been closed
|
||||
*/
|
||||
fun isShutdown(): Boolean {
|
||||
return shutdown.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for this endpoint to be closed
|
||||
*/
|
||||
fun waitForShutdown() {
|
||||
shutdownLatch.await()
|
||||
}
|
||||
|
||||
|
||||
override fun close() {
|
||||
if (shutdown.compareAndSet(expect = false, update = true)) {
|
||||
closables.forEach {
|
||||
it.close()
|
||||
}
|
||||
closables.clear()
|
||||
|
||||
actionDispatch.cancel()
|
||||
shutdownLatch.countDown()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,205 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
import dorkbox.network.ClientConfiguration;
|
||||
import dorkbox.network.connection.bridge.ConnectionBridge;
|
||||
import dorkbox.util.exceptions.SecurityException;
|
||||
|
||||
/**
|
||||
* This serves the purpose of making sure that specific methods are not available to the end user.
|
||||
*/
|
||||
public
|
||||
class EndPointClient extends EndPoint<ClientConfiguration> {
|
||||
|
||||
// is valid when there is a connection to the server, otherwise it is null
|
||||
protected volatile Connection connection;
|
||||
|
||||
private CountDownLatch registration;
|
||||
|
||||
private final Object bootstrapLock = new Object();
|
||||
protected List<BootstrapWrapper> bootstraps = new LinkedList<BootstrapWrapper>();
|
||||
private Iterator<BootstrapWrapper> bootstrapIterator;
|
||||
|
||||
protected volatile int connectionTimeout = 5000; // default is 5 seconds
|
||||
|
||||
private volatile long previousClosedConnectionActivity = 0;
|
||||
|
||||
private volatile ConnectionBridge connectionBridgeFlushAlways;
|
||||
|
||||
|
||||
protected
|
||||
EndPointClient(ClientConfiguration config) throws SecurityException, IOException {
|
||||
super(config);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Internal (to the networking stack) to notify the client that registration has COMPLETED. This is necessary because the client
|
||||
* will BLOCK until it has successfully registered it's connections.
|
||||
*/
|
||||
@Override
|
||||
final
|
||||
void connectionConnected0(final ConnectionImpl connection) {
|
||||
connectionBridgeFlushAlways = new ConnectionBridge() {
|
||||
@Override
|
||||
public
|
||||
ConnectionPoint self(Object message) {
|
||||
ConnectionPoint self = connection.self(message);
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ConnectionPoint TCP(Object message) {
|
||||
ConnectionPoint tcp = connection.TCP(message);
|
||||
|
||||
// needed to place back-pressure when writing too much data to the connection. Will create deadlocks if called from
|
||||
// INSIDE the event loop
|
||||
connection.controlBackPressure(tcp);
|
||||
|
||||
return tcp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ConnectionPoint UDP(Object message) {
|
||||
ConnectionPoint udp = connection.UDP(message);
|
||||
|
||||
// needed to place back-pressure when writing too much data to the connection. Will create deadlocks if called from
|
||||
// INSIDE the event loop
|
||||
connection.controlBackPressure(udp);
|
||||
return udp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
Ping ping() {
|
||||
Ping ping = connection.ping();
|
||||
return ping;
|
||||
}
|
||||
};
|
||||
|
||||
//noinspection unchecked
|
||||
this.connection = connection;
|
||||
|
||||
stopRegistration();
|
||||
|
||||
// invokes the listener.connection() method, and initialize the connection channels with whatever extra info they might need.
|
||||
// This will also start the RMI (if necessary) initialization/creation of objects
|
||||
super.connectionConnected0(connection);
|
||||
}
|
||||
|
||||
|
||||
private
|
||||
void stopRegistration() {
|
||||
// make sure we're not waiting on registration
|
||||
synchronized (bootstrapLock) {
|
||||
// we're done with registration, so no need to keep this around
|
||||
bootstrapIterator = null;
|
||||
while (registration.getCount() > 0) {
|
||||
registration.countDown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public
|
||||
ConnectionPoint send(final Object message) {
|
||||
ConnectionPoint send = connection.send(message);
|
||||
|
||||
// needed to place back-pressure when writing too much data to the connection. Will create deadlocks if called from
|
||||
// INSIDE the event loop
|
||||
((ConnectionImpl)connection).controlBackPressure(send);
|
||||
return send;
|
||||
}
|
||||
|
||||
public
|
||||
ConnectionPoint send(final Object message, final byte priority) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public
|
||||
ConnectionPoint sendUnreliable(final Object message) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public
|
||||
ConnectionPoint sendUnreliable(final Object message, final byte priority) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public
|
||||
Ping ping() {
|
||||
return null;
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Closes all connections ONLY (keeps the client running). To STOP the client, use stop().
|
||||
// * <p/>
|
||||
// * This is used, for example, when reconnecting to a server.
|
||||
// */
|
||||
// protected
|
||||
// void closeConnection() {
|
||||
// if (isConnected.get()) {
|
||||
// // make sure we're not waiting on registration
|
||||
// stopRegistration();
|
||||
//
|
||||
// // for the CLIENT only, we clear these connections! (the server only clears them on shutdown)
|
||||
//
|
||||
// // stop does the same as this + more. Only keep the listeners for connections IF we are the client. If we remove listeners as a client,
|
||||
// // ALL of the client logic will be lost. The server is reactive, so listeners are added to connections as needed (instead of before startup)
|
||||
// connectionManager.closeConnections(true);
|
||||
//
|
||||
// // Sometimes there might be "lingering" connections (ie, halfway though registration) that need to be closed.
|
||||
// registrationWrapper.clearSessions();
|
||||
//
|
||||
//
|
||||
// closeConnections(true);
|
||||
// shutdownAllChannels();
|
||||
// // shutdownEventLoops(); we don't do this here!
|
||||
//
|
||||
// connection = null;
|
||||
// isConnected.set(false);
|
||||
//
|
||||
// previousClosedConnectionActivity = System.nanoTime();
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Internal call to abort registration if the shutdown command is issued during channel registration.
|
||||
*/
|
||||
void abortRegistration() {
|
||||
// make sure we're not waiting on registration
|
||||
stopRegistration();
|
||||
}
|
||||
//
|
||||
// @Override
|
||||
// protected
|
||||
// void shutdownChannelsPre() {
|
||||
// closeConnection();
|
||||
//
|
||||
// // this calls connectionManager.stop()
|
||||
// super.shutdownChannelsPre();
|
||||
// }
|
||||
}
|
|
@ -1,191 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
import dorkbox.network.NetUtil;
|
||||
import dorkbox.network.ServerConfiguration;
|
||||
import dorkbox.network.connection.connectionType.ConnectionRule;
|
||||
import dorkbox.network.connection.connectionType.ConnectionType;
|
||||
import dorkbox.network.ipFilter.IpFilterRule;
|
||||
import dorkbox.network.ipFilter.IpFilterRuleType;
|
||||
import dorkbox.util.exceptions.SecurityException;
|
||||
|
||||
/**
|
||||
* This serves the purpose of making sure that specific methods are not available to the end user.
|
||||
*/
|
||||
public
|
||||
class EndPointServer extends EndPoint<ServerConfiguration> {
|
||||
|
||||
/**
|
||||
* Maintains a thread-safe collection of rules to allow/deny connectivity to this server.
|
||||
*/
|
||||
protected final CopyOnWriteArrayList<IpFilterRule> ipFilterRules = new CopyOnWriteArrayList<>();
|
||||
|
||||
/**
|
||||
* Maintains a thread-safe collection of rules used to define the connection type with this server.
|
||||
*/
|
||||
protected final CopyOnWriteArrayList<ConnectionRule> connectionRules = new CopyOnWriteArrayList<>();
|
||||
|
||||
public
|
||||
EndPointServer(final ServerConfiguration config) throws SecurityException, IOException {
|
||||
super(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely sends objects to a destination
|
||||
*/
|
||||
public
|
||||
ConnectionPoint send(final Object message) {
|
||||
return this.connectionManager.send(message);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* When called by a server, NORMALLY listeners are added at the GLOBAL level (meaning, I add one listener,
|
||||
* and ALL connections are notified of that listener.
|
||||
* <br>
|
||||
* It is POSSIBLE to add a server-connection 'local' listener (via connection.addListener), meaning that ONLY
|
||||
* that listener attached to the connection is notified on that event (ie, admin type listeners)
|
||||
*
|
||||
* @return a newly created listener manager for the connection
|
||||
*/
|
||||
final
|
||||
ConnectionManager addListenerManager(final Connection connection) {
|
||||
return this.connectionManager.addListenerManager(connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* When called by a server, NORMALLY listeners are added at the GLOBAL level (meaning, I add one listener,
|
||||
* and ALL connections are notified of that listener.
|
||||
* <br>
|
||||
* It is POSSIBLE to remove a server-connection 'local' listener (via connection.removeListener), meaning that ONLY
|
||||
* that listener attached to the connection is removed
|
||||
* <p/>
|
||||
* This removes the listener manager for that specific connection
|
||||
*/
|
||||
final
|
||||
void removeListenerManager(final Connection connection) {
|
||||
this.connectionManager.removeListenerManager(connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a custom connection to the server.
|
||||
* <p>
|
||||
* This should only be used in situations where there can be DIFFERENT types of connections (such as a 'web-based' connection) and
|
||||
* you want *this* server instance to manage listeners + message dispatch
|
||||
*
|
||||
* @param connection the connection to add
|
||||
*/
|
||||
public
|
||||
void add(Connection connection) {
|
||||
connectionManager.addConnection0(connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a custom connection to the server.
|
||||
* <p>
|
||||
* This should only be used in situations where there can be DIFFERENT types of connections (such as a 'web-based' connection) and
|
||||
* you want *this* server instance to manage listeners + message dispatch
|
||||
*
|
||||
* @param connection the connection to remove
|
||||
*/
|
||||
public
|
||||
void remove(Connection connection) {
|
||||
connectionManager.removeConnection(connection);
|
||||
}
|
||||
|
||||
// if no rules, then always yes
|
||||
// if rules, then default no unless a rule says yes. ACCEPT rules take precedence over REJECT (so if you have both rules, ACCEPT will happen)
|
||||
boolean acceptRemoteConnection(final InetSocketAddress remoteAddress) {
|
||||
int size = ipFilterRules.size();
|
||||
|
||||
if (size == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
InetAddress address = remoteAddress.getAddress();
|
||||
|
||||
// it's possible for a remote address to match MORE than 1 rule.
|
||||
boolean isAllowed = false;
|
||||
for (int i = 0; i < size; i++) {
|
||||
final IpFilterRule rule = ipFilterRules.get(i);
|
||||
if (rule == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isAllowed) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (rule.matches(remoteAddress)) {
|
||||
isAllowed = rule.ruleType() == IpFilterRuleType.ACCEPT;
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("Validating {} Connection allowed: {}", address, isAllowed);
|
||||
return isAllowed;
|
||||
}
|
||||
|
||||
// after the handshake, what sort of connection do we want (NONE, COMPRESS, ENCRYPT+COMPRESS)
|
||||
byte getConnectionUpgradeType(final InetSocketAddress remoteAddress) {
|
||||
InetAddress address = remoteAddress.getAddress();
|
||||
|
||||
int size = connectionRules.size();
|
||||
|
||||
// if it's unknown, then by default we encrypt the traffic
|
||||
ConnectionType connectionType = ConnectionType.COMPRESS_AND_ENCRYPT;
|
||||
if (size == 0 && address.equals(NetUtil.LOCALHOST)) {
|
||||
// if nothing is specified, then by default localhost is compression and everything else is encrypted
|
||||
connectionType = ConnectionType.COMPRESS;
|
||||
}
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
final ConnectionRule rule = connectionRules.get(i);
|
||||
if (rule == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (rule.matches(remoteAddress)) {
|
||||
connectionType = rule.ruleType();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("Validating {} Permitted type is: {}", remoteAddress, connectionType);
|
||||
return connectionType.getType();
|
||||
}
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public
|
||||
interface ISessionManager {
|
||||
/**
|
||||
* Called when a message is received.
|
||||
* <p>
|
||||
* Will auto-flush the connection queue if necessary.
|
||||
*/
|
||||
void onMessage(ConnectionImpl connection, Object message);
|
||||
|
||||
/**
|
||||
* Invoked when a Channel is open, bound to a local address, and connected to a remote address.
|
||||
*/
|
||||
void addConnection(ConnectionImpl connection);
|
||||
|
||||
/**
|
||||
* Invoked when a Channel is open, bound to a local address, and connected to a remote address.
|
||||
* <p>
|
||||
* Will auto-flush the connection queue if necessary.
|
||||
*/
|
||||
void onConnected(ConnectionImpl connection);
|
||||
|
||||
/**
|
||||
* Invoked when a Channel was disconnected from its remote peer.
|
||||
* <p>
|
||||
* Will auto-flush the connection queue if necessary.
|
||||
*/
|
||||
void onDisconnected(ConnectionImpl connection);
|
||||
|
||||
/**
|
||||
* Returns a non-modifiable list of active connections. This is extremely slow, and not recommended!
|
||||
*/
|
||||
<C extends Connection> Collection<C> getConnections();
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package dorkbox.network.connection
|
||||
|
||||
import dorkbox.network.aeron.client.ClientTimedOutException
|
||||
import io.aeron.Aeron
|
||||
import io.aeron.ChannelUriStringBuilder
|
||||
import io.aeron.Publication
|
||||
import io.aeron.Subscription
|
||||
|
||||
class IpcMediaDriverConnection(override val address: String,
|
||||
override val subscriptionPort: Int,
|
||||
override val publicationPort: Int,
|
||||
override val streamId: Int,
|
||||
override val sessionId: Int,
|
||||
private val connectionTimeoutMS: Long,
|
||||
override val isReliable: Boolean) : MediaDriverConnection {
|
||||
|
||||
override lateinit var subscription: Subscription
|
||||
override lateinit var publication: Publication
|
||||
|
||||
init {
|
||||
|
||||
}
|
||||
|
||||
fun subscriptionURI(): ChannelUriStringBuilder {
|
||||
return ChannelUriStringBuilder()
|
||||
.media("ipc")
|
||||
.controlMode("dynamic")
|
||||
}
|
||||
|
||||
// Create a publication at the given address and port, using the given stream ID.
|
||||
fun publicationURI(): ChannelUriStringBuilder {
|
||||
return ChannelUriStringBuilder()
|
||||
.media("ipc")
|
||||
}
|
||||
|
||||
|
||||
@Throws(ClientTimedOutException::class)
|
||||
override suspend fun buildClient(aeron: Aeron) {
|
||||
|
||||
}
|
||||
|
||||
override fun buildServer(aeron: Aeron) {
|
||||
|
||||
}
|
||||
|
||||
override fun clientInfo() : String {
|
||||
return ""
|
||||
}
|
||||
|
||||
override fun serverInfo() : String {
|
||||
return ""
|
||||
}
|
||||
|
||||
fun connect() : Pair<String, String> {
|
||||
return Pair("","")
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "$address [$subscriptionPort|$publicationPort] [$streamId|$sessionId]"
|
||||
}
|
||||
}
|
|
@ -1,478 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import com.esotericsoftware.kryo.Kryo;
|
||||
import com.esotericsoftware.kryo.io.Input;
|
||||
import com.esotericsoftware.kryo.io.Output;
|
||||
|
||||
import dorkbox.network.pipeline.ByteBufInput;
|
||||
import dorkbox.network.pipeline.ByteBufOutput;
|
||||
import dorkbox.network.rmi.ConnectionRmiSupport;
|
||||
import dorkbox.network.rmi.RmiNopConnection;
|
||||
import dorkbox.network.serialization.NetworkSerializationManager;
|
||||
import dorkbox.util.OS;
|
||||
import dorkbox.util.bytes.OptimizeUtilsByteArray;
|
||||
import dorkbox.util.bytes.OptimizeUtilsByteBuf;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufUtil;
|
||||
import net.jpountz.lz4.LZ4Compressor;
|
||||
import net.jpountz.lz4.LZ4Factory;
|
||||
import net.jpountz.lz4.LZ4FastDecompressor;
|
||||
|
||||
/**
|
||||
* Nothing in this class is thread safe
|
||||
*/
|
||||
@SuppressWarnings("Duplicates")
|
||||
public
|
||||
class KryoExtra extends Kryo {
|
||||
private static final int ABSOLUTE_MAX_SIZE_OBJECT = 500_000; // by default, this is about 500k
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private static final Connection_ NOP_CONNECTION = new RmiNopConnection();
|
||||
|
||||
// snappycomp : 7.534 micros/op; 518.5 MB/s (output: 55.1%)
|
||||
// snappyuncomp : 1.391 micros/op; 2808.1 MB/s
|
||||
// lz4comp : 6.210 micros/op; 629.0 MB/s (output: 55.4%)
|
||||
// lz4uncomp : 0.641 micros/op; 6097.9 MB/s
|
||||
private static final LZ4Factory factory = LZ4Factory.fastestInstance();
|
||||
|
||||
// for kryo serialization
|
||||
private final ByteBufInput readerBuff = new ByteBufInput();
|
||||
private final ByteBufOutput writerBuff = new ByteBufOutput();
|
||||
|
||||
// crypto + compression have to work with native byte arrays, so here we go...
|
||||
private final Input reader = new Input(ABSOLUTE_MAX_SIZE_OBJECT);
|
||||
private final Output writer = new Output(ABSOLUTE_MAX_SIZE_OBJECT);
|
||||
|
||||
private final byte[] temp = new byte[ABSOLUTE_MAX_SIZE_OBJECT];
|
||||
|
||||
|
||||
// volatile to provide object visibility for entire class. This is unique per connection
|
||||
public volatile ConnectionRmiSupport rmiSupport;
|
||||
|
||||
private static final String ALGORITHM = "AES/GCM/NoPadding";
|
||||
private static final int TAG_LENGTH_BIT = 128;
|
||||
private static final int IV_LENGTH_BYTE = 12;
|
||||
|
||||
private final SecureRandom secureRandom = new SecureRandom();
|
||||
private final Cipher cipher;
|
||||
|
||||
private LZ4Compressor compressor = factory.fastCompressor();
|
||||
private LZ4FastDecompressor decompressor = factory.fastDecompressor();
|
||||
|
||||
|
||||
|
||||
|
||||
private NetworkSerializationManager serializationManager;
|
||||
|
||||
public
|
||||
KryoExtra(final NetworkSerializationManager serializationManager) {
|
||||
super();
|
||||
|
||||
this.serializationManager = serializationManager;
|
||||
|
||||
try {
|
||||
cipher = Cipher.getInstance(ALGORITHM);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("could not get cipher instance", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is NOT ENCRYPTED
|
||||
*/
|
||||
public synchronized
|
||||
void write(final ByteBuf buffer, final Object message) throws IOException {
|
||||
write(NOP_CONNECTION, buffer, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is NOT ENCRYPTED
|
||||
*/
|
||||
public synchronized
|
||||
void write(final Connection_ connection, final ByteBuf buffer, final Object message) throws IOException {
|
||||
// required by RMI and some serializers to determine which connection wrote (or has info about) this object
|
||||
this.rmiSupport = connection.rmiSupport();
|
||||
|
||||
// write the object to the NORMAL output buffer!
|
||||
writerBuff.setBuffer(buffer);
|
||||
|
||||
writeClassAndObject(writerBuff, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* ++++++++++++++++++++++++++
|
||||
* + class and object bytes +
|
||||
* ++++++++++++++++++++++++++
|
||||
*/
|
||||
public synchronized
|
||||
Object read(final ByteBuf buffer) throws IOException {
|
||||
return read(NOP_CONNECTION, buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* ++++++++++++++++++++++++++
|
||||
* + class and object bytes +
|
||||
* ++++++++++++++++++++++++++
|
||||
*/
|
||||
public synchronized
|
||||
Object read(final Connection_ connection, final ByteBuf buffer) throws IOException {
|
||||
// required by RMI and some serializers to determine which connection wrote (or has info about) this object
|
||||
this.rmiSupport = connection.rmiSupport();
|
||||
|
||||
// read the object from the buffer.
|
||||
readerBuff.setBuffer(buffer);
|
||||
|
||||
// this properly sets the readerIndex, but only if it's the correct buffer
|
||||
return readClassAndObject(readerBuff);
|
||||
}
|
||||
|
||||
////////////////
|
||||
////////////////
|
||||
////////////////
|
||||
// for more complicated writes, sadly, we have to deal DIRECTLY with byte arrays
|
||||
////////////////
|
||||
////////////////
|
||||
////////////////
|
||||
|
||||
/**
|
||||
* OUTPUT:
|
||||
* ++++++++++++++++++++++++++
|
||||
* + class and object bytes +
|
||||
* ++++++++++++++++++++++++++
|
||||
*/
|
||||
private
|
||||
void write(final Connection_ connection, final Output writer, final Object message) {
|
||||
// required by RMI and some serializers to determine which connection wrote (or has info about) this object
|
||||
this.rmiSupport = connection.rmiSupport();
|
||||
|
||||
// write the object to the NORMAL output buffer!
|
||||
writer.reset();
|
||||
|
||||
writeClassAndObject(writer, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* INPUT:
|
||||
* ++++++++++++++++++++++++++
|
||||
* + class and object bytes +
|
||||
* ++++++++++++++++++++++++++
|
||||
*/
|
||||
private
|
||||
Object read(final Connection_ connection, final Input reader) {
|
||||
// required by RMI and some serializers to determine which connection wrote (or has info about) this object
|
||||
this.rmiSupport = connection.rmiSupport();
|
||||
|
||||
return readClassAndObject(reader);
|
||||
}
|
||||
|
||||
|
||||
public synchronized
|
||||
void writeCompressed(final Logger logger, final ByteBuf buffer, final Object message) throws IOException {
|
||||
writeCompressed(logger, NOP_CONNECTION, buffer, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* BUFFER:
|
||||
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
* + uncompressed length (1-4 bytes) + compressed data +
|
||||
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
*
|
||||
* COMPRESSED DATA:
|
||||
* ++++++++++++++++++++++++++
|
||||
* + class and object bytes +
|
||||
* ++++++++++++++++++++++++++
|
||||
*/
|
||||
public synchronized
|
||||
void writeCompressed(final Logger logger, final Connection_ connection, final ByteBuf buffer, final Object message) throws IOException {
|
||||
// write the object to a TEMP buffer! this will be compressed later
|
||||
write(connection, writer, message);
|
||||
|
||||
// save off how much data the object took
|
||||
int length = writer.position();
|
||||
int maxCompressedLength = compressor.maxCompressedLength(length);
|
||||
|
||||
////////// compressing data
|
||||
// we ALWAYS compress our data stream -- because of how AES-GCM pads data out, the small input (that would result in a larger
|
||||
// output), will be negated by the increase in size by the encryption
|
||||
byte[] compressOutput = temp;
|
||||
|
||||
// LZ4 compress.
|
||||
int compressedLength = compressor.compress(writer.getBuffer(), 0, length, compressOutput, 0, maxCompressedLength);
|
||||
|
||||
if (DEBUG) {
|
||||
String orig = ByteBufUtil.hexDump(writer.getBuffer(), 0, length);
|
||||
String compressed = ByteBufUtil.hexDump(compressOutput, 0, compressedLength);
|
||||
logger.error(OS.LINE_SEPARATOR +
|
||||
"ORIG: (" + length + ")" + OS.LINE_SEPARATOR + orig +
|
||||
OS.LINE_SEPARATOR +
|
||||
"COMPRESSED: (" + compressedLength + ")" + OS.LINE_SEPARATOR + compressed);
|
||||
}
|
||||
|
||||
// now write the ORIGINAL (uncompressed) length. This is so we can use the FAST decompress version
|
||||
OptimizeUtilsByteBuf.writeInt(buffer, length, true);
|
||||
|
||||
// have to copy over the orig data, because we used the temp buffer. Also have to account for the length of the uncompressed size
|
||||
buffer.writeBytes(compressOutput, 0, compressedLength);
|
||||
}
|
||||
|
||||
public synchronized
|
||||
Object readCompressed(final Logger logger, final ByteBuf buffer, int length) throws IOException {
|
||||
return readCompressed(logger, NOP_CONNECTION, buffer, length);
|
||||
}
|
||||
|
||||
/**
|
||||
* BUFFER:
|
||||
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
* + uncompressed length (1-4 bytes) + compressed data +
|
||||
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
*
|
||||
* COMPRESSED DATA:
|
||||
* ++++++++++++++++++++++++++
|
||||
* + class and object bytes +
|
||||
* ++++++++++++++++++++++++++
|
||||
*/
|
||||
public synchronized
|
||||
Object readCompressed(final Logger logger, final Connection_ connection, final ByteBuf buffer, int length) throws IOException {
|
||||
////////////////
|
||||
// Note: we CANNOT write BACK to the buffer as "temp" storage, since there could be additional data on it!
|
||||
////////////////
|
||||
|
||||
// get the decompressed length (at the beginning of the array)
|
||||
final int uncompressedLength = OptimizeUtilsByteBuf.readInt(buffer, true);
|
||||
if (uncompressedLength > ABSOLUTE_MAX_SIZE_OBJECT) {
|
||||
throw new IOException("Uncompressed size (" + uncompressedLength + ") is larger than max allowed size (" + ABSOLUTE_MAX_SIZE_OBJECT + ")!");
|
||||
}
|
||||
|
||||
// because 1-4 bytes for the decompressed size (this number is never negative)
|
||||
final int lengthLength = OptimizeUtilsByteArray.intLength(uncompressedLength, true);
|
||||
|
||||
int start = buffer.readerIndex();
|
||||
|
||||
// have to adjust for uncompressed length-length
|
||||
length = length - lengthLength;
|
||||
|
||||
|
||||
///////// decompress data
|
||||
buffer.readBytes(temp, 0, length);
|
||||
|
||||
|
||||
// LZ4 decompress, requires the size of the ORIGINAL length (because we use the FAST decompressor)
|
||||
reader.reset();
|
||||
decompressor.decompress(temp, 0, reader.getBuffer(), 0, uncompressedLength);
|
||||
reader.setLimit(uncompressedLength);
|
||||
|
||||
if (DEBUG) {
|
||||
String compressed = ByteBufUtil.hexDump(buffer, start, length);
|
||||
String orig = ByteBufUtil.hexDump(reader.getBuffer(), start, uncompressedLength);
|
||||
logger.error(OS.LINE_SEPARATOR +
|
||||
"COMPRESSED: (" + length + ")" + OS.LINE_SEPARATOR + compressed +
|
||||
OS.LINE_SEPARATOR +
|
||||
"ORIG: (" + uncompressedLength + ")" + OS.LINE_SEPARATOR + orig);
|
||||
}
|
||||
|
||||
// read the object from the buffer.
|
||||
return read(connection, reader);
|
||||
}
|
||||
|
||||
/**
|
||||
* BUFFER:
|
||||
* +++++++++++++++++++++++++++++++
|
||||
* + IV (12) + encrypted data +
|
||||
* +++++++++++++++++++++++++++++++
|
||||
*
|
||||
* ENCRYPTED DATA:
|
||||
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
* + uncompressed length (1-4 bytes) + compressed data +
|
||||
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
*
|
||||
* COMPRESSED DATA:
|
||||
* ++++++++++++++++++++++++++
|
||||
* + class and object bytes +
|
||||
* ++++++++++++++++++++++++++
|
||||
*/
|
||||
public synchronized
|
||||
void writeCrypto(final Logger logger, final Connection_ connection, final ByteBuf buffer, final Object message) throws IOException {
|
||||
// write the object to a TEMP buffer! this will be compressed later
|
||||
write(connection, writer, message);
|
||||
|
||||
// save off how much data the object took
|
||||
int length = writer.position();
|
||||
int maxCompressedLength = compressor.maxCompressedLength(length);
|
||||
|
||||
////////// compressing data
|
||||
// we ALWAYS compress our data stream -- because of how AES-GCM pads data out, the small input (that would result in a larger
|
||||
// output), will be negated by the increase in size by the encryption
|
||||
byte[] compressOutput = temp;
|
||||
|
||||
// LZ4 compress. Offset by 4 in the dest array so we have room for the length
|
||||
int compressedLength = compressor.compress(writer.getBuffer(), 0, length, compressOutput, 4, maxCompressedLength);
|
||||
|
||||
if (DEBUG) {
|
||||
String orig = ByteBufUtil.hexDump(writer.getBuffer(), 0, length);
|
||||
String compressed = ByteBufUtil.hexDump(compressOutput, 4, compressedLength);
|
||||
logger.error(OS.LINE_SEPARATOR +
|
||||
"ORIG: (" + length + ")" + OS.LINE_SEPARATOR + orig +
|
||||
OS.LINE_SEPARATOR +
|
||||
"COMPRESSED: (" + compressedLength + ")" + OS.LINE_SEPARATOR + compressed);
|
||||
}
|
||||
|
||||
// now write the ORIGINAL (uncompressed) length. This is so we can use the FAST decompress version
|
||||
final int lengthLength = OptimizeUtilsByteArray.intLength(length, true);
|
||||
|
||||
// this is where we start writing the length data, so that the end of this lines up with the compressed data
|
||||
int start = 4 - lengthLength;
|
||||
|
||||
OptimizeUtilsByteArray.writeInt(compressOutput, length, true, start);
|
||||
|
||||
// now compressOutput contains "uncompressed length + data"
|
||||
int compressedArrayLength = lengthLength + compressedLength;
|
||||
|
||||
|
||||
/////// encrypting data.
|
||||
final SecretKey cryptoKey = connection.cryptoKey();
|
||||
|
||||
byte[] iv = new byte[IV_LENGTH_BYTE]; // NEVER REUSE THIS IV WITH SAME KEY
|
||||
secureRandom.nextBytes(iv);
|
||||
|
||||
GCMParameterSpec parameterSpec = new GCMParameterSpec(TAG_LENGTH_BIT, iv); // 128 bit auth tag length
|
||||
try {
|
||||
cipher.init(Cipher.ENCRYPT_MODE, cryptoKey, parameterSpec);
|
||||
} catch (Exception e) {
|
||||
throw new IOException("Unable to AES encrypt the data", e);
|
||||
}
|
||||
|
||||
// we REUSE the writer buffer! (since that data is now compressed in a different array)
|
||||
|
||||
int encryptedLength;
|
||||
try {
|
||||
encryptedLength = cipher.doFinal(compressOutput, start, compressedArrayLength, writer.getBuffer(), 0);
|
||||
} catch (Exception e) {
|
||||
throw new IOException("Unable to AES encrypt the data", e);
|
||||
}
|
||||
|
||||
// write out our IV
|
||||
buffer.writeBytes(iv, 0, IV_LENGTH_BYTE);
|
||||
|
||||
Arrays.fill(iv, (byte) 0); // overwrite the IV with zeros so we can't leak this value
|
||||
|
||||
// have to copy over the orig data, because we used the temp buffer
|
||||
buffer.writeBytes(writer.getBuffer(), 0, encryptedLength);
|
||||
|
||||
if (DEBUG) {
|
||||
String ivString = ByteBufUtil.hexDump(iv, 0, IV_LENGTH_BYTE);
|
||||
String crypto = ByteBufUtil.hexDump(writer.getBuffer(), 0, encryptedLength);
|
||||
logger.error(OS.LINE_SEPARATOR +
|
||||
"IV: (12)" + OS.LINE_SEPARATOR + ivString +
|
||||
OS.LINE_SEPARATOR +
|
||||
"CRYPTO: (" + encryptedLength + ")" + OS.LINE_SEPARATOR + crypto);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* BUFFER:
|
||||
* +++++++++++++++++++++++++++++++
|
||||
* + IV (12) + encrypted data +
|
||||
* +++++++++++++++++++++++++++++++
|
||||
*
|
||||
* ENCRYPTED DATA:
|
||||
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
* + uncompressed length (1-4 bytes) + compressed data +
|
||||
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
*
|
||||
* COMPRESSED DATA:
|
||||
* ++++++++++++++++++++++++++
|
||||
* + class and object bytes +
|
||||
* ++++++++++++++++++++++++++
|
||||
*/
|
||||
public
|
||||
Object readCrypto(final Logger logger, final Connection_ connection, final ByteBuf buffer, int length) throws IOException {
|
||||
// read out the crypto IV
|
||||
final byte[] iv = new byte[IV_LENGTH_BYTE];
|
||||
buffer.readBytes(iv, 0 , IV_LENGTH_BYTE);
|
||||
|
||||
// have to adjust for the IV
|
||||
length = length - IV_LENGTH_BYTE;
|
||||
|
||||
/////////// decrypt data
|
||||
final SecretKey cryptoKey = connection.cryptoKey();
|
||||
|
||||
try {
|
||||
cipher.init(Cipher.DECRYPT_MODE, cryptoKey, new GCMParameterSpec(TAG_LENGTH_BIT, iv));
|
||||
} catch (Exception e) {
|
||||
throw new IOException("Unable to AES decrypt the data", e);
|
||||
}
|
||||
|
||||
// have to copy out bytes, we reuse the reader byte array!
|
||||
buffer.readBytes(reader.getBuffer(), 0, length);
|
||||
|
||||
if (DEBUG) {
|
||||
String ivString = ByteBufUtil.hexDump(iv, 0, IV_LENGTH_BYTE);
|
||||
String crypto = ByteBufUtil.hexDump(reader.getBuffer(), 0, length);
|
||||
logger.error("IV: (12)" + OS.LINE_SEPARATOR + ivString +
|
||||
OS.LINE_SEPARATOR + "CRYPTO: (" + length + ")" + OS.LINE_SEPARATOR + crypto);
|
||||
}
|
||||
|
||||
int decryptedLength;
|
||||
try {
|
||||
decryptedLength = cipher.doFinal(reader.getBuffer(),0, length, temp, 0);
|
||||
} catch (Exception e) {
|
||||
throw new IOException("Unable to AES decrypt the data", e);
|
||||
}
|
||||
|
||||
///////// decompress data -- as it's ALWAYS compressed
|
||||
|
||||
// get the decompressed length (at the beginning of the array)
|
||||
final int uncompressedLength = OptimizeUtilsByteArray.readInt(temp, true);
|
||||
|
||||
// where does our data start, AFTER the length field
|
||||
int start = OptimizeUtilsByteArray.intLength(uncompressedLength, true); // because 1-4 bytes for the uncompressed size;
|
||||
|
||||
// LZ4 decompress, requires the size of the ORIGINAL length (because we use the FAST decompressor
|
||||
reader.reset();
|
||||
decompressor.decompress(temp, start, reader.getBuffer(), 0, uncompressedLength);
|
||||
reader.setLimit(uncompressedLength);
|
||||
|
||||
if (DEBUG) {
|
||||
int endWithoutUncompressedLength = decryptedLength - start;
|
||||
String compressed = ByteBufUtil.hexDump(temp, start, endWithoutUncompressedLength);
|
||||
String orig = ByteBufUtil.hexDump(reader.getBuffer(), 0, uncompressedLength);
|
||||
logger.error("COMPRESSED: (" + endWithoutUncompressedLength + ")" + OS.LINE_SEPARATOR + compressed +
|
||||
OS.LINE_SEPARATOR +
|
||||
"ORIG: (" + uncompressedLength + ")" + OS.LINE_SEPARATOR + orig);
|
||||
}
|
||||
|
||||
// read the object from the buffer.
|
||||
return read(connection, reader);
|
||||
}
|
||||
|
||||
public
|
||||
NetworkSerializationManager getSerializationManager() {
|
||||
return serializationManager;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,409 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection
|
||||
|
||||
import com.esotericsoftware.kryo.Kryo
|
||||
import com.esotericsoftware.kryo.io.Input
|
||||
import com.esotericsoftware.kryo.io.Output
|
||||
import dorkbox.network.pipeline.AeronInput
|
||||
import dorkbox.network.pipeline.AeronOutput
|
||||
import dorkbox.network.rmi.ConnectionRmiSupport
|
||||
import dorkbox.network.serialization.NetworkSerializationManager
|
||||
import dorkbox.util.OS
|
||||
import dorkbox.util.bytes.OptimizeUtilsByteArray
|
||||
import dorkbox.util.bytes.OptimizeUtilsByteBuf
|
||||
import io.netty.buffer.ByteBuf
|
||||
import io.netty.buffer.ByteBufUtil
|
||||
import net.jpountz.lz4.LZ4Factory
|
||||
import org.agrona.DirectBuffer
|
||||
import org.slf4j.Logger
|
||||
import java.io.IOException
|
||||
import java.security.SecureRandom
|
||||
import javax.crypto.Cipher
|
||||
|
||||
/**
|
||||
* Nothing in this class is thread safe
|
||||
*/
|
||||
class KryoExtra(val serializationManager: NetworkSerializationManager) : Kryo() {
|
||||
// for kryo serialization
|
||||
private val readerBuffer = AeronInput()
|
||||
val writerBuffer = AeronOutput()
|
||||
|
||||
// crypto + compression have to work with native byte arrays, so here we go...
|
||||
private val reader = Input(ABSOLUTE_MAX_SIZE_OBJECT)
|
||||
private val writer = Output(ABSOLUTE_MAX_SIZE_OBJECT)
|
||||
private val temp = ByteArray(ABSOLUTE_MAX_SIZE_OBJECT)
|
||||
|
||||
|
||||
|
||||
// volatile to provide object visibility for entire class. This is unique per connection
|
||||
lateinit var rmiSupport: ConnectionRmiSupport
|
||||
lateinit var connection: Connection_
|
||||
|
||||
private val secureRandom = SecureRandom()
|
||||
private var cipher: Cipher? = null
|
||||
private val compressor = factory.fastCompressor()
|
||||
private val decompressor = factory.fastDecompressor()
|
||||
|
||||
|
||||
companion object {
|
||||
private const val ABSOLUTE_MAX_SIZE_OBJECT = 500000 // by default, this is about 500k
|
||||
private const val DEBUG = false
|
||||
|
||||
// snappycomp : 7.534 micros/op; 518.5 MB/s (output: 55.1%)
|
||||
// snappyuncomp : 1.391 micros/op; 2808.1 MB/s
|
||||
// lz4comp : 6.210 micros/op; 629.0 MB/s (output: 55.4%)
|
||||
// lz4uncomp : 0.641 micros/op; 6097.9 MB/s
|
||||
private val factory = LZ4Factory.fastestInstance()
|
||||
private const val ALGORITHM = "AES/GCM/NoPadding"
|
||||
private const val TAG_LENGTH_BIT = 128
|
||||
private const val IV_LENGTH_BYTE = 12
|
||||
}
|
||||
|
||||
init {
|
||||
cipher = try {
|
||||
Cipher.getInstance(ALGORITHM)
|
||||
} catch (e: Exception) {
|
||||
throw IllegalStateException("could not get cipher instance", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OUTPUT:
|
||||
* ++++++++++++++++++++++++++
|
||||
* + class and object bytes +
|
||||
* ++++++++++++++++++++++++++
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
fun write(connection: Connection_, message: Any) {
|
||||
// required by RMI and some serializers to determine which connection wrote (or has info about) this object
|
||||
this.connection = connection
|
||||
this.rmiSupport = connection.rmiSupport()
|
||||
|
||||
writerBuffer.reset()
|
||||
writeClassAndObject(writerBuffer, message)
|
||||
}
|
||||
|
||||
/**
|
||||
* INPUT:
|
||||
* ++++++++++++++++++++++++++
|
||||
* + class and object bytes +
|
||||
* ++++++++++++++++++++++++++
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
fun read(buffer: DirectBuffer, offset: Int, length: Int, connection: Connection_): Any {
|
||||
// required by RMI and some serializers to determine which connection wrote (or has info about) this object
|
||||
this.connection = connection
|
||||
this.rmiSupport = connection.rmiSupport()
|
||||
|
||||
// this properly sets the buffer info
|
||||
readerBuffer.setBuffer(buffer, offset, length)
|
||||
|
||||
return readClassAndObject(readerBuffer)
|
||||
}
|
||||
|
||||
|
||||
////////////////
|
||||
////////////////
|
||||
////////////////
|
||||
// for more complicated writes, sadly, we have to deal DIRECTLY with byte arrays
|
||||
////////////////
|
||||
////////////////
|
||||
////////////////
|
||||
|
||||
/**
|
||||
* OUTPUT:
|
||||
* ++++++++++++++++++++++++++
|
||||
* + class and object bytes +
|
||||
* ++++++++++++++++++++++++++
|
||||
*/
|
||||
private fun write(connection: Connection_, writer: Output, message: Any) {
|
||||
// required by RMI and some serializers to determine which connection wrote (or has info about) this object
|
||||
this.connection = connection
|
||||
this.rmiSupport = connection.rmiSupport()
|
||||
|
||||
// write the object to the NORMAL output buffer!
|
||||
writer.reset()
|
||||
writeClassAndObject(writer, message)
|
||||
}
|
||||
|
||||
/**
|
||||
* INPUT:
|
||||
* ++++++++++++++++++++++++++
|
||||
* + class and object bytes +
|
||||
* ++++++++++++++++++++++++++
|
||||
*/
|
||||
private fun read(connection: Connection_, reader: Input): Any {
|
||||
// required by RMI and some serializers to determine which connection wrote (or has info about) this object
|
||||
this.connection = connection
|
||||
this.rmiSupport = connection.rmiSupport()
|
||||
|
||||
return readClassAndObject(reader)
|
||||
}
|
||||
|
||||
/**
|
||||
* BUFFER:
|
||||
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
* + uncompressed length (1-4 bytes) + compressed data +
|
||||
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
*
|
||||
* COMPRESSED DATA:
|
||||
* ++++++++++++++++++++++++++
|
||||
* + class and object bytes +
|
||||
* ++++++++++++++++++++++++++
|
||||
*/
|
||||
fun writeCompressed(logger: Logger, connection: Connection_, buffer: ByteBuf, message: Any) {
|
||||
// write the object to a TEMP buffer! this will be compressed later
|
||||
write(connection, writer, message)
|
||||
|
||||
// save off how much data the object took
|
||||
val length = writer.position()
|
||||
val maxCompressedLength = compressor.maxCompressedLength(length)
|
||||
|
||||
////////// compressing data
|
||||
// we ALWAYS compress our data stream -- because of how AES-GCM pads data out, the small input (that would result in a larger
|
||||
// output), will be negated by the increase in size by the encryption
|
||||
val compressOutput = temp
|
||||
|
||||
// LZ4 compress.
|
||||
val compressedLength = compressor.compress(writer.buffer, 0, length, compressOutput, 0, maxCompressedLength)
|
||||
if (DEBUG) {
|
||||
val orig = ByteBufUtil.hexDump(writer.buffer, 0, length)
|
||||
val compressed = ByteBufUtil.hexDump(compressOutput, 0, compressedLength)
|
||||
logger.error(OS.LINE_SEPARATOR +
|
||||
"ORIG: (" + length + ")" + OS.LINE_SEPARATOR + orig +
|
||||
OS.LINE_SEPARATOR +
|
||||
"COMPRESSED: (" + compressedLength + ")" + OS.LINE_SEPARATOR + compressed)
|
||||
}
|
||||
|
||||
// now write the ORIGINAL (uncompressed) length. This is so we can use the FAST decompress version
|
||||
OptimizeUtilsByteBuf.writeInt(buffer, length, true)
|
||||
|
||||
// have to copy over the orig data, because we used the temp buffer. Also have to account for the length of the uncompressed size
|
||||
buffer.writeBytes(compressOutput, 0, compressedLength)
|
||||
}
|
||||
|
||||
/**
|
||||
* BUFFER:
|
||||
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
* + uncompressed length (1-4 bytes) + compressed data +
|
||||
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
*
|
||||
* COMPRESSED DATA:
|
||||
* ++++++++++++++++++++++++++
|
||||
* + class and object bytes +
|
||||
* ++++++++++++++++++++++++++
|
||||
*/
|
||||
fun readCompressed(logger: Logger, connection: Connection_, buffer: ByteBuf, length: Int): Any {
|
||||
////////////////
|
||||
// Note: we CANNOT write BACK to the buffer as "temp" storage, since there could be additional data on it!
|
||||
////////////////
|
||||
|
||||
// get the decompressed length (at the beginning of the array)
|
||||
var length = length
|
||||
val uncompressedLength = OptimizeUtilsByteBuf.readInt(buffer, true)
|
||||
if (uncompressedLength > ABSOLUTE_MAX_SIZE_OBJECT) {
|
||||
throw IOException("Uncompressed size ($uncompressedLength) is larger than max allowed size ($ABSOLUTE_MAX_SIZE_OBJECT)!")
|
||||
}
|
||||
|
||||
// because 1-4 bytes for the decompressed size (this number is never negative)
|
||||
val lengthLength = OptimizeUtilsByteArray.intLength(uncompressedLength, true)
|
||||
val start = buffer.readerIndex()
|
||||
|
||||
// have to adjust for uncompressed length-length
|
||||
length = length - lengthLength
|
||||
|
||||
|
||||
///////// decompress data
|
||||
buffer.readBytes(temp, 0, length)
|
||||
|
||||
|
||||
// LZ4 decompress, requires the size of the ORIGINAL length (because we use the FAST decompressor)
|
||||
reader.reset()
|
||||
decompressor.decompress(temp, 0, reader.buffer, 0, uncompressedLength)
|
||||
reader.setLimit(uncompressedLength)
|
||||
if (DEBUG) {
|
||||
val compressed = ByteBufUtil.hexDump(buffer, start, length)
|
||||
val orig = ByteBufUtil.hexDump(reader.buffer, start, uncompressedLength)
|
||||
logger.error(OS.LINE_SEPARATOR +
|
||||
"COMPRESSED: (" + length + ")" + OS.LINE_SEPARATOR + compressed +
|
||||
OS.LINE_SEPARATOR +
|
||||
"ORIG: (" + uncompressedLength + ")" + OS.LINE_SEPARATOR + orig)
|
||||
}
|
||||
|
||||
// read the object from the buffer.
|
||||
return read(connection, reader)
|
||||
}
|
||||
|
||||
/**
|
||||
* BUFFER:
|
||||
* +++++++++++++++++++++++++++++++
|
||||
* + IV (12) + encrypted data +
|
||||
* +++++++++++++++++++++++++++++++
|
||||
*
|
||||
* ENCRYPTED DATA:
|
||||
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
* + uncompressed length (1-4 bytes) + compressed data +
|
||||
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
*
|
||||
* COMPRESSED DATA:
|
||||
* ++++++++++++++++++++++++++
|
||||
* + class and object bytes +
|
||||
* ++++++++++++++++++++++++++
|
||||
*/
|
||||
// fun writeCrypto(logger: Logger, connection: Connection_, buffer: ByteBuf, message: Any) {
|
||||
// // write the object to a TEMP buffer! this will be compressed later
|
||||
// write(connection, writer, message)
|
||||
//
|
||||
// // save off how much data the object took
|
||||
// val length = writer.position()
|
||||
// val maxCompressedLength = compressor.maxCompressedLength(length)
|
||||
//
|
||||
// ////////// compressing data
|
||||
// // we ALWAYS compress our data stream -- because of how AES-GCM pads data out, the small input (that would result in a larger
|
||||
// // output), will be negated by the increase in size by the encryption
|
||||
// val compressOutput = temp
|
||||
//
|
||||
// // LZ4 compress. Offset by 4 in the dest array so we have room for the length
|
||||
// val compressedLength = compressor.compress(writer.buffer, 0, length, compressOutput, 4, maxCompressedLength)
|
||||
// if (DEBUG) {
|
||||
// val orig = ByteBufUtil.hexDump(writer.buffer, 0, length)
|
||||
// val compressed = ByteBufUtil.hexDump(compressOutput, 4, compressedLength)
|
||||
// logger.error(OS.LINE_SEPARATOR +
|
||||
// "ORIG: (" + length + ")" + OS.LINE_SEPARATOR + orig +
|
||||
// OS.LINE_SEPARATOR +
|
||||
// "COMPRESSED: (" + compressedLength + ")" + OS.LINE_SEPARATOR + compressed)
|
||||
// }
|
||||
//
|
||||
// // now write the ORIGINAL (uncompressed) length. This is so we can use the FAST decompress version
|
||||
// val lengthLength = OptimizeUtilsByteArray.intLength(length, true)
|
||||
//
|
||||
// // this is where we start writing the length data, so that the end of this lines up with the compressed data
|
||||
// val start = 4 - lengthLength
|
||||
// OptimizeUtilsByteArray.writeInt(compressOutput, length, true, start)
|
||||
//
|
||||
// // now compressOutput contains "uncompressed length + data"
|
||||
// val compressedArrayLength = lengthLength + compressedLength
|
||||
//
|
||||
//
|
||||
// /////// encrypting data.
|
||||
// val cryptoKey = connection.cryptoKey()
|
||||
// val iv = ByteArray(IV_LENGTH_BYTE) // NEVER REUSE THIS IV WITH SAME KEY
|
||||
// secureRandom.nextBytes(iv)
|
||||
// val parameterSpec = GCMParameterSpec(TAG_LENGTH_BIT, iv) // 128 bit auth tag length
|
||||
// try {
|
||||
// cipher!!.init(Cipher.ENCRYPT_MODE, cryptoKey, parameterSpec)
|
||||
// } catch (e: Exception) {
|
||||
// throw IOException("Unable to AES encrypt the data", e)
|
||||
// }
|
||||
//
|
||||
// // we REUSE the writer buffer! (since that data is now compressed in a different array)
|
||||
// val encryptedLength: Int
|
||||
// encryptedLength = try {
|
||||
// cipher!!.doFinal(compressOutput, start, compressedArrayLength, writer.buffer, 0)
|
||||
// } catch (e: Exception) {
|
||||
// throw IOException("Unable to AES encrypt the data", e)
|
||||
// }
|
||||
//
|
||||
// // write out our IV
|
||||
// buffer.writeBytes(iv, 0, IV_LENGTH_BYTE)
|
||||
// Arrays.fill(iv, 0.toByte()) // overwrite the IV with zeros so we can't leak this value
|
||||
//
|
||||
// // have to copy over the orig data, because we used the temp buffer
|
||||
// buffer.writeBytes(writer.buffer, 0, encryptedLength)
|
||||
// if (DEBUG) {
|
||||
// val ivString = ByteBufUtil.hexDump(iv, 0, IV_LENGTH_BYTE)
|
||||
// val crypto = ByteBufUtil.hexDump(writer.buffer, 0, encryptedLength)
|
||||
// logger.error(OS.LINE_SEPARATOR +
|
||||
// "IV: (12)" + OS.LINE_SEPARATOR + ivString +
|
||||
// OS.LINE_SEPARATOR +
|
||||
// "CRYPTO: (" + encryptedLength + ")" + OS.LINE_SEPARATOR + crypto)
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* BUFFER:
|
||||
* +++++++++++++++++++++++++++++++
|
||||
* + IV (12) + encrypted data +
|
||||
* +++++++++++++++++++++++++++++++
|
||||
*
|
||||
* ENCRYPTED DATA:
|
||||
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
* + uncompressed length (1-4 bytes) + compressed data +
|
||||
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
*
|
||||
* COMPRESSED DATA:
|
||||
* ++++++++++++++++++++++++++
|
||||
* + class and object bytes +
|
||||
* ++++++++++++++++++++++++++
|
||||
*/
|
||||
// fun readCrypto(logger: Logger, connection: Connection_, buffer: ByteBuf, length: Int): Any {
|
||||
// // read out the crypto IV
|
||||
// var length = length
|
||||
// val iv = ByteArray(IV_LENGTH_BYTE)
|
||||
// buffer.readBytes(iv, 0, IV_LENGTH_BYTE)
|
||||
//
|
||||
// // have to adjust for the IV
|
||||
// length = length - IV_LENGTH_BYTE
|
||||
//
|
||||
// /////////// decrypt data
|
||||
// val cryptoKey = connection.cryptoKey()
|
||||
// try {
|
||||
// cipher!!.init(Cipher.DECRYPT_MODE, cryptoKey, GCMParameterSpec(TAG_LENGTH_BIT, iv))
|
||||
// } catch (e: Exception) {
|
||||
// throw IOException("Unable to AES decrypt the data", e)
|
||||
// }
|
||||
//
|
||||
// // have to copy out bytes, we reuse the reader byte array!
|
||||
// buffer.readBytes(reader.buffer, 0, length)
|
||||
// if (DEBUG) {
|
||||
// val ivString = ByteBufUtil.hexDump(iv, 0, IV_LENGTH_BYTE)
|
||||
// val crypto = ByteBufUtil.hexDump(reader.buffer, 0, length)
|
||||
// logger.error("IV: (12)" + OS.LINE_SEPARATOR + ivString +
|
||||
// OS.LINE_SEPARATOR + "CRYPTO: (" + length + ")" + OS.LINE_SEPARATOR + crypto)
|
||||
// }
|
||||
// val decryptedLength: Int
|
||||
// decryptedLength = try {
|
||||
// cipher!!.doFinal(reader.buffer, 0, length, temp, 0)
|
||||
// } catch (e: Exception) {
|
||||
// throw IOException("Unable to AES decrypt the data", e)
|
||||
// }
|
||||
//
|
||||
// ///////// decompress data -- as it's ALWAYS compressed
|
||||
//
|
||||
// // get the decompressed length (at the beginning of the array)
|
||||
// val uncompressedLength = OptimizeUtilsByteArray.readInt(temp, true)
|
||||
//
|
||||
// // where does our data start, AFTER the length field
|
||||
// val start = OptimizeUtilsByteArray.intLength(uncompressedLength, true) // because 1-4 bytes for the uncompressed size;
|
||||
//
|
||||
// // LZ4 decompress, requires the size of the ORIGINAL length (because we use the FAST decompressor
|
||||
// reader.reset()
|
||||
// decompressor.decompress(temp, start, reader.buffer, 0, uncompressedLength)
|
||||
// reader.setLimit(uncompressedLength)
|
||||
// if (DEBUG) {
|
||||
// val endWithoutUncompressedLength = decryptedLength - start
|
||||
// val compressed = ByteBufUtil.hexDump(temp, start, endWithoutUncompressedLength)
|
||||
// val orig = ByteBufUtil.hexDump(reader.buffer, 0, uncompressedLength)
|
||||
// logger.error("COMPRESSED: (" + endWithoutUncompressedLength + ")" + OS.LINE_SEPARATOR + compressed +
|
||||
// OS.LINE_SEPARATOR +
|
||||
// "ORIG: (" + uncompressedLength + ")" + OS.LINE_SEPARATOR + orig)
|
||||
// }
|
||||
//
|
||||
// // read the object from the buffer.
|
||||
// return read(connection, reader)
|
||||
// }
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection;
|
||||
|
||||
public
|
||||
interface Listener {
|
||||
/**
|
||||
* Called when the remote end has been connected. This will be invoked before any objects are received by the network.
|
||||
* This method should not block for long periods as other network activity will not be processed
|
||||
* until it returns.
|
||||
*/
|
||||
interface OnConnected<C extends Connection> extends Listener {
|
||||
/**
|
||||
* Called when the remote end has been connected. This will be invoked before any objects are received by the network.
|
||||
* This method should not block for long periods as other network activity will not be processed
|
||||
* until it returns.
|
||||
*/
|
||||
void connected(C connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the remote end is no longer connected. There is no guarantee as to what thread will invoke this method.
|
||||
* <p/>
|
||||
* Do not write data in this method! The channel can already be closed, resulting in an error if you attempt to do so.
|
||||
*/
|
||||
interface OnDisconnected<C extends Connection> extends Listener {
|
||||
/**
|
||||
* Called when the remote end is no longer connected. There is no guarantee as to what thread will invoke this method.
|
||||
* <p/>
|
||||
* Do not write data in this method! The channel can already be closed, resulting in an error if you attempt to do so.
|
||||
*/
|
||||
void disconnected(C connection);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Called when there is an error of some kind during the up/down stream process (to/from the socket or otherwise)
|
||||
*/
|
||||
interface OnError<C extends Connection> extends Listener {
|
||||
/**
|
||||
* Called when there is an error of some kind during the up/down stream process (to/from the socket or otherwise).
|
||||
*
|
||||
* The error is sent to an error log before this method is called.
|
||||
*/
|
||||
void error(C connection, Throwable throwable);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Called when an object has been received from the remote end of the connection.
|
||||
* This method should not block for long periods as other network activity will not be processed until it returns.
|
||||
*/
|
||||
interface OnMessageReceived<C extends Connection, M extends Object> extends Listener {
|
||||
void received(C connection, M message);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Permits a listener to specify it's own referenced object type, if passing in a generic parameter doesn't work. This is necessary since
|
||||
* the system looks up incoming message types to determine what listeners to dispatch them to.
|
||||
*/
|
||||
interface SelfDefinedType extends Listener {
|
||||
/**
|
||||
* Permits a listener to specify it's own referenced object type, if passing in a generic parameter doesn't work. This is necessary since
|
||||
* the system looks up incoming message types to determine what listeners to dispatch them to.
|
||||
*/
|
||||
Class<?> getType();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection
|
||||
|
||||
interface Listener {}
|
||||
|
||||
/**
|
||||
* Called before the remote end has been connected.
|
||||
* <p>
|
||||
* This permits the addition of connection filters to decide if a connection is permitted.
|
||||
*/
|
||||
interface FilterConnection<C : Connection> {
|
||||
/**
|
||||
* Called before the remote end has been connected.
|
||||
* <p>
|
||||
* This permits the addition of connection filters to decide if a connection is permitted.
|
||||
* <p>
|
||||
* @return true if the connection is permitted, false if it will be rejected
|
||||
*/
|
||||
fun filter(connection: C): Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the remote end has been connected. This will be invoked before any objects are received by the network.
|
||||
*/
|
||||
interface OnConnected<C : Connection> {
|
||||
/**
|
||||
* Called when the remote end has been connected. This will be invoked before any objects are received by the network.
|
||||
*/
|
||||
fun connected(connection: C)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the remote end is no longer connected.
|
||||
* <p>
|
||||
* Do not try to send messages! The connection will already be closed, resulting in an error if you attempt to do so.
|
||||
*/
|
||||
interface OnDisconnected<C : Connection> {
|
||||
/**
|
||||
* Called when the remote end is no longer connected.
|
||||
* <p>
|
||||
* Do not write data in this method! The connection can already be closed, resulting in an error if you attempt to do so.
|
||||
*/
|
||||
fun disconnected(connection: C)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when there is an error
|
||||
* <p>
|
||||
* The error is also sent to an error log before this method is called.
|
||||
*/
|
||||
interface OnError<C : Connection> {
|
||||
/**
|
||||
* Called when there is an error
|
||||
* <p>
|
||||
* The error is sent to an error log before this method is called.
|
||||
*/
|
||||
fun error(connection: C, throwable: Throwable)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an object has been received from the remote end of the connection.
|
||||
* <p>
|
||||
* This method should not block for long periods as other network activity will not be processed until it returns.
|
||||
*/
|
||||
interface OnMessageReceived<C : Connection, M : Any> {
|
||||
fun received(connection: C, message: M)
|
||||
}
|
||||
|
||||
/**
|
||||
* Permits a listener to specify it's own referenced object type, if passing in a generic parameter doesn't work. This is necessary since
|
||||
* the system looks up incoming message types to determine what listeners to dispatch them to.
|
||||
*/
|
||||
interface SelfDefinedType {
|
||||
/**
|
||||
* Permits a listener to specify it's own referenced object type, if passing in a generic parameter doesn't work. This is necessary since
|
||||
* the system looks up incoming message types to determine what listeners to dispatch them to.
|
||||
*/
|
||||
fun type(): Class<*>
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection;
|
||||
|
||||
/**
|
||||
* Generic types are in place to make sure that users of the application do not
|
||||
* accidentally add an incompatible connection type. This is at runtime.
|
||||
* <p/>
|
||||
* There should always be just a SINGLE connection type for the client or server
|
||||
*/
|
||||
public
|
||||
interface Listeners {
|
||||
/**
|
||||
* Adds a listener to this connection/endpoint to be notified of
|
||||
* connect/disconnect/idle/receive(object) events.
|
||||
* <p/>
|
||||
* If the listener already exists, it is not added again.
|
||||
* <p/>
|
||||
* When called by a server, NORMALLY listeners are added at the GLOBAL level
|
||||
* (meaning, I add one listener, and ALL connections are notified of that
|
||||
* listener.
|
||||
* <p/>
|
||||
* It is POSSIBLE to add a server connection ONLY (ie, not global) listener
|
||||
* (via connection.addListener), meaning that ONLY that listener attached to
|
||||
* the connection is notified on that event (ie, admin type listeners)
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
Listeners add(Listener listener);
|
||||
|
||||
/**
|
||||
* Removes a listener from this connection/endpoint to NO LONGER be notified
|
||||
* of connect/disconnect/idle/receive(object) events.
|
||||
* <p/>
|
||||
* When called by a server, NORMALLY listeners are added at the GLOBAL level
|
||||
* (meaning, I add one listener, and ALL connections are notified of that
|
||||
* listener.
|
||||
* <p/>
|
||||
* It is POSSIBLE to remove a server-connection 'non-global' listener (via
|
||||
* connection.removeListener), meaning that ONLY that listener attached to
|
||||
* the connection is removed
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
Listeners remove(Listener listener);
|
||||
|
||||
/**
|
||||
* Removes all registered listeners from this connection/endpoint to NO
|
||||
* LONGER be notified of connect/disconnect/idle/receive(object) events.
|
||||
*/
|
||||
Listeners removeAll();
|
||||
|
||||
/**
|
||||
* Removes all registered listeners (of the object type) from this
|
||||
* connection/endpoint to NO LONGER be notified of
|
||||
* connect/disconnect/idle/receive(object) events.
|
||||
*/
|
||||
Listeners removeAll(Class<?> classType);
|
||||
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection
|
||||
|
||||
/**
|
||||
* Generic types are in place to make sure that users of the application do not
|
||||
* accidentally add an incompatible connection type.
|
||||
*/
|
||||
interface Listeners<C> where C : Connection {
|
||||
|
||||
/**
|
||||
* Adds a function that will be called BEFORE a client/server "connects" with
|
||||
* each other, and used to determine if a connection should be allowed
|
||||
*
|
||||
* If the function returns TRUE, then the connection will continue to connect.
|
||||
* If the function returns FALSE, then the other end of the connection will
|
||||
* receive a connection error
|
||||
*
|
||||
* For a server, this function will be called for ALL clients.
|
||||
*/
|
||||
fun filter(function: (C) -> Boolean): Listeners<C>
|
||||
|
||||
/**
|
||||
* Adds a function that will be called when a client/server "connects" with
|
||||
* each other
|
||||
*
|
||||
* For a server, this function will be called for ALL clients.
|
||||
*/
|
||||
fun onConnect(function: (C) -> Unit): Listeners<C>
|
||||
|
||||
/**
|
||||
* Adds a function that will be called when a client/server "disconnects" with
|
||||
* each other
|
||||
*
|
||||
* For a server, this function will be called for ALL clients.
|
||||
*
|
||||
* It is POSSIBLE to add a server CONNECTION only (ie, not global) listener
|
||||
* (via connection.addListener), meaning that ONLY that listener attached to
|
||||
* the connection is notified on that event (ie, admin type listeners)
|
||||
*/
|
||||
fun onDisconnect(function: (C) -> Unit): Listeners<C>
|
||||
|
||||
|
||||
/**
|
||||
* Adds a function that will be called when a client/server encounters an error
|
||||
*
|
||||
* For a server, this function will be called for ALL clients.
|
||||
*
|
||||
* It is POSSIBLE to add a server CONNECTION only (ie, not global) listener
|
||||
* (via connection.addListener), meaning that ONLY that listener attached to
|
||||
* the connection is notified on that event (ie, admin type listeners)
|
||||
*/
|
||||
fun onError(function: (C, throwable: Throwable) -> Unit): Listeners<C>
|
||||
|
||||
|
||||
/**
|
||||
* Adds a function that will be called when a client/server receives a message
|
||||
*
|
||||
* For a server, this function will be called for ALL clients.
|
||||
*
|
||||
* It is POSSIBLE to add a server CONNECTION only (ie, not global) listener
|
||||
* (via connection.addListener), meaning that ONLY that listener attached to
|
||||
* the connection is notified on that event (ie, admin type listeners)
|
||||
*/
|
||||
fun <M : Any> onMessage(function: (C, M) -> Unit): Listeners<C>
|
||||
|
||||
|
||||
/**
|
||||
* Removes a listener from this connection/endpoint to NO LONGER be notified
|
||||
* of connect/disconnect/idle/receive(object) events.
|
||||
*
|
||||
*
|
||||
* When called by a server, NORMALLY listeners are added at the GLOBAL level
|
||||
* (meaning, I add one listener, and ALL connections are notified of that
|
||||
* listener.
|
||||
*
|
||||
*
|
||||
* It is POSSIBLE to remove a server-connection 'non-global' listener (via
|
||||
* connection.removeListener), meaning that ONLY that listener attached to
|
||||
* the connection is removed
|
||||
*/
|
||||
fun remove(listener: OnConnected<C>): Listeners<C>
|
||||
|
||||
/**
|
||||
* Removes all registered listeners from this connection/endpoint to NO
|
||||
* LONGER be notified of connect/disconnect/idle/receive(object) events.
|
||||
*/
|
||||
fun removeAll(): Listeners<C>
|
||||
|
||||
/**
|
||||
* Removes all registered listeners (of the object type) from this
|
||||
* connection/endpoint to NO LONGER be notified of
|
||||
* connect/disconnect/idle/receive(object) events.
|
||||
*/
|
||||
fun removeAll(classType: Class<*>): Listeners<C>
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package dorkbox.network.connection
|
||||
|
||||
enum class MediaDriverType(private val type: String) {
|
||||
IPC("ipc"), UDP("udp");
|
||||
|
||||
override fun toString(): String {
|
||||
return type
|
||||
}
|
||||
}
|
|
@ -1,212 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
|
||||
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
|
||||
|
||||
import dorkbox.network.store.DB_Server;
|
||||
import dorkbox.network.store.SettingsStore;
|
||||
import dorkbox.util.bytes.ByteArrayWrapper;
|
||||
import dorkbox.util.exceptions.SecurityException;
|
||||
import dorkbox.util.serialization.SerializationManager;
|
||||
import dorkbox.util.storage.Storage;
|
||||
import dorkbox.util.storage.StorageSystem;
|
||||
|
||||
/**
|
||||
* The property store is the DEFAULT type of store for the network stack.
|
||||
* This is package private, and not intended to be extended.
|
||||
*/
|
||||
final
|
||||
class PropertyStore extends SettingsStore {
|
||||
private Storage storage;
|
||||
private Map<ByteArrayWrapper, DB_Server> servers;
|
||||
|
||||
PropertyStore() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Method of preference for creating/getting this connection store.
|
||||
*
|
||||
* @param serializationManager this is the serialization used for saving objects into the storage database
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
void init(final SerializationManager serializationManager, final Storage ignored) {
|
||||
// make sure our custom types are registered
|
||||
// only register if not ALREADY initialized, since we can initialize in the server and in the client. This creates problems if
|
||||
// running inside the same JVM (we don't permit it)
|
||||
if (serializationManager != null && !serializationManager.initialized()) {
|
||||
serializationManager.register(HashMap.class);
|
||||
serializationManager.register(ByteArrayWrapper.class);
|
||||
serializationManager.register(DB_Server.class);
|
||||
}
|
||||
|
||||
this.storage = StorageSystem.Memory()
|
||||
.build();
|
||||
|
||||
servers = this.storage.get(DB_Server.STORAGE_KEY, new HashMap<ByteArrayWrapper, DB_Server>(16));
|
||||
|
||||
DB_Server localServer = servers.get(DB_Server.IP_SELF); // this will always be null and is here to help people that copy/paste code
|
||||
if (localServer == null) {
|
||||
localServer = new DB_Server();
|
||||
servers.put(DB_Server.IP_SELF, localServer);
|
||||
|
||||
// have to always specify what we are saving
|
||||
this.storage.put(DB_Server.STORAGE_KEY, servers);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple, property based method to getting the private key of the server
|
||||
*/
|
||||
@Override
|
||||
public synchronized
|
||||
ECPrivateKeyParameters getPrivateKey() throws dorkbox.util.exceptions.SecurityException {
|
||||
checkAccess(EndPoint.class);
|
||||
|
||||
return servers.get(DB_Server.IP_SELF)
|
||||
.getPrivateKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple, property based method for saving the private key of the server
|
||||
*/
|
||||
@Override
|
||||
public synchronized
|
||||
void savePrivateKey(final ECPrivateKeyParameters serverPrivateKey) throws SecurityException {
|
||||
checkAccess(EndPoint.class);
|
||||
|
||||
servers.get(DB_Server.IP_SELF)
|
||||
.setPrivateKey(serverPrivateKey);
|
||||
|
||||
// have to always specify what we are saving
|
||||
storage.put(DB_Server.STORAGE_KEY, servers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple, property based method to getting the public key of the server
|
||||
*/
|
||||
@Override
|
||||
public synchronized
|
||||
ECPublicKeyParameters getPublicKey() throws SecurityException {
|
||||
checkAccess(EndPoint.class);
|
||||
|
||||
return servers.get(DB_Server.IP_SELF)
|
||||
.getPublicKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple, property based method for saving the public key of the server
|
||||
*/
|
||||
@Override
|
||||
public synchronized
|
||||
void savePublicKey(final ECPublicKeyParameters serverPublicKey) throws SecurityException {
|
||||
checkAccess(EndPoint.class);
|
||||
|
||||
servers.get(DB_Server.IP_SELF)
|
||||
.setPublicKey(serverPublicKey);
|
||||
|
||||
// have to always specify what we are saving
|
||||
storage.put(DB_Server.STORAGE_KEY, servers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple, property based method to getting the server salt
|
||||
*/
|
||||
@Override
|
||||
public synchronized
|
||||
byte[] getSalt() {
|
||||
final DB_Server localServer = servers.get(DB_Server.IP_SELF);
|
||||
byte[] salt = localServer.getSalt();
|
||||
|
||||
// we don't care who gets the server salt
|
||||
if (salt == null) {
|
||||
SecureRandom secureRandom = new SecureRandom();
|
||||
|
||||
// server salt is used to salt usernames and other various connection handshake parameters
|
||||
byte[] bytes = new byte[256];
|
||||
secureRandom.nextBytes(bytes);
|
||||
|
||||
salt = bytes;
|
||||
|
||||
localServer.setSalt(bytes);
|
||||
|
||||
// have to always specify what we are saving
|
||||
storage.put(DB_Server.STORAGE_KEY, servers);
|
||||
}
|
||||
|
||||
return salt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple, property based method to getting a connected computer by host IP address
|
||||
*/
|
||||
@Override
|
||||
public synchronized
|
||||
ECPublicKeyParameters getRegisteredServerKey(final byte[] hostAddress) throws SecurityException {
|
||||
checkAccess(RegistrationWrapper.class);
|
||||
|
||||
final DB_Server db_server = this.servers.get(ByteArrayWrapper.wrap(hostAddress));
|
||||
if (db_server != null) {
|
||||
return db_server.getPublicKey();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a connected computer by host IP address and public key
|
||||
*/
|
||||
@Override
|
||||
public synchronized
|
||||
void addRegisteredServerKey(final byte[] hostAddress, ECPublicKeyParameters publicKey)
|
||||
throws SecurityException {
|
||||
checkAccess(RegistrationWrapper.class);
|
||||
|
||||
final ByteArrayWrapper wrap = ByteArrayWrapper.wrap(hostAddress);
|
||||
DB_Server db_server = this.servers.get(wrap);
|
||||
if (db_server == null) {
|
||||
db_server = new DB_Server();
|
||||
}
|
||||
|
||||
db_server.setPublicKey(publicKey);
|
||||
servers.put(wrap, db_server);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a registered computer by host IP address
|
||||
*/
|
||||
@Override
|
||||
public synchronized
|
||||
boolean removeRegisteredServerKey(final byte[] hostAddress) throws SecurityException {
|
||||
checkAccess(RegistrationWrapper.class);
|
||||
|
||||
final ByteArrayWrapper wrap = ByteArrayWrapper.wrap(hostAddress);
|
||||
DB_Server db_server = this.servers.remove(wrap);
|
||||
|
||||
return db_server != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void close() {
|
||||
StorageSystem.close(storage);
|
||||
}
|
||||
}
|
|
@ -1,260 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import dorkbox.network.connection.registration.MetaChannel;
|
||||
import dorkbox.network.pipeline.tcp.KryoEncoderTcp;
|
||||
import dorkbox.network.pipeline.tcp.KryoEncoderTcpCompression;
|
||||
import dorkbox.network.pipeline.tcp.KryoEncoderTcpCrypto;
|
||||
import dorkbox.network.pipeline.tcp.KryoEncoderTcpNone;
|
||||
import dorkbox.network.pipeline.udp.KryoDecoderUdp;
|
||||
import dorkbox.network.pipeline.udp.KryoDecoderUdpCompression;
|
||||
import dorkbox.network.pipeline.udp.KryoDecoderUdpCrypto;
|
||||
import dorkbox.network.pipeline.udp.KryoDecoderUdpNone;
|
||||
import dorkbox.network.pipeline.udp.KryoEncoderUdp;
|
||||
import dorkbox.network.pipeline.udp.KryoEncoderUdpCompression;
|
||||
import dorkbox.network.pipeline.udp.KryoEncoderUdpCrypto;
|
||||
import dorkbox.network.pipeline.udp.KryoEncoderUdpNone;
|
||||
import dorkbox.network.serialization.NetworkSerializationManager;
|
||||
import dorkbox.util.collections.IntMap.Values;
|
||||
import dorkbox.util.collections.LockFreeIntMap;
|
||||
import dorkbox.util.crypto.CryptoECC;
|
||||
import dorkbox.util.exceptions.SecurityException;
|
||||
import io.netty.channel.Channel;
|
||||
|
||||
/**
|
||||
* Just wraps common/needed methods of the client/server endpoint by the registration stage/handshake.
|
||||
* <p/>
|
||||
* This is in the connection package, so it can access the endpoint methods that it needs to without having to publicly expose them
|
||||
*/
|
||||
public abstract
|
||||
class RegistrationWrapper {
|
||||
|
||||
public
|
||||
enum STATE { ERROR, WAIT, CONTINUE }
|
||||
|
||||
final org.slf4j.Logger logger;
|
||||
final EndPoint endPoint;
|
||||
|
||||
// keeps track of connections/sessions (TCP/UDP/Local). The session ID '0' is reserved to mean "no session ID yet"
|
||||
final LockFreeIntMap<MetaChannel> sessionMap = new LockFreeIntMap<MetaChannel>(32, ConnectionManager.LOAD_FACTOR);
|
||||
|
||||
|
||||
public final KryoEncoderTcp kryoTcpEncoder;
|
||||
public final KryoEncoderTcpNone kryoTcpEncoderNone;
|
||||
public final KryoEncoderTcpCompression kryoTcpEncoderCompression;
|
||||
public final KryoEncoderTcpCrypto kryoTcpEncoderCrypto;
|
||||
|
||||
public final KryoEncoderUdp kryoUdpEncoder;
|
||||
public final KryoEncoderUdpNone kryoUdpEncoderNone;
|
||||
public final KryoEncoderUdpCompression kryoUdpEncoderCompression;
|
||||
public final KryoEncoderUdpCrypto kryoUdpEncoderCrypto;
|
||||
|
||||
public final KryoDecoderUdp kryoUdpDecoder;
|
||||
public final KryoDecoderUdpNone kryoUdpDecoderNone;
|
||||
public final KryoDecoderUdpCompression kryoUdpDecoderCompression;
|
||||
public final KryoDecoderUdpCrypto kryoUdpDecoderCrypto;
|
||||
|
||||
|
||||
public
|
||||
RegistrationWrapper(final EndPoint endPoint,
|
||||
final Logger logger) {
|
||||
this.endPoint = endPoint;
|
||||
this.logger = logger;
|
||||
|
||||
this.kryoTcpEncoder = new KryoEncoderTcp(endPoint.serializationManager);
|
||||
this.kryoTcpEncoderNone = new KryoEncoderTcpNone(endPoint.serializationManager);
|
||||
this.kryoTcpEncoderCompression = new KryoEncoderTcpCompression(endPoint.serializationManager);
|
||||
this.kryoTcpEncoderCrypto = new KryoEncoderTcpCrypto(endPoint.serializationManager);
|
||||
|
||||
|
||||
this.kryoUdpEncoder = new KryoEncoderUdp(endPoint.serializationManager);
|
||||
this.kryoUdpEncoderNone = new KryoEncoderUdpNone(endPoint.serializationManager);
|
||||
this.kryoUdpEncoderCompression = new KryoEncoderUdpCompression(endPoint.serializationManager);
|
||||
this.kryoUdpEncoderCrypto = new KryoEncoderUdpCrypto(endPoint.serializationManager);
|
||||
|
||||
|
||||
this.kryoUdpDecoder = new KryoDecoderUdp(endPoint.serializationManager);
|
||||
this.kryoUdpDecoderNone = new KryoDecoderUdpNone(endPoint.serializationManager);
|
||||
this.kryoUdpDecoderCompression = new KryoDecoderUdpCompression(endPoint.serializationManager);
|
||||
this.kryoUdpDecoderCrypto = new KryoDecoderUdpCrypto(endPoint.serializationManager);
|
||||
}
|
||||
|
||||
public
|
||||
NetworkSerializationManager getSerialization() {
|
||||
return endPoint.getSerialization();
|
||||
}
|
||||
|
||||
/**
|
||||
* The amount of milli-seconds that must elapse with no read or write before Listener.OnIdle() will be triggered
|
||||
*/
|
||||
public
|
||||
int getIdleTimeout() {
|
||||
return 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal call by the pipeline to notify the "Connection" object that it has "connected".
|
||||
*/
|
||||
public
|
||||
void connectionConnected0(ConnectionImpl connection) {
|
||||
this.endPoint.connectionConnected0(connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal call by the pipeline when: - creating a new network connection - when determining the baseClass for generics
|
||||
*
|
||||
* @param metaChannel can be NULL (when getting the baseClass)
|
||||
*/
|
||||
public
|
||||
ConnectionImpl connection0(MetaChannel metaChannel, final InetSocketAddress remoteAddress) {
|
||||
return this.endPoint.connection0(metaChannel, remoteAddress);
|
||||
}
|
||||
|
||||
public
|
||||
SecureRandom getSecureRandom() {
|
||||
return this.endPoint.secureRandom;
|
||||
}
|
||||
|
||||
public
|
||||
ECPublicKeyParameters getPublicKey() {
|
||||
return this.endPoint.publicKey;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* If the key does not match AND we have disabled remote key validation, then metachannel.changedRemoteKey = true. OTHERWISE, key validation is REQUIRED!
|
||||
*
|
||||
* @return true if the remote address public key matches the one saved or we disabled remote key validation.
|
||||
*/
|
||||
public
|
||||
boolean validateRemoteAddress(final MetaChannel metaChannel, final InetSocketAddress remoteAddress, final ECPublicKeyParameters publicKey) {
|
||||
InetAddress address = remoteAddress.getAddress();
|
||||
byte[] hostAddress = address.getAddress();
|
||||
|
||||
try {
|
||||
ECPublicKeyParameters savedPublicKey = this.endPoint.propertyStore.getRegisteredServerKey(hostAddress);
|
||||
Logger logger2 = this.logger;
|
||||
if (savedPublicKey == null) {
|
||||
if (logger2.isDebugEnabled()) {
|
||||
logger2.debug("Adding new remote IP address key for {}", address.getHostAddress());
|
||||
}
|
||||
this.endPoint.propertyStore.addRegisteredServerKey(hostAddress, publicKey);
|
||||
}
|
||||
else {
|
||||
// COMPARE!
|
||||
if (!CryptoECC.compare(publicKey, savedPublicKey)) {
|
||||
if (this.endPoint.disableRemoteKeyValidation) {
|
||||
logger2.warn("Invalid or non-matching public key from remote connection, their public key has changed. Toggling extra flag in channel to indicate key change. To fix, remove entry for: {}", address.getHostAddress());
|
||||
|
||||
metaChannel.changedRemoteKey = true;
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
// keys do not match, abort!
|
||||
logger2.error("Invalid or non-matching public key from remote connection, their public key has changed. To fix, remove entry for: {}", address.getHostAddress());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The session ID '0' is reserved to mean "no session ID yet"
|
||||
*/
|
||||
public
|
||||
MetaChannel getSession(final int sessionId) {
|
||||
return sessionMap.get(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* The SERVER AND CLIENT will stop tracking a session once the session is complete.
|
||||
*/
|
||||
public
|
||||
void removeSession(final MetaChannel metaChannel) {
|
||||
int sessionId = metaChannel.sessionId;
|
||||
if (sessionId != 0) {
|
||||
sessionMap.remove(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The CLIENT/SERVER will stop tracking a session if there are errors
|
||||
*/
|
||||
public
|
||||
void closeSession(final int sessionId) {
|
||||
if (sessionId != 0) {
|
||||
MetaChannel metaChannel = sessionMap.remove(sessionId);
|
||||
if (metaChannel != null) {
|
||||
if (metaChannel.tcpChannel != null && metaChannel.tcpChannel.isOpen()) {
|
||||
metaChannel.tcpChannel.close();
|
||||
}
|
||||
if (metaChannel.udpChannel != null && metaChannel.udpChannel.isOpen()) {
|
||||
metaChannel.udpChannel.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all session associations (keeps the server/client running).
|
||||
*/
|
||||
public
|
||||
void clearSessions() {
|
||||
List<Channel> channels = new LinkedList<Channel>();
|
||||
|
||||
synchronized (sessionMap) {
|
||||
Values<MetaChannel> values = sessionMap.values();
|
||||
for (MetaChannel metaChannel : values) {
|
||||
if (metaChannel.tcpChannel != null && metaChannel.tcpChannel.isOpen()) {
|
||||
channels.add(metaChannel.tcpChannel);
|
||||
}
|
||||
if (metaChannel.udpChannel != null && metaChannel.udpChannel.isOpen()) {
|
||||
channels.add(metaChannel.udpChannel);
|
||||
}
|
||||
}
|
||||
|
||||
// remote all session associations. Any session in progress will have to restart it's registration process
|
||||
sessionMap.clear();
|
||||
}
|
||||
|
||||
// close all "in progress" registrations as well
|
||||
for (Channel channel : channels) {
|
||||
channel.close();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,177 +0,0 @@
|
|||
package dorkbox.network.connection;
|
||||
|
||||
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import dorkbox.network.connection.registration.MetaChannel;
|
||||
import dorkbox.network.connection.registration.Registration;
|
||||
import dorkbox.network.connection.registration.UpgradeType;
|
||||
import dorkbox.network.serialization.Serialization;
|
||||
import dorkbox.util.collections.IntMap.Values;
|
||||
import dorkbox.util.exceptions.SecurityException;
|
||||
import io.netty.channel.Channel;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public
|
||||
class RegistrationWrapperClient extends RegistrationWrapper {
|
||||
public
|
||||
RegistrationWrapperClient(final EndPoint endPoint, final Logger logger) {
|
||||
super(endPoint, logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* MetaChannel allow access to the same "session" across TCP/UDP/etc
|
||||
* <p>
|
||||
* The connection ID '0' is reserved to mean "no channel ID yet"
|
||||
*/
|
||||
public
|
||||
MetaChannel createSession(int sessionId) {
|
||||
MetaChannel metaChannel = new MetaChannel(sessionId);
|
||||
sessionMap.put(sessionId, metaChannel);
|
||||
|
||||
return metaChannel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the first session we have available. This is for the CLIENT to track sessions (between TCP/UDP) to a server
|
||||
*/
|
||||
public MetaChannel getFirstSession() {
|
||||
Values<MetaChannel> values = sessionMap.values();
|
||||
if (values.hasNext) {
|
||||
return values.next();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public
|
||||
boolean isClient() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal call by the pipeline to check if the client has more protocol registrations to complete.
|
||||
*
|
||||
* @return true if there are more registrations to process, false if we are 100% done with all types to register (TCP/UDP/etc)
|
||||
*/
|
||||
public
|
||||
boolean hasMoreRegistrations() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal call by the pipeline to notify the client to continue registering the different session protocols. The server does not use
|
||||
* this.
|
||||
*/
|
||||
public
|
||||
void startNextProtocolRegistration() {
|
||||
}
|
||||
|
||||
public
|
||||
void removeRegisteredServerKey(final byte[] hostAddress) throws SecurityException {
|
||||
ECPublicKeyParameters savedPublicKey = this.endPoint.propertyStore.getRegisteredServerKey(hostAddress);
|
||||
if (savedPublicKey != null) {
|
||||
Logger logger2 = this.logger;
|
||||
if (logger2.isDebugEnabled()) {
|
||||
logger2.debug("Deleting remote IP address key {}.{}.{}.{}",
|
||||
hostAddress[0],
|
||||
hostAddress[1],
|
||||
hostAddress[2],
|
||||
hostAddress[3]);
|
||||
}
|
||||
|
||||
this.endPoint.propertyStore.removeRegisteredServerKey(hostAddress);
|
||||
}
|
||||
}
|
||||
|
||||
public
|
||||
boolean initClassRegistration(final Channel channel, final Registration registration) {
|
||||
byte[] details = this.endPoint.getSerialization().getKryoRegistrationDetails();
|
||||
|
||||
int length = details.length;
|
||||
if (length > Serialization.CLASS_REGISTRATION_VALIDATION_FRAGMENT_SIZE) {
|
||||
// it is too large to send in a single packet
|
||||
|
||||
// child arrays have index 0 also as their 'index' and 1 is the total number of fragments
|
||||
byte[][] fragments = divideArray(details, Serialization.CLASS_REGISTRATION_VALIDATION_FRAGMENT_SIZE);
|
||||
if (fragments == null) {
|
||||
logger.error("Too many classes have been registered for Serialization. Please report this issue");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int allButLast = fragments.length - 1;
|
||||
|
||||
for (int i = 0; i < allButLast; i++) {
|
||||
final byte[] fragment = fragments[i];
|
||||
Registration fragmentedRegistration = new Registration(registration.sessionID);
|
||||
fragmentedRegistration.payload = fragment;
|
||||
|
||||
// tell the server we are fragmented
|
||||
fragmentedRegistration.upgradeType = UpgradeType.FRAGMENTED;
|
||||
|
||||
// tell the server we are upgraded (it will bounce back telling us to connect)
|
||||
fragmentedRegistration.upgraded = true;
|
||||
channel.writeAndFlush(fragmentedRegistration);
|
||||
}
|
||||
|
||||
// now tell the server we are done with the fragments
|
||||
Registration fragmentedRegistration = new Registration(registration.sessionID);
|
||||
fragmentedRegistration.payload = fragments[allButLast];
|
||||
|
||||
// tell the server we are fragmented
|
||||
fragmentedRegistration.upgradeType = UpgradeType.FRAGMENTED;
|
||||
|
||||
// tell the server we are upgraded (it will bounce back telling us to connect)
|
||||
fragmentedRegistration.upgraded = true;
|
||||
channel.writeAndFlush(fragmentedRegistration);
|
||||
} else {
|
||||
registration.payload = details;
|
||||
|
||||
// tell the server we are upgraded (it will bounce back telling us to connect)
|
||||
registration.upgraded = true;
|
||||
channel.writeAndFlush(registration);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split array into chunks, max of 256 chunks.
|
||||
* byte[0] = chunk ID
|
||||
* byte[1] = total chunks (0-255) (where 0->1, 2->3, 127->127 because this is indexed by a byte)
|
||||
*/
|
||||
private static
|
||||
byte[][] divideArray(byte[] source, int chunksize) {
|
||||
|
||||
int fragments = (int) Math.ceil(source.length / ((double) chunksize));
|
||||
if (fragments > 127) {
|
||||
// cannot allow more than 127
|
||||
return null;
|
||||
}
|
||||
|
||||
// pre-allocate the memory
|
||||
byte[][] splitArray = new byte[fragments][chunksize + 2];
|
||||
int start = 0;
|
||||
|
||||
for (int i = 0; i < splitArray.length; i++) {
|
||||
int length;
|
||||
|
||||
if (start + chunksize > source.length) {
|
||||
length = source.length - start;
|
||||
}
|
||||
else {
|
||||
length = chunksize;
|
||||
}
|
||||
splitArray[i] = new byte[length + 2];
|
||||
splitArray[i][0] = (byte) i; // index
|
||||
splitArray[i][1] = (byte) fragments; // total number of fragments
|
||||
System.arraycopy(source, start, splitArray[i], 2, length);
|
||||
|
||||
start += chunksize;
|
||||
}
|
||||
|
||||
return splitArray;
|
||||
}
|
||||
}
|
|
@ -1,114 +0,0 @@
|
|||
package dorkbox.network.connection;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import dorkbox.network.connection.registration.MetaChannel;
|
||||
import dorkbox.network.connection.registration.Registration;
|
||||
import dorkbox.network.connection.registration.UpgradeType;
|
||||
import dorkbox.network.serialization.Serialization;
|
||||
import dorkbox.util.RandomUtil;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public
|
||||
class RegistrationWrapperServer extends RegistrationWrapper {
|
||||
public
|
||||
RegistrationWrapperServer(final EndPoint endPoint, final Logger logger) {
|
||||
super(endPoint, logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* MetaChannel allow access to the same "session" across TCP/UDP/etc.
|
||||
* <p>
|
||||
* The connection ID '0' is reserved to mean "no channel ID yet"
|
||||
*/
|
||||
public
|
||||
MetaChannel createSession() {
|
||||
int sessionId = RandomUtil.int_();
|
||||
while (sessionId == 0 && sessionMap.containsKey(sessionId)) {
|
||||
sessionId = RandomUtil.int_();
|
||||
}
|
||||
|
||||
MetaChannel metaChannel;
|
||||
synchronized (sessionMap) {
|
||||
// one final check, but slower...
|
||||
while (sessionId == 0 && sessionMap.containsKey(sessionId)) {
|
||||
sessionId = RandomUtil.int_();
|
||||
}
|
||||
|
||||
metaChannel = new MetaChannel(sessionId);
|
||||
sessionMap.put(sessionId, metaChannel);
|
||||
|
||||
|
||||
// TODO: clean out sessions that are stale!
|
||||
}
|
||||
|
||||
return metaChannel;
|
||||
}
|
||||
|
||||
public
|
||||
boolean acceptRemoteConnection(final InetSocketAddress remoteAddress) {
|
||||
return ((EndPointServer) this.endPoint).acceptRemoteConnection(remoteAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* Only called by the server!
|
||||
*
|
||||
* If we are loopback or the client is a specific IP/CIDR address, then we do things differently. The LOOPBACK address will never encrypt or compress the traffic.
|
||||
*/
|
||||
public
|
||||
byte getConnectionUpgradeType(final InetSocketAddress remoteAddress) {
|
||||
return ((EndPointServer) this.endPoint).getConnectionUpgradeType(remoteAddress);
|
||||
}
|
||||
|
||||
|
||||
public
|
||||
STATE verifyClassRegistration(final MetaChannel metaChannel, final Registration registration) {
|
||||
if (registration.upgradeType == UpgradeType.FRAGMENTED) {
|
||||
byte[] fragment = registration.payload;
|
||||
|
||||
// this means that the registrations are FRAGMENTED!
|
||||
// max size of ALL fragments is xxx * 127
|
||||
if (metaChannel.fragmentedRegistrationDetails == null) {
|
||||
metaChannel.remainingFragments = fragment[1];
|
||||
metaChannel.fragmentedRegistrationDetails = new byte[Serialization.CLASS_REGISTRATION_VALIDATION_FRAGMENT_SIZE * fragment[1]];
|
||||
}
|
||||
|
||||
System.arraycopy(fragment, 2, metaChannel.fragmentedRegistrationDetails, fragment[0] * Serialization.CLASS_REGISTRATION_VALIDATION_FRAGMENT_SIZE, fragment.length - 2);
|
||||
metaChannel.remainingFragments--;
|
||||
|
||||
|
||||
if (fragment[0] + 1 == fragment[1]) {
|
||||
// this is the last fragment in the in byte array (but NOT necessarily the last fragment to arrive)
|
||||
int correctSize = (Serialization.CLASS_REGISTRATION_VALIDATION_FRAGMENT_SIZE * (fragment[1] - 1)) + (fragment.length - 2);
|
||||
byte[] correctlySized = new byte[correctSize];
|
||||
System.arraycopy(metaChannel.fragmentedRegistrationDetails, 0, correctlySized, 0, correctSize);
|
||||
metaChannel.fragmentedRegistrationDetails = correctlySized;
|
||||
}
|
||||
|
||||
if (metaChannel.remainingFragments == 0) {
|
||||
// there are no more fragments available
|
||||
byte[] details = metaChannel.fragmentedRegistrationDetails;
|
||||
metaChannel.fragmentedRegistrationDetails = null;
|
||||
|
||||
if (!this.endPoint.getSerialization().verifyKryoRegistration(details)) {
|
||||
// error
|
||||
return STATE.ERROR;
|
||||
}
|
||||
} else {
|
||||
// wait for more fragments
|
||||
return STATE.WAIT;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!this.endPoint.getSerialization().verifyKryoRegistration(registration.payload)) {
|
||||
return STATE.ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
return STATE.CONTINUE;
|
||||
}
|
||||
}
|
|
@ -1,389 +0,0 @@
|
|||
/*
|
||||
* Copyright 2019 dorkbox, llc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import dorkbox.util.Property;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.util.concurrent.EventExecutor;
|
||||
import io.netty.util.concurrent.Future;
|
||||
|
||||
/**
|
||||
* This is the highest level endpoint, for lifecycle support/management.
|
||||
*/
|
||||
public
|
||||
class Shutdownable {
|
||||
|
||||
protected static final String shutdownHookName = "::SHUTDOWN_HOOK::";
|
||||
protected static final String stopTreadName = "::STOP_THREAD::";
|
||||
|
||||
|
||||
/**
|
||||
* The HIGH and LOW watermark points for connections
|
||||
*/
|
||||
@Property
|
||||
public static final int WRITE_BUFF_HIGH = 32 * 1024;
|
||||
@Property
|
||||
public static final int WRITE_BUFF_LOW = 8 * 1024;
|
||||
|
||||
/**
|
||||
* The amount of time in milli-seconds to wait for this endpoint to close all {@link Channel}s and shutdown gracefully.
|
||||
*/
|
||||
@Property
|
||||
public static long maxShutdownWaitTimeInMilliSeconds = 2000L; // in milliseconds
|
||||
|
||||
/**
|
||||
* Runs a runnable inside a NEW thread that is NOT in the same thread group as Netty
|
||||
*/
|
||||
public static
|
||||
void runNewThread(final String threadName, final Runnable runnable) {
|
||||
Thread thread = new Thread(Thread.currentThread()
|
||||
.getThreadGroup()
|
||||
.getParent(),
|
||||
runnable);
|
||||
thread.setDaemon(true);
|
||||
thread.setName(threadName);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
protected final org.slf4j.Logger logger;
|
||||
|
||||
protected final ThreadGroup threadGroup;
|
||||
|
||||
private final Class<? extends Shutdownable> type;
|
||||
|
||||
protected final Object shutdownInProgress = new Object();
|
||||
private volatile boolean isShutdown = false;
|
||||
|
||||
// the eventLoop groups are used to track and manage the event loops for startup/shutdown
|
||||
private final List<EventLoopGroup> eventLoopGroups = new ArrayList<EventLoopGroup>(8);
|
||||
private final List<ChannelFuture> shutdownChannelList = new ArrayList<ChannelFuture>();
|
||||
|
||||
// make sure that the endpoint is closed on JVM shutdown (if it's still open at that point in time)
|
||||
private Thread shutdownHook;
|
||||
|
||||
private final CountDownLatch blockUntilDone = new CountDownLatch(1);
|
||||
|
||||
private AtomicBoolean stopCalled = new AtomicBoolean(false);
|
||||
|
||||
public
|
||||
Shutdownable(final Class<? extends Shutdownable> type) {
|
||||
this.type = type;
|
||||
|
||||
// setup the thread group to easily ID what the following threads belong to (and their spawned threads...)
|
||||
SecurityManager s = System.getSecurityManager();
|
||||
threadGroup = new ThreadGroup(s != null
|
||||
? s.getThreadGroup()
|
||||
: Thread.currentThread()
|
||||
.getThreadGroup(), type.getSimpleName());
|
||||
threadGroup.setDaemon(true);
|
||||
|
||||
logger = org.slf4j.LoggerFactory.getLogger(type.getSimpleName());
|
||||
|
||||
|
||||
shutdownHook = new Thread() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
if (Shutdownable.this.shouldShutdownHookRun()) {
|
||||
Shutdownable.this.stop();
|
||||
}
|
||||
}
|
||||
};
|
||||
shutdownHook.setName(shutdownHookName);
|
||||
try {
|
||||
Runtime.getRuntime()
|
||||
.addShutdownHook(shutdownHook);
|
||||
} catch (Throwable ignored) {
|
||||
// if we are in the middle of shutdown, we cannot do this.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a channel future to be tracked and managed for shutdown.
|
||||
*/
|
||||
protected final
|
||||
void manageForShutdown(ChannelFuture future) {
|
||||
synchronized (shutdownChannelList) {
|
||||
shutdownChannelList.add(future);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an eventloop group to be tracked & managed for shutdown
|
||||
*/
|
||||
protected final
|
||||
void manageForShutdown(EventLoopGroup loopGroup) {
|
||||
synchronized (eventLoopGroups) {
|
||||
eventLoopGroups.add(loopGroup);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an eventloop group to be tracked & managed for shutdown
|
||||
*/
|
||||
protected final
|
||||
void removeFromShutdown(EventLoopGroup loopGroup) {
|
||||
synchronized (eventLoopGroups) {
|
||||
eventLoopGroups.remove(loopGroup);
|
||||
}
|
||||
}
|
||||
|
||||
// server only does this on stop. Client does this on closeConnections
|
||||
void shutdownAllChannels() {
|
||||
synchronized (shutdownChannelList) {
|
||||
// now we stop all of our channels. For the server, this will close the server manager for UDP sessions
|
||||
for (ChannelFuture f : shutdownChannelList) {
|
||||
Channel channel = f.channel();
|
||||
if (channel.isOpen()) {
|
||||
// from the git example on how to shutdown a channel
|
||||
channel.close().syncUninterruptibly();
|
||||
Thread.yield();
|
||||
}
|
||||
}
|
||||
|
||||
// we have to clear the shutdown list. (
|
||||
shutdownChannelList.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// shutdown all event loops associated
|
||||
void shutdownEventLoops() {
|
||||
// we want to WAIT until after the event executors have completed shutting down.
|
||||
List<Future<?>> shutdownThreadList = new LinkedList<Future<?>>();
|
||||
|
||||
List<EventLoopGroup> loopGroups;
|
||||
synchronized (eventLoopGroups) {
|
||||
loopGroups = new ArrayList<EventLoopGroup>(eventLoopGroups.size());
|
||||
loopGroups.addAll(eventLoopGroups);
|
||||
}
|
||||
|
||||
for (EventLoopGroup loopGroup : loopGroups) {
|
||||
Future<?> future = loopGroup.shutdownGracefully(maxShutdownWaitTimeInMilliSeconds / 10, maxShutdownWaitTimeInMilliSeconds, TimeUnit.MILLISECONDS);
|
||||
shutdownThreadList.add(future);
|
||||
Thread.yield();
|
||||
}
|
||||
|
||||
// now wait for them to finish!
|
||||
// It can take a few seconds to shut down the executor. This will affect unit testing, where connections are quickly created/stopped
|
||||
for (Future<?> f : shutdownThreadList) {
|
||||
try {
|
||||
f.await(maxShutdownWaitTimeInMilliSeconds);
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
Thread.yield();
|
||||
}
|
||||
}
|
||||
|
||||
protected final
|
||||
String stopWithErrorMessage(Logger logger, String errorMessage, Throwable throwable) {
|
||||
if (logger.isDebugEnabled() && throwable != null) {
|
||||
// extra info if debug is enabled
|
||||
logger.error(errorMessage, throwable.getCause());
|
||||
}
|
||||
else {
|
||||
logger.error(errorMessage);
|
||||
}
|
||||
|
||||
stop();
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the shutdown process during JVM shutdown, if necessary.
|
||||
* </p>
|
||||
* By default, we always can shutdown via the JVM shutdown hook.
|
||||
*/
|
||||
protected
|
||||
boolean shouldShutdownHookRun() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check to see if the current thread is running from it's OWN thread, or from Netty... This is used to prevent deadlocks.
|
||||
*
|
||||
* @return true if the specified thread is as Netty thread, false if it's own thread.
|
||||
*/
|
||||
protected
|
||||
boolean isInEventLoop(Thread thread) {
|
||||
for (EventLoopGroup loopGroup : eventLoopGroups) {
|
||||
for (EventExecutor next : loopGroup) {
|
||||
if (next.inEventLoop(thread)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely closes all associated resources/threads/connections.
|
||||
* <p/>
|
||||
* If we want to WAIT for this endpoint to shutdown, we must explicitly call waitForShutdown()
|
||||
* <p/>
|
||||
* Override stopExtraActions() if you want to provide extra behavior while stopping the endpoint
|
||||
*/
|
||||
public final
|
||||
void stop() {
|
||||
// only permit us to "stop" once!
|
||||
if (!stopCalled.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check to make sure we are in our OWN thread, otherwise, this thread will never exit -- because it will wait indefinitely
|
||||
// for itself to finish (since it blocks itself).
|
||||
// This occurs when calling stop from within a listener callback.
|
||||
Thread currentThread = Thread.currentThread();
|
||||
String threadName = currentThread.getName();
|
||||
boolean isShutdownThread = !threadName.equals(shutdownHookName) && !threadName.equals(stopTreadName);
|
||||
|
||||
// used to check the event groups to see if we are running from one of them. NOW we force to
|
||||
// ALWAYS shutdown inside a NEW thread
|
||||
if (!isShutdownThread || !isInEventLoop(currentThread)) {
|
||||
stopInThread();
|
||||
}
|
||||
else {
|
||||
Thread thread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
Shutdownable.this.stopInThread();
|
||||
}
|
||||
});
|
||||
thread.setDaemon(false);
|
||||
thread.setName(stopTreadName);
|
||||
thread.start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra EXTERNAL actions to perform when stopping this endpoint.
|
||||
*/
|
||||
protected
|
||||
void stopExtraActions() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions that happen by the endpoint before the channels are shutdown
|
||||
*/
|
||||
protected
|
||||
void shutdownChannelsPre() {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Actions that happen by the endpoint before any extra actions are run.
|
||||
*/
|
||||
protected
|
||||
void stopExtraActionsInternal() {
|
||||
}
|
||||
|
||||
// This actually does the "stopping", since there is some logic to making sure we don't deadlock, this is important
|
||||
private
|
||||
void stopInThread() {
|
||||
// make sure we are not trying to stop during a startup procedure.
|
||||
// This will wait until we have finished starting up/shutting down.
|
||||
synchronized (shutdownInProgress) {
|
||||
shutdownChannelsPre();
|
||||
shutdownAllChannels();
|
||||
shutdownEventLoops();
|
||||
|
||||
logger.info("Stopping endpoint.");
|
||||
|
||||
// there is no need to call "stop" again if we close the connection.
|
||||
// however, if this is called WHILE from the shutdown hook, blammo! problems!
|
||||
|
||||
// Also, you can call client/server.stop from another thread, which is run when the JVM is shutting down
|
||||
// (as there is nothing left to do), and also have problems.
|
||||
if (!Thread.currentThread()
|
||||
.getName()
|
||||
.equals(shutdownHookName)) {
|
||||
try {
|
||||
Runtime.getRuntime()
|
||||
.removeShutdownHook(shutdownHook);
|
||||
} catch (Exception e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
stopExtraActionsInternal();
|
||||
|
||||
// when the eventloop closes, the associated selectors are ALSO closed!
|
||||
stopExtraActions();
|
||||
|
||||
isShutdown = true;
|
||||
}
|
||||
|
||||
// tell the blocked "bind" method that it may continue (and exit)
|
||||
blockUntilDone.countDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Blocks the current thread until the endpoint has been stopped. If the endpoint is already stopped, this do nothing.
|
||||
*/
|
||||
public final
|
||||
void waitForShutdown() {
|
||||
// we now BLOCK until the stop method is called.
|
||||
try {
|
||||
blockUntilDone.await();
|
||||
} catch (InterruptedException e) {
|
||||
logger.error("Thread interrupted while waiting for stop!");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if we have already shutdown, false otherwise
|
||||
*/
|
||||
public final
|
||||
boolean isShutdown() {
|
||||
synchronized (shutdownInProgress) {
|
||||
return isShutdown;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
String toString() {
|
||||
return "EndPoint [" + getName() + "]";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the type class of this connection endpoint
|
||||
*/
|
||||
public Class<? extends Shutdownable> getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the simple name (for the class) of this connection endpoint
|
||||
*/
|
||||
public
|
||||
String getName() {
|
||||
return type.getSimpleName();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,176 @@
|
|||
package dorkbox.network.connection
|
||||
|
||||
import dorkbox.network.aeron.client.ClientException
|
||||
import dorkbox.network.aeron.client.ClientTimedOutException
|
||||
import dorkbox.network.aeron.server.ServerException
|
||||
import io.aeron.Aeron
|
||||
import io.aeron.ChannelUriStringBuilder
|
||||
import io.aeron.Publication
|
||||
import io.aeron.Subscription
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
interface MediaDriverConnection : AutoCloseable {
|
||||
val address: String
|
||||
val streamId: Int
|
||||
val sessionId: Int
|
||||
|
||||
val subscriptionPort: Int
|
||||
val publicationPort: Int
|
||||
|
||||
val subscription: Subscription
|
||||
val publication: Publication
|
||||
|
||||
val isReliable: Boolean
|
||||
|
||||
@Throws(ClientTimedOutException::class)
|
||||
suspend fun buildClient(aeron: Aeron)
|
||||
|
||||
fun buildServer(aeron: Aeron)
|
||||
|
||||
fun clientInfo() : String
|
||||
fun serverInfo() : String
|
||||
}
|
||||
|
||||
/**
|
||||
* For a client, the ports specified here MUST be manually flipped because they are in the perspective of the SERVER
|
||||
*/
|
||||
class UdpMediaDriverConnection(override val address: String,
|
||||
override val subscriptionPort: Int,
|
||||
override val publicationPort: Int,
|
||||
override val streamId: Int,
|
||||
override val sessionId: Int,
|
||||
private val connectionTimeoutMS: Long = 0,
|
||||
override val isReliable: Boolean = true) : MediaDriverConnection {
|
||||
|
||||
override lateinit var subscription: Subscription
|
||||
override lateinit var publication: Publication
|
||||
|
||||
var success: Boolean = false
|
||||
|
||||
|
||||
private fun uri(): ChannelUriStringBuilder {
|
||||
val builder = ChannelUriStringBuilder().reliable(isReliable).media("udp")
|
||||
if (sessionId != EndPoint.RESERVED_SESSION_ID_INVALID) {
|
||||
builder.sessionId(sessionId)
|
||||
}
|
||||
|
||||
return builder
|
||||
}
|
||||
|
||||
override suspend fun buildClient(aeron: Aeron) {
|
||||
if (address.isEmpty()) {
|
||||
throw ClientException("Invalid address : '$address'")
|
||||
}
|
||||
|
||||
// Create a subscription with a control port (for dynamic MDC) at the given address and port, using the given stream ID.
|
||||
val subscriptionUri = uri()
|
||||
.controlEndpoint("$address:$subscriptionPort")
|
||||
.controlMode("dynamic")
|
||||
|
||||
|
||||
// Create a publication at the given address and port, using the given stream ID.
|
||||
// Note: The Aeron.addPublication method will block until the Media Driver acknowledges the request or a timeout occurs.
|
||||
val publicationUri = uri()
|
||||
.endpoint("$address:$publicationPort")
|
||||
|
||||
|
||||
// NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe
|
||||
// publication of any state to other threads and not be long running or re-entrant with the client.
|
||||
val subscription = aeron.addSubscription(subscriptionUri.build(), streamId)
|
||||
val publication = aeron.addPublication(publicationUri.build(), streamId)
|
||||
|
||||
var success = false
|
||||
|
||||
// this will wait for the server to acknowledge the connection (all via aeron)
|
||||
var startTime = System.currentTimeMillis()
|
||||
while (System.currentTimeMillis() - startTime < connectionTimeoutMS) {
|
||||
if (subscription.isConnected && subscription.imageCount() > 0) {
|
||||
success = true
|
||||
break
|
||||
}
|
||||
|
||||
delay(timeMillis = 10L)
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
subscription.close()
|
||||
throw ClientTimedOutException("Creating subscription connection to aeron")
|
||||
}
|
||||
|
||||
|
||||
success = false
|
||||
|
||||
// this will wait for the server to acknowledge the connection (all via aeron)
|
||||
startTime = System.currentTimeMillis()
|
||||
while (System.currentTimeMillis() - startTime < connectionTimeoutMS) {
|
||||
if (publication.isConnected) {
|
||||
success = true
|
||||
break
|
||||
}
|
||||
|
||||
delay(timeMillis = 10L)
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
subscription.close()
|
||||
publication.close()
|
||||
throw ClientTimedOutException("Creating publication connection to aeron")
|
||||
}
|
||||
|
||||
this.success = true
|
||||
|
||||
this.subscription = subscription
|
||||
this.publication = publication
|
||||
}
|
||||
|
||||
override fun buildServer(aeron: Aeron) {
|
||||
if (address.isEmpty()) {
|
||||
throw ServerException("Invalid address. It is empty!")
|
||||
}
|
||||
|
||||
// Create a subscription with a control port (for dynamic MDC) at the given address and port, using the given stream ID.
|
||||
val subscriptionUri = uri()
|
||||
.endpoint("$address:$subscriptionPort")
|
||||
|
||||
|
||||
// Create a publication with a control port (for dynamic MDC) at the given address and port, using the given stream ID.
|
||||
// Note: The Aeron.addPublication method will block until the Media Driver acknowledges the request or a timeout occurs.
|
||||
val publicationUri = uri()
|
||||
.controlEndpoint("$address:$publicationPort")
|
||||
.controlMode("dynamic")
|
||||
|
||||
|
||||
// NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe
|
||||
// publication of any state to other threads and not be long running or re-entrant with the client.
|
||||
subscription = aeron.addSubscription(subscriptionUri.build(), streamId)
|
||||
publication = aeron.addPublication(publicationUri.build(), streamId)
|
||||
}
|
||||
|
||||
|
||||
override fun clientInfo(): String {
|
||||
return if (sessionId != EndPoint.RESERVED_SESSION_ID_INVALID) {
|
||||
"Connecting to $address [$subscriptionPort|$publicationPort] [$streamId|$sessionId] (reliable:$isReliable)"
|
||||
} else {
|
||||
"Connecting to $address [$subscriptionPort|$publicationPort] [$streamId] (reliable:$isReliable)"
|
||||
}
|
||||
}
|
||||
|
||||
override fun serverInfo(): String {
|
||||
return if (sessionId != EndPoint.RESERVED_SESSION_ID_INVALID) {
|
||||
"Listening on $address [$subscriptionPort|$publicationPort] [$streamId|$sessionId] (reliable:$isReliable)"
|
||||
} else {
|
||||
"Listening on $address [$subscriptionPort|$publicationPort] [$streamId] (reliable:$isReliable)"
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
if (success) {
|
||||
subscription.close()
|
||||
publication.close()
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "$address [$subscriptionPort|$publicationPort] [$streamId|$sessionId] (reliable:$isReliable)"
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection.bridge;
|
||||
|
||||
import dorkbox.network.connection.Ping;
|
||||
|
||||
public
|
||||
interface ConnectionBridge extends ConnectionBridgeBase {
|
||||
/**
|
||||
* Sends a "ping" packet, trying UDP then TCP (in that order) to measure <b>ROUND TRIP</b> time to the remote connection.
|
||||
*
|
||||
* @return Ping can have a listener attached, which will get called when the ping returns.
|
||||
*/
|
||||
Ping ping();
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection.bridge;
|
||||
|
||||
import dorkbox.network.connection.ConnectionPoint;
|
||||
|
||||
public
|
||||
interface ConnectionBridgeBase {
|
||||
/**
|
||||
* Sends the message to other listeners INSIDE this endpoint. It does not send it to a remote address.
|
||||
*/
|
||||
ConnectionPoint self(Object message);
|
||||
|
||||
/**
|
||||
* Sends the message over the network using TCP. (or via LOCAL when it's a local channel).
|
||||
*/
|
||||
ConnectionPoint TCP(Object message);
|
||||
|
||||
/**
|
||||
* Sends the message over the network using UDP (or via LOCAL when it's a local channel).
|
||||
*/
|
||||
ConnectionPoint UDP(Object message);
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection.bridge;
|
||||
|
||||
public
|
||||
interface ConnectionBridgeServer extends ConnectionBridgeBase {
|
||||
|
||||
/**
|
||||
* Exposes methods to send the object to all server connections (except the specified one) over the network. (or via LOCAL when it's a
|
||||
* local channel).
|
||||
*/
|
||||
ConnectionExceptSpecifiedBridgeServer except();
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection.bridge;
|
||||
|
||||
import dorkbox.network.connection.Connection;
|
||||
import dorkbox.network.connection.ConnectionPoint;
|
||||
|
||||
public
|
||||
interface ConnectionExceptSpecifiedBridgeServer {
|
||||
|
||||
/**
|
||||
* Sends the object to all server connections (except the specified one) over the network using TCP. (or via LOCAL when it's a local
|
||||
* channel).
|
||||
*/
|
||||
ConnectionPoint TCP(Connection connection, Object message);
|
||||
|
||||
/**
|
||||
* Sends the object to all server connections (except the specified one) over the network using UDP (or via LOCAL when it's a local
|
||||
* channel).
|
||||
*/
|
||||
ConnectionPoint UDP(Connection connection, Object message);
|
||||
}
|
|
@ -5,7 +5,7 @@ import dorkbox.network.connection.registration.UpgradeType;
|
|||
/**
|
||||
* Used in {@link IpConnectionTypeRule} to decide what kind of connection a matching IP Address should have.
|
||||
*/
|
||||
public enum ConnectionType {
|
||||
public enum ConnectionProperties {
|
||||
/**
|
||||
* No compression, no encryption
|
||||
*/
|
||||
|
@ -24,7 +24,7 @@ public enum ConnectionType {
|
|||
|
||||
private final byte type;
|
||||
|
||||
ConnectionType(byte type) {
|
||||
ConnectionProperties(byte type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
|
@ -1,163 +0,0 @@
|
|||
package dorkbox.network.connection.connectionType;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
import io.netty.util.internal.SocketUtils;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public
|
||||
class ConnectionRule {
|
||||
|
||||
private final IpConnectionTypeRule filterRule;
|
||||
|
||||
public
|
||||
ConnectionRule(String ipAddress, int cidrPrefix, ConnectionType ruleType) {
|
||||
try {
|
||||
filterRule = selectFilterRule(SocketUtils.addressByName(ipAddress), cidrPrefix, ruleType);
|
||||
} catch (UnknownHostException e) {
|
||||
throw new IllegalArgumentException("ipAddress", e);
|
||||
}
|
||||
}
|
||||
|
||||
public
|
||||
ConnectionRule(InetAddress ipAddress, int cidrPrefix, ConnectionType ruleType) {
|
||||
filterRule = selectFilterRule(ipAddress, cidrPrefix, ruleType);
|
||||
}
|
||||
|
||||
public boolean matches(InetSocketAddress remoteAddress) {
|
||||
return filterRule.matches(remoteAddress);
|
||||
}
|
||||
|
||||
public
|
||||
ConnectionType ruleType() {
|
||||
return filterRule.ruleType();
|
||||
}
|
||||
|
||||
|
||||
private static
|
||||
IpConnectionTypeRule selectFilterRule(InetAddress ipAddress, int cidrPrefix, ConnectionType ruleType) {
|
||||
if (ipAddress == null) {
|
||||
throw new NullPointerException("ipAddress");
|
||||
}
|
||||
|
||||
if (ruleType == null) {
|
||||
throw new NullPointerException("ruleType");
|
||||
}
|
||||
|
||||
if (ipAddress instanceof Inet4Address) {
|
||||
return new Ip4SubnetFilterRule((Inet4Address) ipAddress, cidrPrefix, ruleType);
|
||||
} else if (ipAddress instanceof Inet6Address) {
|
||||
return new Ip6SubnetFilterRule((Inet6Address) ipAddress, cidrPrefix, ruleType);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Only IPv4 and IPv6 addresses are supported");
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Ip4SubnetFilterRule implements IpConnectionTypeRule {
|
||||
private final int networkAddress;
|
||||
private final int subnetMask;
|
||||
private final ConnectionType ruleType;
|
||||
|
||||
private Ip4SubnetFilterRule(Inet4Address ipAddress, int cidrPrefix, ConnectionType ruleType) {
|
||||
if (cidrPrefix < 0 || cidrPrefix > 32) {
|
||||
throw new IllegalArgumentException(String.format("IPv4 requires the subnet prefix to be in range of " +
|
||||
"[0,32]. The prefix was: %d", cidrPrefix));
|
||||
}
|
||||
|
||||
subnetMask = prefixToSubnetMask(cidrPrefix);
|
||||
networkAddress = ipToInt(ipAddress) & subnetMask;
|
||||
this.ruleType = ruleType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(InetSocketAddress remoteAddress) {
|
||||
final InetAddress inetAddress = remoteAddress.getAddress();
|
||||
if (inetAddress instanceof Inet4Address) {
|
||||
int ipAddress = ipToInt((Inet4Address) inetAddress);
|
||||
return (ipAddress & subnetMask) == networkAddress;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConnectionType ruleType() {
|
||||
return ruleType;
|
||||
}
|
||||
|
||||
private static int ipToInt(Inet4Address ipAddress) {
|
||||
byte[] octets = ipAddress.getAddress();
|
||||
assert octets.length == 4;
|
||||
|
||||
return (octets[0] & 0xff) << 24 |
|
||||
(octets[1] & 0xff) << 16 |
|
||||
(octets[2] & 0xff) << 8 |
|
||||
octets[3] & 0xff;
|
||||
}
|
||||
|
||||
private static int prefixToSubnetMask(int cidrPrefix) {
|
||||
/**
|
||||
* Perform the shift on a long and downcast it to int afterwards.
|
||||
* This is necessary to handle a cidrPrefix of zero correctly.
|
||||
* The left shift operator on an int only uses the five least
|
||||
* significant bits of the right-hand operand. Thus -1 << 32 evaluates
|
||||
* to -1 instead of 0. The left shift operator applied on a long
|
||||
* uses the six least significant bits.
|
||||
*
|
||||
* Also see https://github.com/netty/netty/issues/2767
|
||||
*/
|
||||
return (int) ((-1L << 32 - cidrPrefix) & 0xffffffff);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Ip6SubnetFilterRule implements IpConnectionTypeRule {
|
||||
private static final BigInteger MINUS_ONE = BigInteger.valueOf(-1);
|
||||
|
||||
private final BigInteger networkAddress;
|
||||
private final BigInteger subnetMask;
|
||||
private final ConnectionType ruleType;
|
||||
|
||||
private Ip6SubnetFilterRule(Inet6Address ipAddress, int cidrPrefix, ConnectionType ruleType) {
|
||||
if (cidrPrefix < 0 || cidrPrefix > 128) {
|
||||
throw new IllegalArgumentException(String.format("IPv6 requires the subnet prefix to be in range of " +
|
||||
"[0,128]. The prefix was: %d", cidrPrefix));
|
||||
}
|
||||
|
||||
subnetMask = prefixToSubnetMask(cidrPrefix);
|
||||
networkAddress = ipToInt(ipAddress).and(subnetMask);
|
||||
this.ruleType = ruleType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(InetSocketAddress remoteAddress) {
|
||||
final InetAddress inetAddress = remoteAddress.getAddress();
|
||||
if (inetAddress instanceof Inet6Address) {
|
||||
BigInteger ipAddress = ipToInt((Inet6Address) inetAddress);
|
||||
return ipAddress.and(subnetMask).equals(networkAddress);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConnectionType ruleType() {
|
||||
return ruleType;
|
||||
}
|
||||
|
||||
private static BigInteger ipToInt(Inet6Address ipAddress) {
|
||||
byte[] octets = ipAddress.getAddress();
|
||||
assert octets.length == 16;
|
||||
|
||||
return new BigInteger(octets);
|
||||
}
|
||||
|
||||
private static BigInteger prefixToSubnetMask(int cidrPrefix) {
|
||||
return MINUS_ONE.shiftLeft(128 - cidrPrefix);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
package dorkbox.network.connection.connectionType
|
||||
|
||||
import java.math.BigInteger
|
||||
import java.net.Inet4Address
|
||||
import java.net.Inet6Address
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class ConnectionRule(ipAddress: InetAddress, cidrPrefix: Int, ruleType: ConnectionProperties) {
|
||||
private val filterRule: IpConnectionTypeRule
|
||||
|
||||
companion object {
|
||||
private fun selectFilterRule(ipAddress: InetAddress, cidrPrefix: Int, ruleType: ConnectionProperties): IpConnectionTypeRule {
|
||||
return when (ipAddress) {
|
||||
is Inet4Address -> {
|
||||
Ip4SubnetFilterRule(ipAddress, cidrPrefix, ruleType)
|
||||
}
|
||||
is Inet6Address -> {
|
||||
Ip6SubnetFilterRule(ipAddress, cidrPrefix, ruleType)
|
||||
}
|
||||
else -> {
|
||||
throw IllegalArgumentException("Only IPv4 and IPv6 addresses are supported")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
filterRule = selectFilterRule(ipAddress, cidrPrefix, ruleType)
|
||||
}
|
||||
|
||||
fun matches(remoteAddress: InetSocketAddress): Boolean {
|
||||
return filterRule.matches(remoteAddress)
|
||||
}
|
||||
|
||||
fun ruleType(): ConnectionProperties {
|
||||
return filterRule.ruleType()
|
||||
}
|
||||
|
||||
private class Ip4SubnetFilterRule(ipAddress: Inet4Address, cidrPrefix: Int, ruleType: ConnectionProperties) : IpConnectionTypeRule {
|
||||
|
||||
companion object {
|
||||
private fun ipToInt(ipAddress: Inet4Address): Int {
|
||||
val octets = ipAddress.address
|
||||
assert(octets.size == 4)
|
||||
return (0xff and octets[0].toInt()) shl 24 or (
|
||||
(0xff and octets[1].toInt()) shl 16) or (
|
||||
(0xff and octets[2].toInt()) shl 8) or (
|
||||
(0xff and octets[3].toInt()) )
|
||||
}
|
||||
|
||||
|
||||
private fun prefixToSubnetMask(cidrPrefix: Int): Int {
|
||||
/**
|
||||
* Perform the shift on a long and downcast it to int afterwards.
|
||||
* This is necessary to handle a cidrPrefix of zero correctly.
|
||||
* The left shift operator on an int only uses the five least
|
||||
* significant bits of the right-hand operand. Thus -1 << 32 evaluates
|
||||
* to -1 instead of 0. The left shift operator applied on a long
|
||||
* uses the six least significant bits.
|
||||
*
|
||||
* Also see https://github.com/netty/netty/issues/2767
|
||||
*/
|
||||
return (-1L shl 32 - cidrPrefix and -0x1).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
private val networkAddress: Int
|
||||
private val subnetMask: Int
|
||||
private val ruleType: ConnectionProperties
|
||||
|
||||
init {
|
||||
require(!(cidrPrefix < 0 || cidrPrefix > 32)) {
|
||||
String.format("IPv4 requires the subnet prefix to be in range of " +
|
||||
"[0,32]. The prefix was: %d", cidrPrefix)
|
||||
}
|
||||
subnetMask = prefixToSubnetMask(cidrPrefix)
|
||||
networkAddress = ipToInt(ipAddress) and subnetMask
|
||||
this.ruleType = ruleType
|
||||
}
|
||||
|
||||
override fun matches(remoteAddress: InetSocketAddress): Boolean {
|
||||
val inetAddress = remoteAddress.address
|
||||
if (inetAddress is Inet4Address) {
|
||||
val ipAddress = ipToInt(inetAddress)
|
||||
return ipAddress and subnetMask == networkAddress
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun ruleType(): ConnectionProperties {
|
||||
return ruleType
|
||||
}
|
||||
}
|
||||
|
||||
private class Ip6SubnetFilterRule(ipAddress: Inet6Address, cidrPrefix: Int, ruleType: ConnectionProperties) : IpConnectionTypeRule {
|
||||
|
||||
companion object {
|
||||
private val MINUS_ONE = BigInteger.valueOf(-1)
|
||||
|
||||
private fun ipToInt(ipAddress: Inet6Address): BigInteger {
|
||||
val octets = ipAddress.address
|
||||
assert(octets.size == 16)
|
||||
return BigInteger(octets)
|
||||
}
|
||||
|
||||
private fun prefixToSubnetMask(cidrPrefix: Int): BigInteger {
|
||||
return MINUS_ONE.shiftLeft(128 - cidrPrefix)
|
||||
}
|
||||
}
|
||||
|
||||
private val networkAddress: BigInteger
|
||||
private val subnetMask: BigInteger
|
||||
private val ruleType: ConnectionProperties
|
||||
|
||||
init {
|
||||
require(!(cidrPrefix < 0 || cidrPrefix > 128)) {
|
||||
String.format("IPv6 requires the subnet prefix to be in range of " +
|
||||
"[0,128]. The prefix was: %d", cidrPrefix)
|
||||
}
|
||||
subnetMask = prefixToSubnetMask(cidrPrefix)
|
||||
networkAddress = ipToInt(ipAddress).and(subnetMask)
|
||||
this.ruleType = ruleType
|
||||
}
|
||||
|
||||
override fun matches(remoteAddress: InetSocketAddress): Boolean {
|
||||
val inetAddress = remoteAddress.address
|
||||
if (inetAddress is Inet6Address) {
|
||||
val ipAddress = ipToInt(inetAddress)
|
||||
return ipAddress.and(subnetMask) == networkAddress
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun ruleType(): ConnectionProperties {
|
||||
return ruleType
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
package dorkbox.network.connection.connectionType;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
import io.netty.handler.ipfilter.IpFilterRuleType;
|
||||
|
||||
/**
|
||||
* Implement this interface to create new rules.
|
||||
*/
|
||||
public interface IpConnectionTypeRule {
|
||||
/**
|
||||
* @return This method should return true if remoteAddress is valid according to your criteria. False otherwise.
|
||||
*/
|
||||
boolean matches(InetSocketAddress remoteAddress);
|
||||
|
||||
/**
|
||||
* @return This method should return {@link IpFilterRuleType#ACCEPT} if all
|
||||
* {@link IpConnectionTypeRule#matches(InetSocketAddress)} for which {@link #matches(InetSocketAddress)}
|
||||
* returns true should the accepted. If you want to exclude all of those IP addresses then
|
||||
* {@link IpFilterRuleType#REJECT} should be returned.
|
||||
*/
|
||||
ConnectionType ruleType();
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package dorkbox.network.connection.connectionType
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
/**
|
||||
* Implement this interface to create new rules.
|
||||
*/
|
||||
interface IpConnectionTypeRule {
|
||||
/**
|
||||
* @return This method should return true if remoteAddress is valid according to your criteria. False otherwise.
|
||||
*/
|
||||
fun matches(remoteAddress: InetSocketAddress): Boolean
|
||||
|
||||
/**
|
||||
* @return This method should return [IpFilterRuleType.ACCEPT] if all
|
||||
* [IpConnectionTypeRule.matches] for which [.matches]
|
||||
* returns true should the accepted. If you want to exclude all of those IP addresses then
|
||||
* [IpFilterRuleType.REJECT] should be returned.
|
||||
*/
|
||||
fun ruleType(): ConnectionProperties
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection.listenerManagement;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import dorkbox.network.connection.Connection;
|
||||
import dorkbox.network.connection.Listener;
|
||||
import dorkbox.network.connection.Listener.OnError;
|
||||
import dorkbox.util.collections.ConcurrentEntry;
|
||||
import dorkbox.util.collections.ConcurrentIterator;
|
||||
|
||||
public abstract
|
||||
class ConcurrentManager<C extends Connection, T extends Listener> extends ConcurrentIterator<T> {
|
||||
|
||||
private final Logger logger;
|
||||
|
||||
ConcurrentManager(final Logger logger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized
|
||||
void add(final T listener) {
|
||||
super.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* The returned value indicates how many listeners are left in this manager
|
||||
*
|
||||
* @return >= 0 if the listener was removed, -1 otherwise
|
||||
*/
|
||||
public synchronized
|
||||
int removeWithSize(final T listener) {
|
||||
boolean removed = super.remove(listener);
|
||||
|
||||
if (removed) {
|
||||
return super.size();
|
||||
}
|
||||
else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if a listener was found, false otherwise
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
boolean doAction(final C connection, final AtomicBoolean shutdown) {
|
||||
// access a snapshot (single-writer-principle)
|
||||
ConcurrentEntry<T> head = headREF.get(this);
|
||||
ConcurrentEntry<T> current = head;
|
||||
|
||||
T listener;
|
||||
while (current != null && !shutdown.get()) {
|
||||
listener = current.getValue();
|
||||
current = current.next();
|
||||
|
||||
// Concurrent iteration...
|
||||
try {
|
||||
listenerAction(connection, listener);
|
||||
} catch (Exception e) {
|
||||
if (listener instanceof OnError) {
|
||||
((OnError) listener).error(connection, e);
|
||||
}
|
||||
else {
|
||||
logger.error("Unable to notify listener '{}', connection '{}'.", listener, connection, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return head != null; // true if we have something, otherwise false
|
||||
}
|
||||
|
||||
abstract void listenerAction(final C connection, final T listener) throws Exception;
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection.listenerManagement;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import dorkbox.network.connection.Connection;
|
||||
import dorkbox.network.connection.Listener.OnConnected;
|
||||
|
||||
/**
|
||||
* Called when the remote computer has been connected. This will be invoked before any objects are received by the network.
|
||||
* This method should not block for long periods as other network activity will not be processed
|
||||
* until it returns.
|
||||
*/
|
||||
public final
|
||||
class OnConnectedManager<C extends Connection> extends ConcurrentManager<C, OnConnected<C>> {
|
||||
|
||||
public
|
||||
OnConnectedManager(final Logger logger) {
|
||||
super(logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if a listener was found, false otherwise
|
||||
*/
|
||||
public
|
||||
boolean notifyConnected(final C connection, final AtomicBoolean shutdown) {
|
||||
return doAction(connection, shutdown);
|
||||
}
|
||||
|
||||
@Override
|
||||
void listenerAction(final C connection, final OnConnected<C> listener) throws Exception {
|
||||
listener.connected(connection);
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection.listenerManagement;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import dorkbox.network.connection.Connection;
|
||||
import dorkbox.network.connection.Listener.OnDisconnected;
|
||||
|
||||
/**
|
||||
* Called when the remote end has been connected. This will be invoked before any objects are received by the network.
|
||||
* This method should not block for long periods as other network activity will not be processed
|
||||
* until it returns.
|
||||
*/
|
||||
public final
|
||||
class OnDisconnectedManager<C extends Connection> extends ConcurrentManager<C, OnDisconnected<C>> {
|
||||
|
||||
private static final AtomicBoolean disconnectBoolean = new AtomicBoolean(false);
|
||||
|
||||
public
|
||||
OnDisconnectedManager(final Logger logger) {
|
||||
super(logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if a listener was found, false otherwise
|
||||
*/
|
||||
public
|
||||
boolean notifyDisconnected(final C connection) {
|
||||
// we override the boolean, because we ALWAYS want to call the disconnect listeners!
|
||||
return doAction(connection, disconnectBoolean);
|
||||
}
|
||||
|
||||
@Override
|
||||
void listenerAction(final C connection, final OnDisconnected<C> listener) throws Exception {
|
||||
listener.disconnected(connection);
|
||||
}
|
||||
}
|
|
@ -1,259 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection.listenerManagement;
|
||||
|
||||
import static dorkbox.util.collections.ConcurrentIterator.headREF;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import com.esotericsoftware.kryo.util.IdentityMap;
|
||||
|
||||
import dorkbox.network.connection.Connection;
|
||||
import dorkbox.network.connection.ConnectionManager;
|
||||
import dorkbox.network.connection.Listener;
|
||||
import dorkbox.network.connection.Listener.OnError;
|
||||
import dorkbox.network.connection.Listener.OnMessageReceived;
|
||||
import dorkbox.network.connection.Listener.SelfDefinedType;
|
||||
import dorkbox.util.collections.ConcurrentEntry;
|
||||
import dorkbox.util.collections.ConcurrentIterator;
|
||||
import dorkbox.util.generics.ClassHelper;
|
||||
|
||||
/**
|
||||
* Called when the remote end has been connected. This will be invoked before any objects are received by the network.
|
||||
* This method should not block for long periods as other network activity will not be processed
|
||||
* until it returns.
|
||||
*/
|
||||
@SuppressWarnings("Duplicates")
|
||||
public final
|
||||
class OnMessageReceivedManager<C extends Connection> {
|
||||
// Recommended for best performance while adhering to the "single writer principle". Must be static-final
|
||||
private static final AtomicReferenceFieldUpdater<OnMessageReceivedManager, IdentityMap> REF = AtomicReferenceFieldUpdater.newUpdater(
|
||||
OnMessageReceivedManager.class,
|
||||
IdentityMap.class,
|
||||
"listeners");
|
||||
|
||||
/**
|
||||
* Gets the referenced object type for a specific listener, but ONLY necessary for listeners that receive messages
|
||||
* <p/>
|
||||
* This works for compile time code. The generic type parameter #2 (index 1) is pulled from type arguments.
|
||||
* generic parameters cannot be primitive types
|
||||
*/
|
||||
private static
|
||||
Class<?> identifyType(final OnMessageReceived listener) {
|
||||
if (listener instanceof SelfDefinedType) {
|
||||
return ((SelfDefinedType) listener).getType();
|
||||
}
|
||||
|
||||
final Class<?> clazz = listener.getClass();
|
||||
Class<?> objectType = ClassHelper.getGenericParameterAsClassForSuperClass(Listener.OnMessageReceived.class, clazz, 1);
|
||||
|
||||
if (objectType != null) {
|
||||
// SOMETIMES generics get confused on which parameter we actually mean (when sub-classing)
|
||||
if (objectType != Object.class && ClassHelper.hasInterface(Connection.class, objectType)) {
|
||||
Class<?> objectType2 = ClassHelper.getGenericParameterAsClassForSuperClass(Listener.OnMessageReceived.class, clazz, 2);
|
||||
if (objectType2 != null) {
|
||||
objectType = objectType2;
|
||||
}
|
||||
}
|
||||
|
||||
return objectType;
|
||||
}
|
||||
else {
|
||||
// there was no defined parameters
|
||||
return Object.class;
|
||||
}
|
||||
}
|
||||
|
||||
private final Logger logger;
|
||||
|
||||
//
|
||||
// The iterators for IdentityMap are NOT THREAD SAFE!
|
||||
//
|
||||
private volatile IdentityMap<Type, ConcurrentIterator> listeners = new IdentityMap<Type, ConcurrentIterator>(32, ConnectionManager.LOAD_FACTOR);
|
||||
|
||||
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
|
||||
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
|
||||
// use-case 99% of the time)
|
||||
|
||||
public
|
||||
OnMessageReceivedManager(final Logger logger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public
|
||||
void add(final OnMessageReceived listener) {
|
||||
final Class<?> type = identifyType(listener);
|
||||
|
||||
synchronized (this) {
|
||||
ConcurrentIterator subscribedListeners = listeners.get(type);
|
||||
if (subscribedListeners == null) {
|
||||
subscribedListeners = new ConcurrentIterator();
|
||||
listeners.put(type, subscribedListeners);
|
||||
}
|
||||
|
||||
//noinspection unchecked
|
||||
subscribedListeners.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The returned value indicates how many listeners are left in this manager
|
||||
*
|
||||
* @return >= 0 if the listener was removed, -1 otherwise
|
||||
*/
|
||||
public
|
||||
int removeWithSize(final OnMessageReceived listener) {
|
||||
final Class<?> type = identifyType(listener);
|
||||
|
||||
int size = -1; // default is "not found"
|
||||
synchronized (this) {
|
||||
// access a snapshot of the listeners (single-writer-principle)
|
||||
final ConcurrentIterator concurrentIterator = listeners.get(type);
|
||||
if (concurrentIterator != null) {
|
||||
//noinspection unchecked
|
||||
boolean removed = concurrentIterator.remove(listener);
|
||||
if (removed) {
|
||||
size = concurrentIterator.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if a listener was found, false otherwise
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public
|
||||
boolean notifyReceived(final C connection, final Object message, final AtomicBoolean shutdown) {
|
||||
boolean found = false;
|
||||
Class<?> objectType = message.getClass();
|
||||
|
||||
|
||||
// this is the GLOBAL version (unless it's the call from below, then it's the connection scoped version)
|
||||
final IdentityMap<Type, ConcurrentIterator> listeners = REF.get(this);
|
||||
ConcurrentIterator concurrentIterator = listeners.get(objectType);
|
||||
|
||||
if (concurrentIterator != null) {
|
||||
ConcurrentEntry<OnMessageReceived<C, Object>> head = headREF.get(concurrentIterator);
|
||||
ConcurrentEntry<OnMessageReceived<C, Object>> current = head;
|
||||
|
||||
OnMessageReceived<C, Object> listener;
|
||||
while (current != null && !shutdown.get()) {
|
||||
listener = current.getValue();
|
||||
current = current.next();
|
||||
|
||||
try {
|
||||
listener.received(connection, message);
|
||||
} catch (Exception e) {
|
||||
if (listener instanceof OnError) {
|
||||
((OnError<C>) listener).error(connection, e);
|
||||
}
|
||||
else {
|
||||
logger.error("Unable to notify on message '{}' for listener '{}', connection '{}'.",
|
||||
objectType,
|
||||
listener,
|
||||
connection,
|
||||
e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
found = head != null; // true if we have something to publish to, otherwise false
|
||||
}
|
||||
|
||||
// we march through all super types of the object, and find the FIRST set
|
||||
// of listeners that are registered and cast it as that, and notify the method.
|
||||
// NOTICE: we do NOT call ALL TYPES -- meaning, if we have Object->Foo->Bar
|
||||
// and have listeners for Object and Foo
|
||||
// we will call Bar (from the above code)
|
||||
// we will call Foo (from this code)
|
||||
// we will NOT call Object (since we called Foo). If Foo was not registered, THEN we would call object!
|
||||
|
||||
objectType = objectType.getSuperclass();
|
||||
while (objectType != null) {
|
||||
// check to see if we have what we are looking for in our CURRENT class
|
||||
concurrentIterator = listeners.get(objectType);
|
||||
if (concurrentIterator != null) {
|
||||
ConcurrentEntry<OnMessageReceived<C, Object>> head = headREF.get(concurrentIterator);
|
||||
ConcurrentEntry<OnMessageReceived<C, Object>> current = head;
|
||||
|
||||
OnMessageReceived<C, Object> listener;
|
||||
while (current != null && !shutdown.get()) {
|
||||
listener = current.getValue();
|
||||
current = current.next();
|
||||
|
||||
try {
|
||||
listener.received(connection, message);
|
||||
} catch (Exception e) {
|
||||
if (listener instanceof OnError) {
|
||||
((OnError<C>) listener).error(connection, e);
|
||||
}
|
||||
else {
|
||||
logger.error("Unable to notify on message '{}' for listener '{}', connection '{}'.",
|
||||
objectType,
|
||||
listener,
|
||||
connection,
|
||||
e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
found = head != null; // true if we have something to publish to, otherwise false
|
||||
break;
|
||||
}
|
||||
|
||||
// NO MATCH, so walk up.
|
||||
objectType = objectType.getSuperclass();
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the listener was removed, false otherwise
|
||||
*/
|
||||
public synchronized
|
||||
boolean removeAll(final Class<?> classType) {
|
||||
boolean found;
|
||||
|
||||
found = listeners.remove(classType) != null;
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
/**
|
||||
* called on shutdown
|
||||
*/
|
||||
public synchronized
|
||||
void clear() {
|
||||
// The iterators for this map are NOT THREAD SAFE!
|
||||
// using .entries() is what we are supposed to use!
|
||||
final IdentityMap.Entries<Type, ConcurrentIterator> entries = listeners.entries();
|
||||
for (final IdentityMap.Entry<Type, ConcurrentIterator> next : entries) {
|
||||
if (next.value != null) {
|
||||
next.value.clear();
|
||||
}
|
||||
}
|
||||
|
||||
listeners.clear();
|
||||
}
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection.registration;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
|
||||
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
|
||||
|
||||
import dorkbox.network.connection.ConnectionImpl;
|
||||
import io.netty.channel.Channel;
|
||||
|
||||
public
|
||||
class MetaChannel {
|
||||
|
||||
// used to keep track and associate TCP/UDP/etc sessions. This is always defined by the server
|
||||
// a sessionId if '0', means we are still figuring it out.
|
||||
public volatile int sessionId;
|
||||
|
||||
public volatile Channel localChannel = null; // only available for local "in jvm" channels. XOR with tcp/udp channels with CLIENT, server can have all types at once
|
||||
public volatile Channel tcpChannel = null;
|
||||
public volatile Channel udpChannel = null;
|
||||
|
||||
// keep track of how many protocols to register, so that way when we are ready to connect the SERVER sends a message to the client over
|
||||
// all registered protocols and the last protocol to receive the message does the registration
|
||||
public AtomicInteger totalProtocols = new AtomicInteger(0);
|
||||
|
||||
|
||||
// only permits the FIST protocol for running the pipeline upgrade.
|
||||
public AtomicBoolean canUpgradePipeline = new AtomicBoolean(true);
|
||||
|
||||
public volatile ConnectionImpl connection; // only needed until the connection has been notified.
|
||||
|
||||
public volatile ECPublicKeyParameters publicKey; // used for ECC crypto + handshake on NETWORK (remote) connections. This is the remote public key.
|
||||
public volatile AsymmetricCipherKeyPair ecdhKey; // used for ECC Diffie-Hellman-Merkle key exchanges: see http://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange
|
||||
|
||||
public volatile SecretKey secretKey;
|
||||
|
||||
// indicates if the remote ECC key has changed for an IP address. If the client detects this, it will not connect.
|
||||
// If the server detects this, it has the option for additional security (two-factor auth, perhaps?)
|
||||
public volatile boolean changedRemoteKey = false;
|
||||
|
||||
public volatile byte remainingFragments;
|
||||
public volatile byte[] fragmentedRegistrationDetails;
|
||||
|
||||
|
||||
public
|
||||
MetaChannel(final int sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection.registration;
|
||||
|
||||
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
|
||||
import org.bouncycastle.crypto.params.IESParameters;
|
||||
|
||||
/**
|
||||
* Internal message to handle the TCP/UDP registration process
|
||||
*/
|
||||
public
|
||||
class Registration {
|
||||
// used to keep track and associate TCP/UDP/etc sessions. This is always defined by the server
|
||||
// a sessionId if '0', means we are still figuring it out.
|
||||
public int sessionID = 0;
|
||||
|
||||
public ECPublicKeyParameters publicKey;
|
||||
public IESParameters eccParameters;
|
||||
|
||||
public byte[] payload;
|
||||
|
||||
// true if we have more registrations to process, false if we are done
|
||||
public boolean hasMore = false;
|
||||
|
||||
// > 0 when we are ready to setup the connection (hasMore will always be false if this is >0). 0 when we are ready to connect
|
||||
// ALSO used if there are fragmented frames for registration data (since we have to split it up to fit inside a single UDP packet without fragmentation)
|
||||
public byte upgradeType = (byte) 0;
|
||||
|
||||
// true when we are fully upgraded
|
||||
public boolean upgraded = false;
|
||||
|
||||
private
|
||||
Registration() {
|
||||
// for serialization
|
||||
}
|
||||
|
||||
public
|
||||
Registration(final int sessionID) {
|
||||
this.sessionID = sessionID;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection.registration
|
||||
|
||||
/**
|
||||
* Internal message to handle the connection registration process
|
||||
*/
|
||||
class Registration private constructor() {
|
||||
// used to keep track and associate TCP/UDP/etc sessions. This is always defined by the server
|
||||
// a sessionId if '0', means we are still figuring it out.
|
||||
var oneTimePad = 0
|
||||
|
||||
// -1 means there is an error
|
||||
var state = INVALID
|
||||
var errorMessage: String? = null
|
||||
var publicationPort = 0
|
||||
var subscriptionPort = 0
|
||||
var sessionId = 0
|
||||
var streamId = 0
|
||||
var publicKey: ByteArray? = null
|
||||
|
||||
// by default, this will be a reliable connection. When the client connects to the server, the client will specify if the new connection
|
||||
// is a reliable/unreliable connection when setting up the MediaDriverConnection
|
||||
val isReliable = true
|
||||
|
||||
|
||||
// NOTE: this is for ECDSA!
|
||||
// var eccParameters: IESParameters? = null
|
||||
var payload: ByteArray? = null
|
||||
|
||||
// > 0 when we are ready to setup the connection (hasMore will always be false if this is >0). 0 when we are ready to connect
|
||||
// ALSO used if there are fragmented frames for registration data (since we have to split it up to fit inside a single UDP packet without fragmentation)
|
||||
var upgradeType = 0.toByte()
|
||||
|
||||
// true when we are fully upgraded
|
||||
var upgraded = false
|
||||
|
||||
companion object {
|
||||
const val INVALID = -1
|
||||
const val HELLO = 0
|
||||
const val HELLO_ACK = 1
|
||||
|
||||
fun hello(oneTimePad: Int, publicKey: ByteArray): Registration {
|
||||
val hello = Registration()
|
||||
hello.state = HELLO
|
||||
hello.oneTimePad = oneTimePad
|
||||
hello.publicKey = publicKey
|
||||
return hello
|
||||
}
|
||||
|
||||
fun helloAck(oneTimePad: Int): Registration {
|
||||
val hello = Registration()
|
||||
hello.state = HELLO_ACK
|
||||
hello.oneTimePad = oneTimePad
|
||||
return hello
|
||||
}
|
||||
|
||||
fun error(errorMessage: String?): Registration {
|
||||
val error = Registration()
|
||||
error.state = INVALID
|
||||
error.errorMessage = errorMessage
|
||||
return error
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,107 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection.registration;
|
||||
|
||||
import dorkbox.network.connection.RegistrationWrapper;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandler.Sharable;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
|
||||
@Sharable
|
||||
public abstract
|
||||
class RegistrationHandler<T extends RegistrationWrapper> extends ChannelInboundHandlerAdapter {
|
||||
protected static final String CONNECTION_HANDLER = "connection";
|
||||
|
||||
protected final T registrationWrapper;
|
||||
protected final org.slf4j.Logger logger;
|
||||
protected final String name;
|
||||
protected final EventLoopGroup workerEventLoop;
|
||||
|
||||
/**
|
||||
* @param name
|
||||
* @param registrationWrapper
|
||||
* @param workerEventLoop can be null for local JVM connections
|
||||
*/
|
||||
public
|
||||
RegistrationHandler(final String name, T registrationWrapper, final EventLoopGroup workerEventLoop) {
|
||||
this.name = name;
|
||||
this.workerEventLoop = workerEventLoop;
|
||||
this.logger = org.slf4j.LoggerFactory.getLogger(this.name);
|
||||
this.registrationWrapper = registrationWrapper;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
protected
|
||||
void initChannel(final Channel channel) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public final
|
||||
void channelRegistered(final ChannelHandlerContext context) throws Exception {
|
||||
boolean success = false;
|
||||
try {
|
||||
initChannel(context.channel());
|
||||
context.fireChannelRegistered();
|
||||
success = true;
|
||||
} catch (Throwable t) {
|
||||
this.logger.error("Failed to initialize a channel. Closing: {}", context.channel(), t);
|
||||
} finally {
|
||||
if (!success) {
|
||||
context.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void channelActive(final ChannelHandlerContext context) throws Exception {
|
||||
this.logger.error("ChannelActive NOT IMPLEMENTED!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void channelRead(final ChannelHandlerContext context, Object message) throws Exception {
|
||||
this.logger.error("MessageReceived NOT IMPLEMENTED!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void channelReadComplete(final ChannelHandlerContext context) throws Exception {
|
||||
}
|
||||
|
||||
@Override
|
||||
public abstract
|
||||
void exceptionCaught(final ChannelHandlerContext context, final Throwable cause) throws Exception;
|
||||
|
||||
/**
|
||||
* shutdown. Something messed up or was incorrect
|
||||
*/
|
||||
protected final
|
||||
void shutdown(final Channel channel, final int sessionId) {
|
||||
// properly shutdown the TCP/UDP channels.
|
||||
if (sessionId == 0 && channel.isOpen()) {
|
||||
channel.close();
|
||||
}
|
||||
|
||||
// also, once we notify, we unregister this.
|
||||
if (registrationWrapper != null) {
|
||||
registrationWrapper.closeSession(sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection.registration.local;
|
||||
|
||||
import dorkbox.network.connection.RegistrationWrapper;
|
||||
import dorkbox.network.connection.registration.MetaChannel;
|
||||
import dorkbox.network.connection.registration.RegistrationHandler;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.util.AttributeKey;
|
||||
|
||||
public abstract
|
||||
class RegistrationLocalHandler<T extends RegistrationWrapper> extends RegistrationHandler<T> {
|
||||
public static final AttributeKey<MetaChannel> META_CHANNEL = AttributeKey.valueOf(RegistrationLocalHandler.class, "MetaChannel.local");
|
||||
|
||||
RegistrationLocalHandler(String name, T registrationWrapper) {
|
||||
super(name, registrationWrapper, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void channelActive(final ChannelHandlerContext context) throws Exception {
|
||||
// to suppress warnings in the super class
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void exceptionCaught(ChannelHandlerContext context, Throwable cause) throws Exception {
|
||||
Channel channel = context.channel();
|
||||
|
||||
logger.error("Unexpected exception while trying to receive data on LOCAL channel. ({})" + System.getProperty("line.separator"),
|
||||
channel.remoteAddress(),
|
||||
cause);
|
||||
|
||||
if (channel.isOpen()) {
|
||||
channel.close();
|
||||
}
|
||||
}
|
||||
|
||||
// this SHOULDN'T ever happen, but we might shutdown in the middle of registration
|
||||
@Override
|
||||
public final
|
||||
void channelInactive(ChannelHandlerContext context) throws Exception {
|
||||
Channel channel = context.channel();
|
||||
|
||||
logger.info("Closed LOCAL connection: {}", channel.remoteAddress());
|
||||
|
||||
super.channelInactive(context);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection.registration.local;
|
||||
|
||||
import dorkbox.network.connection.ConnectionImpl;
|
||||
import dorkbox.network.connection.RegistrationWrapperClient;
|
||||
import dorkbox.network.connection.registration.MetaChannel;
|
||||
import dorkbox.network.connection.registration.Registration;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
|
||||
public
|
||||
class RegistrationLocalHandlerClient extends RegistrationLocalHandler<RegistrationWrapperClient> {
|
||||
|
||||
public
|
||||
RegistrationLocalHandlerClient(String name, RegistrationWrapperClient registrationWrapper) {
|
||||
super(name, registrationWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* STEP 1: Channel is first created
|
||||
*/
|
||||
@Override
|
||||
protected
|
||||
void initChannel(Channel channel) {
|
||||
MetaChannel metaChannel = registrationWrapper.createSession(0);
|
||||
metaChannel.localChannel = channel;
|
||||
|
||||
channel.attr(META_CHANNEL)
|
||||
.set(metaChannel);
|
||||
|
||||
logger.trace("New LOCAL connection.");
|
||||
}
|
||||
|
||||
/**
|
||||
* STEP 2: Channel is now active. Start the registration process
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
void channelActive(ChannelHandlerContext context) throws Exception {
|
||||
super.channelActive(context);
|
||||
|
||||
Channel channel = context.channel();
|
||||
logger.info("Connected to LOCAL connection. [{} ==> {}]",
|
||||
context.channel()
|
||||
.localAddress(),
|
||||
channel.remoteAddress());
|
||||
|
||||
// client starts the registration process
|
||||
Registration registration = new Registration(0);
|
||||
|
||||
// ALSO make sure to verify registration details
|
||||
|
||||
// we don't verify anything on the CLIENT. We only verify on the server.
|
||||
// we don't support registering NEW classes after the client starts.
|
||||
if (!registrationWrapper.initClassRegistration(channel, registration)) {
|
||||
// abort if something messed up!
|
||||
shutdown(channel, registration.sessionID);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void channelRead(ChannelHandlerContext context, Object message) throws Exception {
|
||||
// the "server" bounces back the registration message when it's valid.
|
||||
ReferenceCountUtil.release(message);
|
||||
|
||||
Channel channel = context.channel();
|
||||
MetaChannel metaChannel = channel.attr(META_CHANNEL)
|
||||
.getAndSet(null);
|
||||
|
||||
// have to setup new listeners
|
||||
if (metaChannel != null) {
|
||||
ChannelPipeline pipeline = channel.pipeline();
|
||||
pipeline.remove(this);
|
||||
|
||||
// Event though a local channel is XOR with everything else, we still have to make the client clean up it's state.
|
||||
registrationWrapper.startNextProtocolRegistration();
|
||||
|
||||
ConnectionImpl connection = registrationWrapper.connection0(metaChannel, null);
|
||||
|
||||
// have to setup connection handler
|
||||
pipeline.addLast(CONNECTION_HANDLER, connection);
|
||||
registrationWrapper.connectionConnected0(connection);
|
||||
}
|
||||
else {
|
||||
// this should NEVER happen!
|
||||
logger.error("Error registering LOCAL channel! MetaChannel is null!");
|
||||
shutdown(channel, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection.registration.local;
|
||||
|
||||
import dorkbox.network.connection.ConnectionImpl;
|
||||
import dorkbox.network.connection.RegistrationWrapper.STATE;
|
||||
import dorkbox.network.connection.RegistrationWrapperServer;
|
||||
import dorkbox.network.connection.registration.MetaChannel;
|
||||
import dorkbox.network.connection.registration.Registration;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
|
||||
public
|
||||
class RegistrationLocalHandlerServer extends RegistrationLocalHandler<RegistrationWrapperServer> {
|
||||
|
||||
|
||||
public
|
||||
RegistrationLocalHandlerServer(String name, RegistrationWrapperServer registrationWrapper) {
|
||||
super(name, registrationWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* STEP 1: Channel is first created
|
||||
*/
|
||||
@Override
|
||||
protected
|
||||
void initChannel(Channel channel) {
|
||||
MetaChannel metaChannel = registrationWrapper.createSession();
|
||||
metaChannel.localChannel = channel;
|
||||
|
||||
channel.attr(META_CHANNEL)
|
||||
.set(metaChannel);
|
||||
|
||||
logger.trace("New LOCAL connection.");
|
||||
}
|
||||
|
||||
/**
|
||||
* STEP 2: Channel is now active. Start the registration process (Client starts the process)
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
void channelActive(ChannelHandlerContext context) throws Exception {
|
||||
Channel channel = context.channel();
|
||||
logger.info("Connected to LOCAL connection. [{} <== {}]",
|
||||
context.channel()
|
||||
.localAddress(),
|
||||
channel.remoteAddress());
|
||||
|
||||
|
||||
super.channelActive(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void channelRead(ChannelHandlerContext context, Object message) throws Exception {
|
||||
Channel channel = context.channel();
|
||||
ChannelPipeline pipeline = channel.pipeline();
|
||||
|
||||
if (!(message instanceof Registration)) {
|
||||
logger.error("Expected registration message was [{}] instead!", message.getClass());
|
||||
shutdown(channel, 0);
|
||||
ReferenceCountUtil.release(message);
|
||||
return;
|
||||
}
|
||||
|
||||
MetaChannel metaChannel = channel.attr(META_CHANNEL).get();
|
||||
|
||||
|
||||
if (metaChannel == null) {
|
||||
logger.error("Server MetaChannel was null. It shouldn't be.");
|
||||
shutdown(channel, 0);
|
||||
ReferenceCountUtil.release(message);
|
||||
return;
|
||||
}
|
||||
|
||||
Registration registration = (Registration) message;
|
||||
|
||||
// verify the class ID registration details.
|
||||
// the client will send their class registration data. VERIFY IT IS CORRECT!
|
||||
STATE state = registrationWrapper.verifyClassRegistration(metaChannel, registration);
|
||||
if (state == STATE.ERROR) {
|
||||
// abort! There was an error
|
||||
shutdown(channel, 0);
|
||||
return;
|
||||
}
|
||||
else if (state == STATE.WAIT) {
|
||||
return;
|
||||
}
|
||||
// else, continue.
|
||||
|
||||
|
||||
|
||||
// have to remove the pipeline FIRST, since if we don't, and we expect to receive a message --- when we REMOVE "this" from the pipeline,
|
||||
// we will ALSO REMOVE all it's messages, which we want to receive!
|
||||
pipeline.remove(this);
|
||||
|
||||
registration.payload = null;
|
||||
|
||||
// we no longer need the meta channel, so remove it
|
||||
channel.attr(META_CHANNEL).set(null);
|
||||
channel.writeAndFlush(registration);
|
||||
|
||||
ReferenceCountUtil.release(registration);
|
||||
logger.trace("Sent registration");
|
||||
|
||||
ConnectionImpl connection = registrationWrapper.connection0(metaChannel, null);
|
||||
|
||||
if (connection != null) {
|
||||
// have to setup connection handler
|
||||
pipeline.addLast(CONNECTION_HANDLER, connection);
|
||||
registrationWrapper.connectionConnected0(connection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,14 +23,10 @@ import java.util.concurrent.TimeUnit;
|
|||
import org.bouncycastle.jce.ECNamedCurveTable;
|
||||
import org.bouncycastle.jce.spec.ECParameterSpec;
|
||||
|
||||
import dorkbox.network.connection.ConnectionImpl;
|
||||
import dorkbox.network.connection.EndPoint;
|
||||
import dorkbox.network.connection.RegistrationWrapper;
|
||||
import dorkbox.network.connection.registration.MetaChannel;
|
||||
import dorkbox.network.connection.registration.Registration;
|
||||
import dorkbox.network.connection.registration.RegistrationHandler;
|
||||
import dorkbox.network.connection.registration.UpgradeType;
|
||||
import dorkbox.network.pipeline.tcp.KryoDecoderTcp;
|
||||
import dorkbox.network.pipeline.tcp.KryoDecoderTcpCompression;
|
||||
import dorkbox.network.pipeline.tcp.KryoDecoderTcpCrypto;
|
||||
import dorkbox.network.pipeline.tcp.KryoDecoderTcpNone;
|
||||
|
@ -86,7 +82,7 @@ class RegistrationRemoteHandler<T extends RegistrationWrapper> extends Registrat
|
|||
|
||||
|
||||
RegistrationRemoteHandler(final String name, final T registrationWrapper, final EventLoopGroup workerEventLoop) {
|
||||
super(name, registrationWrapper, workerEventLoop);
|
||||
super(name, workerEventLoop);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -100,34 +96,34 @@ class RegistrationRemoteHandler<T extends RegistrationWrapper> extends Registrat
|
|||
Class<? extends Channel> channelClass = channel.getClass();
|
||||
// because of the way TCP works, we have to have special readers/writers. For UDP, all data must be in a single packet.
|
||||
|
||||
boolean isTcpChannel = ConnectionImpl.isTcpChannel(channelClass);
|
||||
boolean isUdpChannel = !isTcpChannel && ConnectionImpl.isUdpChannel(channelClass);
|
||||
|
||||
if (isTcpChannel) {
|
||||
///////////////////////
|
||||
// DECODE (or upstream)
|
||||
///////////////////////
|
||||
pipeline.addFirst(TCP_DECODE, new KryoDecoderTcp(registrationWrapper.getSerialization())); // cannot be shared because of possible fragmentation.
|
||||
}
|
||||
else if (isUdpChannel) {
|
||||
// can be shared because there cannot be fragmentation for our UDP packets. If there is, we throw an error and continue...
|
||||
pipeline.addFirst(UDP_DECODE, this.registrationWrapper.kryoUdpDecoder);
|
||||
}
|
||||
|
||||
// this makes the proper event get raised in the registrationHandler to kill NEW idle connections. Once "connected" they last a lot longer.
|
||||
// we ALWAYS have this initial IDLE handler, so we don't have to worry about a SLOW-LORIS ATTACK against the server.
|
||||
// in Seconds -- not shared, because it is per-connection
|
||||
pipeline.addFirst(IDLE_HANDLER, new IdleStateHandler(2, 0, 0));
|
||||
|
||||
if (isTcpChannel) {
|
||||
/////////////////////////
|
||||
// ENCODE (or downstream)
|
||||
/////////////////////////
|
||||
pipeline.addFirst(TCP_ENCODE, this.registrationWrapper.kryoTcpEncoder); // this is shared
|
||||
}
|
||||
else if (isUdpChannel) {
|
||||
pipeline.addFirst(UDP_ENCODE, this.registrationWrapper.kryoUdpEncoder);
|
||||
}
|
||||
// boolean isTcpChannel = ConnectionImpl.isTcpChannel(channelClass);
|
||||
// boolean isUdpChannel = !isTcpChannel && ConnectionImpl.isUdpChannel(channelClass);
|
||||
//
|
||||
// if (isTcpChannel) {
|
||||
// ///////////////////////
|
||||
// // DECODE (or upstream)
|
||||
// ///////////////////////
|
||||
// pipeline.addFirst(TCP_DECODE, new KryoDecoderTcp(registrationWrapper.getSerialization())); // cannot be shared because of possible fragmentation.
|
||||
// }
|
||||
// else if (isUdpChannel) {
|
||||
// // can be shared because there cannot be fragmentation for our UDP packets. If there is, we throw an error and continue...
|
||||
// // pipeline.addFirst(UDP_DECODE, this.registrationWrapper.kryoUdpDecoder);
|
||||
// }
|
||||
//
|
||||
// // this makes the proper event get raised in the registrationHandler to kill NEW idle connections. Once "connected" they last a lot longer.
|
||||
// // we ALWAYS have this initial IDLE handler, so we don't have to worry about a SLOW-LORIS ATTACK against the server.
|
||||
// // in Seconds -- not shared, because it is per-connection
|
||||
// pipeline.addFirst(IDLE_HANDLER, new IdleStateHandler(2, 0, 0));
|
||||
//
|
||||
// if (isTcpChannel) {
|
||||
// /////////////////////////
|
||||
// // ENCODE (or downstream)
|
||||
// /////////////////////////
|
||||
// // pipeline.addFirst(TCP_ENCODE, this.registrationWrapper.kryoTcpEncoder); // this is shared
|
||||
// }
|
||||
// else if (isUdpChannel) {
|
||||
// // pipeline.addFirst(UDP_ENCODE, this.registrationWrapper.kryoUdpEncoder);
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -139,36 +135,36 @@ class RegistrationRemoteHandler<T extends RegistrationWrapper> extends Registrat
|
|||
// add the channel so we can access it later.
|
||||
// do NOT want to add UDP channels, since they are tracked differently.
|
||||
|
||||
if (this.logger.isDebugEnabled()) {
|
||||
Channel channel = context.channel();
|
||||
Class<? extends Channel> channelClass = channel.getClass();
|
||||
boolean isUdp = ConnectionImpl.isUdpChannel(channelClass);
|
||||
|
||||
StringBuilder stringBuilder = new StringBuilder(96);
|
||||
|
||||
stringBuilder.append("Connected to remote ");
|
||||
if (ConnectionImpl.isTcpChannel(channelClass)) {
|
||||
stringBuilder.append("TCP");
|
||||
}
|
||||
else if (isUdp) {
|
||||
stringBuilder.append("UDP");
|
||||
}
|
||||
else if (ConnectionImpl.isLocalChannel(channelClass)) {
|
||||
stringBuilder.append("LOCAL");
|
||||
}
|
||||
else {
|
||||
stringBuilder.append("UNKNOWN");
|
||||
}
|
||||
|
||||
stringBuilder.append(" connection [");
|
||||
EndPoint.getHostDetails(stringBuilder, channel.localAddress());
|
||||
|
||||
stringBuilder.append(getConnectionDirection());
|
||||
EndPoint.getHostDetails(stringBuilder, channel.remoteAddress());
|
||||
stringBuilder.append("]");
|
||||
|
||||
this.logger.debug(stringBuilder.toString());
|
||||
}
|
||||
// if (this.logger.isDebugEnabled()) {
|
||||
// Channel channel = context.channel();
|
||||
// Class<? extends Channel> channelClass = channel.getClass();
|
||||
// boolean isUdp = ConnectionImpl.isUdpChannel(channelClass);
|
||||
//
|
||||
// StringBuilder stringBuilder = new StringBuilder(96);
|
||||
//
|
||||
// stringBuilder.append("Connected to remote ");
|
||||
// if (ConnectionImpl.isTcpChannel(channelClass)) {
|
||||
// stringBuilder.append("TCP");
|
||||
// }
|
||||
// else if (isUdp) {
|
||||
// stringBuilder.append("UDP");
|
||||
// }
|
||||
// else if (ConnectionImpl.isLocalChannel(channelClass)) {
|
||||
// stringBuilder.append("LOCAL");
|
||||
// }
|
||||
// else {
|
||||
// stringBuilder.append("UNKNOWN");
|
||||
// }
|
||||
//
|
||||
// stringBuilder.append(" connection [");
|
||||
// EndPoint.Companion.getHostDetails(stringBuilder, channel.localAddress());
|
||||
//
|
||||
// stringBuilder.append(getConnectionDirection());
|
||||
// EndPoint.Companion.getHostDetails(stringBuilder, channel.remoteAddress());
|
||||
// stringBuilder.append("]");
|
||||
//
|
||||
// this.logger.debug(stringBuilder.toString());
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -238,15 +234,15 @@ class RegistrationRemoteHandler<T extends RegistrationWrapper> extends Registrat
|
|||
// shared decoder
|
||||
switch (upgradeType) {
|
||||
case (UpgradeType.NONE) :
|
||||
pipeline.replace(UDP_DECODE, UDP_DECODE_NONE, this.registrationWrapper.kryoUdpDecoderNone);
|
||||
// pipeline.replace(UDP_DECODE, UDP_DECODE_NONE, this.registrationWrapper.kryoUdpDecoderNone);
|
||||
break;
|
||||
|
||||
case (UpgradeType.COMPRESS) :
|
||||
pipeline.replace(UDP_DECODE, UDP_DECODE_COMPRESS, this.registrationWrapper.kryoUdpDecoderCompression);
|
||||
// pipeline.replace(UDP_DECODE, UDP_DECODE_COMPRESS, this.registrationWrapper.kryoUdpDecoderCompression);
|
||||
break;
|
||||
|
||||
case (UpgradeType.ENCRYPT) :
|
||||
pipeline.replace(UDP_DECODE, UDP_DECODE_CRYPTO, this.registrationWrapper.kryoUdpDecoderCrypto);
|
||||
// pipeline.replace(UDP_DECODE, UDP_DECODE_CRYPTO, this.registrationWrapper.kryoUdpDecoderCrypto);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unable to upgrade UDP connection pipeline for type: " + upgradeType);
|
||||
|
@ -268,15 +264,15 @@ class RegistrationRemoteHandler<T extends RegistrationWrapper> extends Registrat
|
|||
// shared encoder
|
||||
switch (upgradeType) {
|
||||
case (UpgradeType.NONE) :
|
||||
pipeline.replace(TCP_ENCODE, TCP_ENCODE_NONE, this.registrationWrapper.kryoTcpEncoderNone);
|
||||
// pipeline.replace(TCP_ENCODE, TCP_ENCODE_NONE, this.registrationWrapper.kryoTcpEncoderNone);
|
||||
break;
|
||||
|
||||
case (UpgradeType.COMPRESS) :
|
||||
pipeline.replace(TCP_ENCODE, TCP_ENCODE_COMPRESS, this.registrationWrapper.kryoTcpEncoderCompression);
|
||||
// pipeline.replace(TCP_ENCODE, TCP_ENCODE_COMPRESS, this.registrationWrapper.kryoTcpEncoderCompression);
|
||||
break;
|
||||
|
||||
case (UpgradeType.ENCRYPT) :
|
||||
pipeline.replace(TCP_ENCODE, TCP_ENCODE_CRYPTO, this.registrationWrapper.kryoTcpEncoderCrypto);
|
||||
// pipeline.replace(TCP_ENCODE, TCP_ENCODE_CRYPTO, this.registrationWrapper.kryoTcpEncoderCrypto);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unable to upgrade TCP connection pipeline for type: " + upgradeType);
|
||||
|
@ -286,15 +282,15 @@ class RegistrationRemoteHandler<T extends RegistrationWrapper> extends Registrat
|
|||
// shared encoder
|
||||
switch (upgradeType) {
|
||||
case (UpgradeType.NONE) :
|
||||
pipeline.replace(UDP_ENCODE, UDP_ENCODE_NONE, this.registrationWrapper.kryoUdpEncoderNone);
|
||||
// pipeline.replace(UDP_ENCODE, UDP_ENCODE_NONE, this.registrationWrapper.kryoUdpEncoderNone);
|
||||
break;
|
||||
|
||||
case (UpgradeType.COMPRESS) :
|
||||
pipeline.replace(UDP_ENCODE, UDP_ENCODE_COMPRESS, this.registrationWrapper.kryoUdpEncoderCompression);
|
||||
// pipeline.replace(UDP_ENCODE, UDP_ENCODE_COMPRESS, this.registrationWrapper.kryoUdpEncoderCompression);
|
||||
break;
|
||||
|
||||
case (UpgradeType.ENCRYPT) :
|
||||
pipeline.replace(UDP_ENCODE, UDP_ENCODE_CRYPTO, this.registrationWrapper.kryoUdpEncoderCrypto);
|
||||
// pipeline.replace(UDP_ENCODE, UDP_ENCODE_CRYPTO, this.registrationWrapper.kryoUdpEncoderCrypto);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unable to upgrade UDP connection pipeline for type: " + upgradeType);
|
||||
|
@ -356,10 +352,10 @@ class RegistrationRemoteHandler<T extends RegistrationWrapper> extends Registrat
|
|||
stringBuilder.append(channelType);
|
||||
stringBuilder.append(" connection [");
|
||||
|
||||
EndPoint.getHostDetails(stringBuilder, channel.localAddress());
|
||||
EndPoint.Companion.getHostDetails(stringBuilder, channel.localAddress());
|
||||
|
||||
stringBuilder.append(getConnectionDirection());
|
||||
EndPoint.getHostDetails(stringBuilder, channel.remoteAddress());
|
||||
EndPoint.Companion.getHostDetails(stringBuilder, channel.remoteAddress());
|
||||
|
||||
stringBuilder.append("]");
|
||||
|
||||
|
|
|
@ -222,10 +222,10 @@ class RegistrationRemoteHandlerClient extends RegistrationRemoteHandler<Registra
|
|||
metaChannel.connection = this.registrationWrapper.connection0(metaChannel, remoteAddress);
|
||||
|
||||
if (metaChannel.tcpChannel != null) {
|
||||
metaChannel.tcpChannel.pipeline().addLast(CONNECTION_HANDLER, metaChannel.connection);
|
||||
// metaChannel.tcpChannel.pipeline().addLast(CONNECTION_HANDLER, metaChannel.connection);
|
||||
}
|
||||
if (metaChannel.udpChannel != null) {
|
||||
metaChannel.udpChannel.pipeline().addLast(CONNECTION_HANDLER, metaChannel.connection);
|
||||
// metaChannel.udpChannel.pipeline().addLast(CONNECTION_HANDLER, metaChannel.connection);
|
||||
}
|
||||
|
||||
|
||||
|
@ -300,7 +300,7 @@ class RegistrationRemoteHandlerClient extends RegistrationRemoteHandler<Registra
|
|||
for (Object message : messages) {
|
||||
logger.trace(" deferred onMessage({}, {})", connection.id(), message);
|
||||
try {
|
||||
connection.channelRead(null, message);
|
||||
// connection.channelRead(null, message);
|
||||
} catch (Exception e) {
|
||||
logger.error("Error running deferred messages!", e);
|
||||
}
|
||||
|
|
|
@ -71,7 +71,7 @@ class RegistrationRemoteHandlerServer extends RegistrationRemoteHandler<Registra
|
|||
final InetSocketAddress remoteAddress = (InetSocketAddress) channel.remoteAddress();
|
||||
if (!registrationWrapper.acceptRemoteConnection(remoteAddress)) {
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
EndPoint.getHostDetails(stringBuilder, remoteAddress);
|
||||
EndPoint.Companion.getHostDetails(stringBuilder, remoteAddress);
|
||||
|
||||
logger.error("Remote connection [{}] is not permitted! Aborting connection process.", stringBuilder.toString());
|
||||
shutdown(channel, 0);
|
||||
|
@ -210,10 +210,10 @@ class RegistrationRemoteHandlerServer extends RegistrationRemoteHandler<Registra
|
|||
metaChannel.connection = this.registrationWrapper.connection0(metaChannel, remoteAddress);
|
||||
|
||||
if (metaChannel.tcpChannel != null) {
|
||||
metaChannel.tcpChannel.pipeline().addLast(CONNECTION_HANDLER, metaChannel.connection);
|
||||
// metaChannel.tcpChannel.pipeline().addLast(CONNECTION_HANDLER, metaChannel.connection);
|
||||
}
|
||||
if (metaChannel.udpChannel != null) {
|
||||
metaChannel.udpChannel.pipeline().addLast(CONNECTION_HANDLER, metaChannel.connection);
|
||||
// metaChannel.udpChannel.pipeline().addLast(CONNECTION_HANDLER, metaChannel.connection);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -74,7 +74,7 @@ class RegistrationRemoteHandlerServerTCP extends RegistrationRemoteHandlerServer
|
|||
// this is what happens when the registration happens too quickly...
|
||||
Object connection = context.pipeline().last();
|
||||
if (connection instanceof ConnectionImpl) {
|
||||
((ConnectionImpl) connection).channelRead(context, message);
|
||||
// ((ConnectionImpl) connection).channelRead(context, message);
|
||||
}
|
||||
else {
|
||||
shutdown(channel, 0);
|
||||
|
|
|
@ -79,7 +79,7 @@ class RegistrationRemoteHandlerServerUDP extends RegistrationRemoteHandlerServer
|
|||
// this is what happens when the registration happens too quickly...
|
||||
Object connection = context.pipeline().last();
|
||||
if (connection instanceof ConnectionImpl) {
|
||||
((ConnectionImpl) connection).channelRead(context, message);
|
||||
// ((ConnectionImpl) connection).channelRead(context, message);
|
||||
}
|
||||
else {
|
||||
shutdown(channel, 0);
|
||||
|
|
|
@ -1,151 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection.wrapper;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import dorkbox.network.connection.ConnectionImpl;
|
||||
import dorkbox.network.connection.ConnectionPoint;
|
||||
import dorkbox.network.connection.ISessionManager;
|
||||
import dorkbox.network.connection.registration.MetaChannel;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.local.LocalAddress;
|
||||
import io.netty.util.concurrent.Promise;
|
||||
|
||||
public
|
||||
class ChannelLocalWrapper implements ChannelWrapper, ConnectionPoint {
|
||||
private static final SecretKey dummyCryptoKey = new SecretKeySpec(new byte[32], "AES");
|
||||
|
||||
private final Channel channel;
|
||||
|
||||
private final AtomicBoolean shouldFlush = new AtomicBoolean(false);
|
||||
private String remoteAddress;
|
||||
|
||||
|
||||
public
|
||||
ChannelLocalWrapper(MetaChannel metaChannel) {
|
||||
this.channel = metaChannel.localChannel;
|
||||
this.remoteAddress = ((LocalAddress) this.channel.remoteAddress()).id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Write an object to the underlying channel
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
void write(Object object) {
|
||||
this.channel.write(object);
|
||||
this.shouldFlush.set(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the channel is writable. Useful when sending large amounts of data at once.
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
boolean isWritable() {
|
||||
// it's immediate, since it's in the same JVM.
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ConnectionPoint tcp() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ConnectionPoint udp() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
<V> Promise<V> newPromise() {
|
||||
return channel.eventLoop()
|
||||
.newPromise();
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
SecretKey cryptoKey() {
|
||||
return dummyCryptoKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
boolean isLoopback() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final
|
||||
String getRemoteHost() {
|
||||
return this.remoteAddress;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void close(ConnectionImpl connection, ISessionManager sessionManager, boolean hintedClose) {
|
||||
// long maxShutdownWaitTimeInMilliSeconds = EndPoint.maxShutdownWaitTimeInMilliSeconds;
|
||||
//
|
||||
// this.shouldFlush.set(false);
|
||||
//
|
||||
// // Wait until the connection is closed or the connection attempt fails.
|
||||
// this.channel.close()
|
||||
// .awaitUninterruptibly(maxShutdownWaitTimeInMilliSeconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
int id() {
|
||||
return this.channel.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
int hashCode() {
|
||||
return this.channel.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
ChannelLocalWrapper other = (ChannelLocalWrapper) obj;
|
||||
if (this.remoteAddress == null) {
|
||||
if (other.remoteAddress != null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (!this.remoteAddress.equals(other.remoteAddress)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection.wrapper;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import dorkbox.network.connection.ConnectionPoint;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelPromise;
|
||||
import io.netty.util.concurrent.Promise;
|
||||
|
||||
public
|
||||
class ChannelNetwork implements ConnectionPoint {
|
||||
|
||||
final Channel channel;
|
||||
final AtomicBoolean shouldFlush = new AtomicBoolean(false);
|
||||
private final ChannelPromise voidPromise;
|
||||
|
||||
public
|
||||
ChannelNetwork(Channel channel) {
|
||||
this.channel = channel;
|
||||
this.voidPromise = channel.voidPromise();
|
||||
}
|
||||
|
||||
/**
|
||||
* Write an object to the underlying channel. If the underlying channel is NOT writable, this will block unit it is writable
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
void write(Object object) throws Exception {
|
||||
shouldFlush.set(true);
|
||||
// we don't care, or want to save the future. This is so GC is less.
|
||||
channel.write(object, voidPromise);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the channel is writable. Useful when sending large amounts of data at once.
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
boolean isWritable() {
|
||||
return channel.isWritable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
<V> Promise<V> newPromise() {
|
||||
return channel.eventLoop().newPromise();
|
||||
}
|
||||
|
||||
void close(final int delay, final long maxShutdownWaitTimeInMilliSeconds) {
|
||||
shouldFlush.set(false);
|
||||
if (channel.isActive()) {
|
||||
if (delay > 0) {
|
||||
// for UDP, we send a hint to the other connection that we should close. While not always 100% successful, this helps
|
||||
// clean up connections on the remote end. So we want to wait a short amount of time for this to be successful
|
||||
channel.eventLoop()
|
||||
.schedule(new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
channel.close()
|
||||
.awaitUninterruptibly(maxShutdownWaitTimeInMilliSeconds);
|
||||
}
|
||||
}, delay, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
else {
|
||||
channel.close()
|
||||
.awaitUninterruptibly(maxShutdownWaitTimeInMilliSeconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,183 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection.wrapper;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import dorkbox.network.connection.ConnectionImpl;
|
||||
import dorkbox.network.connection.ConnectionPoint;
|
||||
import dorkbox.network.connection.ISessionManager;
|
||||
import dorkbox.network.connection.registration.MetaChannel;
|
||||
import io.netty.util.NetUtil;
|
||||
|
||||
public
|
||||
class ChannelNetworkWrapper implements ChannelWrapper {
|
||||
|
||||
private final int sessionId;
|
||||
|
||||
private final ChannelNetwork tcp;
|
||||
private final ChannelNetwork udp;
|
||||
|
||||
// did the remote connection public ECC key change?
|
||||
private final boolean remotePublicKeyChanged;
|
||||
|
||||
private final String remoteAddress;
|
||||
private final boolean isLoopback;
|
||||
|
||||
private final SecretKey secretKey;
|
||||
|
||||
public
|
||||
ChannelNetworkWrapper(final MetaChannel metaChannel, final InetSocketAddress remoteAddress) {
|
||||
|
||||
this.sessionId = metaChannel.sessionId;
|
||||
this.isLoopback = remoteAddress.getAddress().equals(NetUtil.LOCALHOST);
|
||||
|
||||
if (metaChannel.tcpChannel != null) {
|
||||
this.tcp = new ChannelNetwork(metaChannel.tcpChannel);
|
||||
} else {
|
||||
this.tcp = null;
|
||||
}
|
||||
|
||||
if (metaChannel.udpChannel != null) {
|
||||
this.udp = new ChannelNetwork(metaChannel.udpChannel);
|
||||
}
|
||||
else {
|
||||
this.udp = null;
|
||||
}
|
||||
|
||||
|
||||
this.remoteAddress = remoteAddress.getAddress().getHostAddress();
|
||||
this.remotePublicKeyChanged = metaChannel.changedRemoteKey;
|
||||
|
||||
// AES key (only for networked connections)
|
||||
secretKey = metaChannel.secretKey;
|
||||
}
|
||||
|
||||
public final
|
||||
boolean remoteKeyChanged() {
|
||||
return this.remotePublicKeyChanged;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ConnectionPoint tcp() {
|
||||
return this.tcp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ConnectionPoint udp() {
|
||||
return this.udp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the AES key.
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
SecretKey cryptoKey() {
|
||||
return this.secretKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
boolean isLoopback() {
|
||||
return isLoopback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
String getRemoteHost() {
|
||||
return this.remoteAddress;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void close(final ConnectionImpl connection, final ISessionManager sessionManager, boolean hintedClose) {
|
||||
// long maxShutdownWaitTimeInMilliSeconds = EndPoint.maxShutdownWaitTimeInMilliSeconds;
|
||||
//
|
||||
// if (this.tcp != null) {
|
||||
// this.tcp.close(0, maxShutdownWaitTimeInMilliSeconds);
|
||||
// }
|
||||
//
|
||||
// if (this.udp != null) {
|
||||
// if (hintedClose) {
|
||||
// // we already hinted that we should close this channel... don't do it again!
|
||||
// this.udp.close(0, maxShutdownWaitTimeInMilliSeconds);
|
||||
// }
|
||||
// else {
|
||||
// // send a hint to the other connection that we should close. While not always 100% successful, this helps clean up connections
|
||||
// // on the remote end
|
||||
// try {
|
||||
// this.udp.write(new DatagramCloseMessage());
|
||||
// this.udp.flush();
|
||||
// } catch (Exception e) {
|
||||
// e.printStackTrace();
|
||||
// }
|
||||
// this.udp.close(200, maxShutdownWaitTimeInMilliSeconds);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // we need to yield the thread here, so that the socket has a chance to close
|
||||
// Thread.yield();
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
int id() {
|
||||
return this.sessionId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
int hashCode() {
|
||||
// a unique ID for every connection. However, these ID's can also be reused
|
||||
return this.sessionId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (!super.equals(obj)) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ChannelNetworkWrapper other = (ChannelNetworkWrapper) obj;
|
||||
if (this.remoteAddress == null) {
|
||||
if (other.remoteAddress != null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (!this.remoteAddress.equals(other.remoteAddress)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
String toString() {
|
||||
return "NetworkConnection [" + getRemoteHost() + "]";
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.connection.wrapper;
|
||||
|
||||
import dorkbox.network.connection.ConnectionPoint;
|
||||
import io.netty.util.concurrent.ImmediateEventExecutor;
|
||||
import io.netty.util.concurrent.Promise;
|
||||
|
||||
public
|
||||
class ChannelNull implements ConnectionPoint {
|
||||
|
||||
private static final ConnectionPoint INSTANCE = new ChannelNull();
|
||||
|
||||
public static
|
||||
ConnectionPoint get() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private
|
||||
ChannelNull() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void write(Object object) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the channel is writable. Useful when sending large amounts of data at once.
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
boolean isWritable() {
|
||||
// this channel is ALWAYS writable! (it just does nothing...)
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
<V> Promise<V> newPromise() {
|
||||
return ImmediateEventExecutor.INSTANCE.newPromise();
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue