WIP changing the network lib from netty -> AERON

This commit is contained in:
nathan 2020-07-03 01:45:18 +02:00
parent 4c8c50e8a3
commit fc7baa6c8d
229 changed files with 16065 additions and 15933 deletions

View File

@ -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
}
}

View File

@ -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");
}
}

View File

@ -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()
// }
}

View File

@ -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();
}
}

View File

@ -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();
}
}
}

View File

@ -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() {
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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
// }
}

View File

@ -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();
}
}

View File

@ -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 &gt; 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 +
'}'
}
}

View File

@ -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 &gt; 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 ""
}
}

View File

@ -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 +
'}'
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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));
}
}

View File

@ -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");
}
}
}

View File

@ -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);
}
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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"));
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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"));
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,6 @@
package dorkbox.network.aeron.server
/**
* A session/stream could not be allocated.
*/
class AllocationException(message: String) : ServerException(message)

View File

@ -0,0 +1,6 @@
package dorkbox.network.aeron.server
/**
* A port could not be allocated.
*/
class PortAllocationException(message: String) : ServerException(message)

View File

@ -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;
}
}

View File

@ -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--
}
}
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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 + '}';
}
}

View File

@ -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)
}
}

View File

@ -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>)
}

View File

@ -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);
}
}

View File

@ -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)
}
}

View File

@ -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;
}
}

View File

@ -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)
// }
}
}

View File

@ -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!!
}
}

View File

@ -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
}
}

View File

@ -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();
}

View File

@ -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<*>
}

View File

@ -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();
}
}
}

View File

@ -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()
}
}
}

View File

@ -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();
// }
}

View File

@ -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();
}
}

View File

@ -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();
}

View File

@ -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]"
}
}

View File

@ -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;
}
}

View File

@ -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)
// }
}

View File

@ -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();
}
}

View File

@ -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<*>
}

View File

@ -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);
}

View File

@ -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>
}

View File

@ -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
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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)"
}
}

View File

@ -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();
}

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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);
}
}
}

View File

@ -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
}
}
}

View File

@ -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();
}

View File

@ -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
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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("]");

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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);
}
}
}
}

View File

@ -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() + "]";
}
}

View File

@ -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