before kotlin conversion

This commit is contained in:
nathan 2020-06-04 23:24:24 +02:00
parent 8a2cac9ed3
commit 794965348a
103 changed files with 4473 additions and 3014 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2018 dorkbox, llc
* Copyright 2020 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -14,34 +14,23 @@
* limitations under the License.
*/
import Build_gradle.Extras.bcVersion
import java.time.Instant
import kotlin.collections.set
///////////////////////////////
////// PUBLISH TO SONATYPE / MAVEN CENTRAL
//////
////// TESTING (local maven repo) -> PUBLISHING -> publishToMavenLocal
//////
////// RELEASE (sonatype / maven central) -> "PUBLISH AND RELEASE" -> publishAndRelease
////// TESTING : (to local maven repo) <'publish and release' - 'publishToMavenLocal'>
////// RELEASE : (to sonatype/maven central), <'publish and release' - 'publishToSonatypeAndRelease'>
///////////////////////////////
println("\tGradle ${project.gradle.gradleVersion} on Java ${JavaVersion.current()}")
plugins {
java
signing
`maven-publish`
// publish on sonatype
id("de.marcphilipp.nexus-publish") version "0.4.0"
// close and release on sonatype
id("io.codearte.nexus-staging") version "0.21.2"
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.GradleUtils") version "1.4"
id("com.dorkbox.GradlePublish") version "1.1"
id("com.dorkbox.GradleModuleInfo") version "1.0"
id("com.dorkbox.GradleUtils") version "1.6"
kotlin("jvm") version "1.3.72"
}
@ -56,6 +45,7 @@ object Extras {
const val name = "Network"
const val id = "Network"
const val vendor = "Dorkbox LLC"
const val vendorUrl = "https://dorkbox.com"
const val url = "https://git.dorkbox.com/dorkbox/Network"
val buildDate = Instant.now().toString()
@ -77,6 +67,38 @@ description = Extras.description
group = Extras.group
version = Extras.version
// NOTE: now using aeron instead of netty
// using netty IP filters for connections
// /*
// * Copyright 2014 The Netty Project
// *
// * The Netty Project licenses this file to you 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.ipFilter;
// also, NOT using bouncastle, but instead the google one
// better SSL library
// implementation("org.conscrypt:conscrypt-openjdk-uber:2.2.1")
// init {
// try {
// Security.insertProviderAt(Conscrypt.newProvider(), 1);
// }
// catch (e: Throwable) {
// e.printStackTrace();
// }
// }
licensing {
license(License.APACHE_2) {
@ -263,10 +285,10 @@ dependencies {
implementation("com.esotericsoftware:kryo:5.0.0-RC2")
implementation("net.jpountz.lz4:lz4:1.3.0")
implementation("org.bouncycastle:bcprov-jdk15on:$bcVersion")
implementation("org.bouncycastle:bcpg-jdk15on:$bcVersion")
implementation("org.bouncycastle:bcmail-jdk15on:$bcVersion")
implementation("org.bouncycastle:bctls-jdk15on:$bcVersion")
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")
@ -276,120 +298,43 @@ dependencies {
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")
}
///////////////////////////////
////// PUBLISH TO SONATYPE / MAVEN CENTRAL
//////
////// TESTING (local maven repo) -> "PUBLISHING" -> publishToMavenLocal
//////
////// RELEASE (sonatype / maven central) -> "PUBLISHING" -> publishToSonaytypeAndRelease
///////////////////////////////
publishing {
publications {
create<MavenPublication>("maven") {
groupId = Extras.group
artifactId = Extras.id
version = Extras.version
publishToSonatype {
groupId = Extras.group
artifactId = Extras.id
version = Extras.version
from(components["java"])
name = Extras.name
description = Extras.description
url = Extras.url
artifact(task<Jar>("sourceJar") {
description = "Creates a JAR that contains the source code."
vendor = Extras.vendor
vendorUrl = Extras.vendorUrl
from(sourceSets["main"].java)
archiveClassifier.set("sources")
})
artifact(task<Jar>("javaDocJar") {
description = "Creates a JAR that contains the javadocs."
archiveClassifier.set("javadoc")
})
pom {
name.set(Extras.name)
description.set(Extras.description)
url.set(Extras.url)
issueManagement {
url.set("${Extras.url}/issues")
system.set("Gitea Issues")
}
organization {
name.set(Extras.vendor)
url.set("https://dorkbox.com")
}
developers {
developer {
id.set("dorkbox")
name.set(Extras.vendor)
email.set("email@dorkbox.com")
}
}
scm {
url.set(Extras.url)
connection.set("scm:${Extras.url}.git")
}
}
}
issueManagement {
url = "${Extras.url}/issues"
nickname = "Gitea Issues"
}
tasks.withType<PublishToMavenRepository> {
doFirst {
println("\tPublishing '${publication.groupId}:${publication.artifactId}:${publication.version}' to ${repository.url}")
}
onlyIf {
publication == publishing.publications["maven"] && repository == publishing.repositories["sonatype"]
}
developer {
id = "dorkbox"
name = Extras.vendor
email = "email@dorkbox.com"
}
tasks.withType<PublishToMavenLocal> {
onlyIf {
publication == publishing.publications["maven"]
}
}
// output the release URL in the console
tasks["releaseRepository"].doLast {
val url = "https://oss.sonatype.org/content/repositories/releases/"
val projectName = Extras.group.replace('.', '/')
val name = Extras.name
val version = Extras.version
println("Maven URL: $url$projectName/$name/$version/")
}
nexusStaging {
username = Extras.sonatypeUserName
sonatype {
userName = Extras.sonatypeUserName
password = Extras.sonatypePassword
}
nexusPublishing {
packageGroup.set(Extras.group)
repositories {
sonatype() {
username.set(Extras.sonatypeUserName)
password.set(Extras.sonatypePassword)
}
}
}
signing {
useInMemoryPgpKeys(File(Extras.sonatypePrivateKeyFile).readText(), Extras.sonatypePrivateKeyPassword)
sign(publishing.publications["maven"])
}
task<Task>("publishToSonatypeAndRelease") {
group = "publishing"
// required to make sure the tasks run in the correct order
tasks["closeAndReleaseRepository"].mustRunAfter(tasks["publishToSonatype"])
dependsOn("publishToSonatype", "closeAndReleaseRepository")
privateKey {
fileName = Extras.sonatypePrivateKeyFile
password = Extras.sonatypePrivateKeyPassword
}
}

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.4.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

24
logback.xml Normal file
View File

@ -0,0 +1,24 @@
<configuration scan="true">
<!-- see http://logback.qos.ch/manual/architecture.html for more info -->
<!-- logger order goes (from lowest to highest) TRACE->DEBUG->INFO->WARN->ERROR->OFF -->
<!-- Ignore startup info -->
<statusListener class="ch.qos.logback.core.status.NopStatusListener"/>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- external/libs -->
<logger name="ch.qos.logback" level="ERROR"/>
<root level="INFO"> <!-- Release: ERROR -->
<appender-ref ref="STDOUT"/>
</root>
</configuration>

View File

@ -15,42 +15,37 @@
*/
package dorkbox.network;
import static dorkbox.network.pipeline.ConnectionType.LOCAL;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.security.SecureRandom;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import dorkbox.network.connection.BootstrapWrapper;
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.connection.RegistrationWrapperClient;
import dorkbox.network.connection.idle.IdleBridge;
import dorkbox.network.connection.idle.IdleSender;
import dorkbox.network.connection.registration.local.RegistrationLocalHandlerClient;
import dorkbox.network.connection.registration.remote.RegistrationRemoteHandlerClientTCP;
import dorkbox.network.connection.registration.remote.RegistrationRemoteHandlerClientUDP;
import dorkbox.network.rmi.RemoteObject;
import dorkbox.network.rmi.RemoteObjectCallback;
import dorkbox.network.rmi.TimeoutException;
import dorkbox.util.OS;
import dorkbox.util.exceptions.SecurityException;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.FixedRecvByteBufAllocator;
import io.netty.channel.WriteBufferWaterMark;
import io.netty.channel.epoll.EpollDatagramChannel;
import io.netty.channel.epoll.EpollSocketChannel;
import io.netty.channel.kqueue.KQueueDatagramChannel;
import io.netty.channel.kqueue.KQueueSocketChannel;
import io.netty.channel.local.LocalAddress;
import io.netty.channel.local.LocalChannel;
import io.netty.channel.socket.nio.NioDatagramChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.channel.socket.oio.OioDatagramChannel;
import io.netty.channel.socket.oio.OioSocketChannel;
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
@ -67,24 +62,27 @@ class Client<C extends Connection> extends EndPointClient implements Connection
return "4.1";
}
private final String localChannelName;
private final String hostName;
private Configuration config;
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 {
this(Configuration.localOnly());
}
/**
* Starts a TCP & UDP client (or a LOCAL client), with the specified serialization scheme
*/
public
Client(String host, int tcpPort, int udpPort, String localChannelName) throws SecurityException {
this(new Configuration(host, tcpPort, udpPort, localChannelName));
Client() throws SecurityException, IOException {
this(new ClientConfiguration());
}
/**
@ -92,144 +90,8 @@ class Client<C extends Connection> extends EndPointClient implements Connection
*/
@SuppressWarnings("AutoBoxing")
public
Client(final Configuration config) throws SecurityException {
Client(final ClientConfiguration config) throws SecurityException, IOException {
super(config);
String threadName = Client.class.getSimpleName();
this.config = config;
boolean hostConfigured = (config.tcpPort > 0 || config.udpPort > 0) && config.host != null;
boolean isLocalChannel = config.localChannelName != null;
if (isLocalChannel && hostConfigured) {
String msg = threadName + " Local channel use and TCP/UDP use are MUTUALLY exclusive. Unable to determine what to do.";
logger.error(msg);
throw new IllegalArgumentException(msg);
}
localChannelName = config.localChannelName;
hostName = config.host;
Bootstrap localBootstrap = null;
Bootstrap tcpBootstrap = null;
Bootstrap udpBootstrap = null;
if (config.localChannelName != null) {
localBootstrap = new Bootstrap();
}
if (config.tcpPort > 0 || config.udpPort > 0) {
if (config.host == null) {
throw new IllegalArgumentException("You must define what host you want to connect to.");
}
if (config.host.equals("0.0.0.0")) {
throw new IllegalArgumentException("You cannot connect to 0.0.0.0, you must define what host you want to connect to.");
}
}
if (config.tcpPort > 0) {
tcpBootstrap = new Bootstrap();
}
if (config.udpPort > 0) {
udpBootstrap = new Bootstrap();
}
if (localBootstrap == null && tcpBootstrap == null && udpBootstrap == null) {
throw new IllegalArgumentException("You must define how you want to connect, either LOCAL channel name, TCP port, or UDP port");
}
if (config.localChannelName != null) {
// no networked bootstraps. LOCAL connection only
bootstraps.add(new BootstrapWrapper("LOCAL", config.localChannelName, -1, localBootstrap));
localBootstrap.group(newEventLoop(LOCAL, 1, threadName + "-JVM-BOSS"))
.channel(LocalChannel.class)
.remoteAddress(new LocalAddress(config.localChannelName))
.handler(new RegistrationLocalHandlerClient(threadName, (RegistrationWrapperClient) registrationWrapper));
}
EventLoopGroup workerEventLoop = null;
if (tcpBootstrap != null || udpBootstrap != null) {
workerEventLoop = newEventLoop(config.workerThreadPoolSize, threadName);
}
if (tcpBootstrap != null) {
bootstraps.add(new BootstrapWrapper("TCP", config.host, config.tcpPort, tcpBootstrap));
if (OS.isAndroid()) {
// android ONLY supports OIO (not NIO)
tcpBootstrap.channel(OioSocketChannel.class);
}
else if (OS.isLinux() && NativeLibrary.isAvailable()) {
// epoll network stack is MUCH faster (but only on linux)
tcpBootstrap.channel(EpollSocketChannel.class);
}
else if (OS.isMacOsX() && NativeLibrary.isAvailable()) {
// KQueue network stack is MUCH faster (but only on macosx)
tcpBootstrap.channel(KQueueSocketChannel.class);
}
else {
tcpBootstrap.channel(NioSocketChannel.class);
}
tcpBootstrap.group(newEventLoop(1, threadName + "-TCP-BOSS"))
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.option(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(WRITE_BUFF_LOW, WRITE_BUFF_HIGH))
.remoteAddress(config.host, config.tcpPort)
.handler(new RegistrationRemoteHandlerClientTCP(threadName, (RegistrationWrapperClient) registrationWrapper, workerEventLoop));
// android screws up on this!!
tcpBootstrap.option(ChannelOption.TCP_NODELAY, !OS.isAndroid())
.option(ChannelOption.SO_KEEPALIVE, true);
}
if (udpBootstrap != null) {
bootstraps.add(new BootstrapWrapper("UDP", config.host, config.udpPort, udpBootstrap));
if (OS.isAndroid()) {
// android ONLY supports OIO (not NIO)
udpBootstrap.channel(OioDatagramChannel.class);
}
else if (OS.isLinux() && NativeLibrary.isAvailable()) {
// epoll network stack is MUCH faster (but only on linux)
udpBootstrap.channel(EpollDatagramChannel.class);
}
else if (OS.isMacOsX() && NativeLibrary.isAvailable()) {
// KQueue network stack is MUCH faster (but only on macosx)
udpBootstrap.channel(KQueueDatagramChannel.class);
}
else {
udpBootstrap.channel(NioDatagramChannel.class);
}
udpBootstrap.group(newEventLoop(1, threadName + "-UDP-BOSS"))
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
// Netty4 has a default of 2048 bytes as upper limit for datagram packets.
.option(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(EndPoint.udpMaxSize))
.option(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(WRITE_BUFF_LOW, WRITE_BUFF_HIGH))
.localAddress(new InetSocketAddress(0)) // bind to wildcard
.remoteAddress(new InetSocketAddress(config.host, config.udpPort))
.handler(new RegistrationRemoteHandlerClientUDP(threadName, (RegistrationWrapperClient) registrationWrapper, workerEventLoop));
// Enable to READ and WRITE MULTICAST data (ie, 192.168.1.0)
// in order to WRITE: write as normal, just make sure it ends in .255
// in order to LISTEN:
// InetAddress group = InetAddress.getByName("203.0.113.0");
// NioDatagramChannel.joinGroup(group);
// THEN once done
// NioDatagramChannel.leaveGroup(group), close the socket
udpBootstrap.option(ChannelOption.SO_BROADCAST, false)
.option(ChannelOption.SO_SNDBUF, udpMaxSize);
}
}
/**
@ -239,7 +101,7 @@ class Client<C extends Connection> extends EndPointClient implements Connection
* if the client is unable to reconnect in the previously requested connection-timeout
*/
public
void reconnect() throws IOException {
void reconnect() throws IOException, EchoClientException {
reconnect(connectionTimeout);
}
@ -250,7 +112,7 @@ class Client<C extends Connection> extends EndPointClient implements Connection
* if the client is unable to reconnect in the requested time
*/
public
void reconnect(final int connectionTimeout) throws IOException {
void reconnect(final int connectionTimeout) throws IOException, EchoClientException {
// make sure we are closed first
close();
@ -265,7 +127,7 @@ class Client<C extends Connection> extends EndPointClient implements Connection
* if the client is unable to connect in 30 seconds
*/
public
void connect() throws IOException {
void connect() throws IOException, EchoClientException {
connect(30000);
}
@ -281,61 +143,136 @@ class Client<C extends Connection> extends EndPointClient implements Connection
* if the client is unable to connect in the requested time
*/
public
void connect(final int connectionTimeout) throws IOException {
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) {
// 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);
}
}
// 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();
/*
* 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();
}
}
}
});
return;
}
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);
} catch (final IOException e) {
throw new ClientIOException(e);
}
}
// 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();
}
// 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
@ -362,6 +299,19 @@ class Client<C extends Connection> extends EndPointClient implements Connection
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.
*/
@ -380,39 +330,9 @@ class Client<C extends Connection> extends EndPointClient implements Connection
return connection.idAsHex();
}
@Override
public
boolean hasUDP() {
return connection.hasUDP();
}
/**
* Expose methods to send objects to a destination when the connection has become idle.
*/
@Override
public
IdleBridge sendOnIdle(IdleSender<?, ?> sender) {
return connection.sendOnIdle(sender);
}
/**
* Expose methods to send objects to a destination when the connection has become idle.
*/
@Override
public
IdleBridge sendOnIdle(Object message) {
return connection.sendOnIdle(message);
}
/**
* Marks the connection to be closed as soon as possible. This is evaluated when the current
* thread execution returns to the network stack.
*/
@Override
public
void closeAsap() {
connection.closeAsap();
}
/**
* Tells the remote connection to create a new proxy object that implements the specified interface. The methods on this object "map"
@ -499,7 +419,8 @@ class Client<C extends Connection> extends EndPointClient implements Connection
@Override
public
void close() {
closeConnection();
super.close();
// closeConnection();
}
/**
@ -511,5 +432,248 @@ class Client<C extends Connection> extends EndPointClient implements Connection
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

@ -13,27 +13,27 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.connection.idle;
import dorkbox.network.connection.Connection;
package dorkbox.network;
public
class IdleListenerUDP<C extends Connection, M> implements IdleListener<C, M> {
class ClientConfiguration extends Configuration {
/**
* used by the Idle Sender
* 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
IdleListenerUDP() {
}
public String remoteAddress = "localhost";
/**
* used by the Idle Sender
*/
@Override
public
void send(C connection, M message) {
connection.send()
.UDP(message);
ClientConfiguration() {
super();
}
}

View File

@ -0,0 +1,256 @@
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

@ -15,39 +15,31 @@
*/
package dorkbox.network;
import dorkbox.network.connection.EndPoint;
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 {
/**
* On the server, if host is null, it will bind to the "any" address, otherwise you must specify the hostname/IP to bind to.
* Specify the UDP port to use. This port is used to establish client-server connections.
* <p>
* Must be greater than 0
*/
public String host = null;
public int port = 0;
/**
* Specify the TCP port to use. The server will listen on this port, the client will connect to it.
* Specify the UDP MDC control port to use. This port is used to establish client-server connections.
* <p>
* Must be > 0 to be used
* Must be greater than 0
*/
public int tcpPort = 0;
/**
* Specify the UDP port to use. The server will listen on this port, the client will connect to it.
* <p>
* Must be > 0 to be used
* <p>
* UDP requires TCP to handshake
*/
public int udpPort = 0;
/**
* Specify the local channel name to use, if the default is not wanted.
* <p>
* Local/remote configurations are incompatible with each other.
*/
public String localChannelName = null;
public int controlPort = 0;
/**
* Allows the end user to change how server settings are stored. For example, a custom database instead of the default.
@ -59,34 +51,110 @@ class Configuration {
*/
public NetworkSerializationManager serialization = null;
/**
* The number of threads used for the worker threads by the end point. By default, this is the CPU_COUNT/2 or 1, whichever is larger.
* 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 int workerThreadPoolSize = Math.max(Runtime.getRuntime().availableProcessors() / 2, 1);
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() {
}
/**
* Creates a new configuration for a connection that is local inside the JVM using the default name.
* <p>
* Local/remote configurations are incompatible with each other when running as a client. Servers can listen on all of them.
*/
public static
Configuration localOnly() {
Configuration configuration = new Configuration();
configuration.localChannelName = EndPoint.LOCAL_CHANNEL;
return configuration;
}
public
Configuration(String host, int tcpPort, int udpPort, String localChannelName) {
this.host = host;
this.tcpPort = tcpPort;
this.udpPort = udpPort;
this.localChannelName = localChannelName;
}
}

View File

@ -1,148 +0,0 @@
/*
* Copyright 2018 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.lang.reflect.Field;
import dorkbox.network.connection.EndPoint;
import dorkbox.util.NativeLoader;
import dorkbox.util.OS;
import io.netty.channel.epoll.Epoll;
import io.netty.channel.kqueue.KQueue;
import io.netty.util.internal.PlatformDependent;
import io.netty.util.internal.SystemPropertyUtil;
/**
*
*/
public
class NativeLibrary {
/**
* Tries to extract the native transport libraries for Linux/MacOsX into a "semi-permanent" location. If this is unsuccessful for any
* reason, netty will fall back to it's own logic.
*/
static {
if (EndPoint.enableNativeLibrary && (OS.isLinux() || OS.isMacOsX())) {
// try to load the native libraries for Linux/MacOsX...
String originalLibraryPath = SystemPropertyUtil.get("java.library.path");
File outputDirectory;
String workdir = SystemPropertyUtil.get("io.netty.native.workdir");
if (workdir != null) {
File f = new File(workdir);
if (!f.isDirectory()) {
f.mkdirs();
}
try {
f = f.getAbsoluteFile();
} catch (Exception ignored) {
// Good to have an absolute path, but it's OK.
}
outputDirectory = f;
// logger.debug("-Dio.netty.native.workdir: " + WORKDIR);
}
else {
outputDirectory = PlatformDependent.tmpdir();
// logger.debug("-Dio.netty.native.workdir: " + WORKDIR + " (io.netty.tmpdir)");
}
try {
System.setProperty("java.library.path", originalLibraryPath + File.pathSeparator + outputDirectory.getAbsolutePath());
// reset the classloader library path.
Field fieldSysPath = ClassLoader.class.getDeclaredField("sys_paths");
fieldSysPath.setAccessible(true);
fieldSysPath.set(null, null);
} catch (Exception ignored) {
}
String staticLibName;
if (OS.isLinux()) {
staticLibName = "netty_transport_native_epoll";
}
else {
staticLibName = "netty_transport_native_kqueue";
}
staticLibName = "lib" + staticLibName + '_' + PlatformDependent.normalizedArch();
String jarLibName = "META-INF/native/" + staticLibName;
if (OS.isLinux()) {
jarLibName += ".so";
}
else {
jarLibName += ".jnilib";
}
try {
NativeLoader.extractLibrary(jarLibName, outputDirectory.getAbsolutePath(), staticLibName, null);
// we have to try to load the native library HERE, while the java.library.path has it
if (OS.isLinux()) {
//noinspection ResultOfMethodCallIgnored
Epoll.isAvailable();
}
else if (OS.isMacOsX()) {
//noinspection ResultOfMethodCallIgnored
KQueue.isAvailable();
}
} catch (Exception ignored) {
} finally {
System.setProperty("java.library.path", originalLibraryPath);
try {
// reset the classloader library path.
Field fieldSysPath = ClassLoader.class.getDeclaredField("sys_paths");
fieldSysPath.setAccessible(true);
fieldSysPath.set(null, null);
} catch (Exception ignored) {
}
}
}
// either not Linux/MacOsX, or loading the library failed.
}
/**
* @return true if the (possibly) required native libraries have been loaded
*/
public static
boolean isAvailable() {
if (!EndPoint.enableNativeLibrary) {
return false;
}
if (OS.isLinux()) {
return Epoll.isAvailable();
}
else if (OS.isMacOsX()) {
return KQueue.isAvailable();
}
// not Linux/MacOsX
return true;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -15,47 +15,28 @@
*/
package dorkbox.network;
import static dorkbox.network.pipeline.ConnectionType.LOCAL;
import java.io.IOException;
import java.net.Socket;
import java.time.Clock;
import java.util.Arrays;
import java.util.List;
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.EndPoint;
import dorkbox.network.connection.EndPointServer;
import dorkbox.network.connection.RegistrationWrapperServer;
import dorkbox.network.connection.connectionType.ConnectionRule;
import dorkbox.network.connection.registration.local.RegistrationLocalHandlerServer;
import dorkbox.network.connection.registration.remote.RegistrationRemoteHandlerServerTCP;
import dorkbox.network.connection.registration.remote.RegistrationRemoteHandlerServerUDP;
import dorkbox.network.pipeline.discovery.BroadcastResponse;
import dorkbox.util.OS;
import dorkbox.util.Property;
import dorkbox.network.ipFilter.IpFilterRule;
import dorkbox.util.exceptions.SecurityException;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.bootstrap.SessionBootstrap;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.FixedRecvByteBufAllocator;
import io.netty.channel.WriteBufferWaterMark;
import io.netty.channel.epoll.EpollDatagramChannel;
import io.netty.channel.epoll.EpollServerSocketChannel;
import io.netty.channel.kqueue.KQueueDatagramChannel;
import io.netty.channel.kqueue.KQueueServerSocketChannel;
import io.netty.channel.local.LocalAddress;
import io.netty.channel.local.LocalServerChannel;
import io.netty.channel.socket.nio.NioDatagramChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.oio.OioDatagramChannel;
import io.netty.channel.socket.oio.OioServerSocketChannel;
import io.netty.handler.ipfilter.IpFilterRule;
import io.netty.handler.ipfilter.IpFilterRuleType;
import io.netty.handler.ipfilter.IpSubnetFilterRule;
import io.netty.util.NetUtil;
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
@ -66,11 +47,6 @@ import io.netty.util.NetUtil;
public
class Server<C extends Connection> extends EndPointServer {
/**
* Rule that will always allow LOCALHOST to connect to the server. This is not added by default
*/
public static final IpFilterRule permitLocalHostRule = new IpSubnetFilterRule(NetUtil.LOCALHOST, 32, IpFilterRuleType.ACCEPT);
/**
* Gets the version number.
*/
@ -79,32 +55,17 @@ class Server<C extends Connection> extends EndPointServer {
return "4.1";
}
/**
* The maximum queue length for incoming connection indications (a request to connect). If a connection indication arrives when the
* queue is full, the connection is refused.
*/
@Property
public static int backlogConnectionCount = 50;
private final ServerBootstrap localBootstrap;
private final ServerBootstrap tcpBootstrap;
private final SessionBootstrap udpBootstrap;
private final int tcpPort;
private final int udpPort;
private final String localChannelName;
private final String hostName;
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 {
this(Configuration.localOnly());
Server() throws SecurityException, IOException {
this(new ServerConfiguration());
}
/**
@ -112,169 +73,32 @@ class Server<C extends Connection> extends EndPointServer {
*/
@SuppressWarnings("AutoBoxing")
public
Server(Configuration config) throws SecurityException {
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);
tcpPort = config.tcpPort;
udpPort = config.udpPort;
EchoServerExecutorService exec = null;
try {
this.executor = EchoServerExecutor.create(Server.class);
localChannelName = config.localChannelName;
if (config.host == null) {
// we set this to "0.0.0.0" so that it is clear that we are trying to bind to that address.
hostName = "0.0.0.0";
// make it clear that this is what we do (configuration wise) so that variable examination is consistent
config.host = hostName;
}
else {
hostName = config.host;
}
if (localChannelName != null) {
localBootstrap = new ServerBootstrap();
}
else {
localBootstrap = null;
}
if (tcpPort > 0) {
tcpBootstrap = new ServerBootstrap();
}
else {
tcpBootstrap = null;
}
if (udpPort > 0) {
// This is what allows us to have UDP behave "similar" to TCP, in that a session is established based on the port/ip of the
// remote connection. This allows us to reuse channels and have "state" for a UDP connection that normally wouldn't exist.
// Additionally, this is what responds to discovery broadcast packets
udpBootstrap = new SessionBootstrap(tcpPort, udpPort);
}
else {
udpBootstrap = null;
}
String threadName = Server.class.getSimpleName();
// always use local channels on the server.
if (localBootstrap != null) {
localBootstrap.group(newEventLoop(LOCAL, 1, threadName + "-JVM-BOSS"),
newEventLoop(LOCAL, 1, threadName ))
.channel(LocalServerChannel.class)
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.option(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(WRITE_BUFF_LOW, WRITE_BUFF_HIGH))
.localAddress(new LocalAddress(localChannelName))
.childHandler(new RegistrationLocalHandlerServer(threadName, (RegistrationWrapperServer) registrationWrapper));
}
EventLoopGroup workerEventLoop = null;
if (tcpBootstrap != null || udpBootstrap != null) {
workerEventLoop = newEventLoop(config.workerThreadPoolSize, threadName);
}
if (tcpBootstrap != null) {
if (OS.isAndroid()) {
// android ONLY supports OIO (not NIO)
tcpBootstrap.channel(OioServerSocketChannel.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;
}
else if (OS.isLinux() && NativeLibrary.isAvailable()) {
// epoll network stack is MUCH faster (but only on linux)
tcpBootstrap.channel(EpollServerSocketChannel.class);
} catch (final Exception e) {
try {
if (exec != null) {
exec.close();
}
} catch (final Exception c_ex) {
e.addSuppressed(c_ex);
}
else if (OS.isMacOsX() && NativeLibrary.isAvailable()) {
// KQueue network stack is MUCH faster (but only on macosx)
tcpBootstrap.channel(KQueueServerSocketChannel.class);
}
else {
tcpBootstrap.channel(NioServerSocketChannel.class);
}
// TODO: If we use netty for an HTTP server,
// Beside the usual ChannelOptions the Native Transport allows to enable TCP_CORK which may come in handy if you implement a HTTP Server.
tcpBootstrap.group(newEventLoop(1, threadName + "-TCP-BOSS"),
newEventLoop(1, threadName + "-TCP-REGISTRATION"))
.option(ChannelOption.SO_BACKLOG, backlogConnectionCount)
.option(ChannelOption.SO_REUSEADDR, true)
.option(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(WRITE_BUFF_LOW, WRITE_BUFF_HIGH))
.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new RegistrationRemoteHandlerServerTCP(threadName, (RegistrationWrapperServer) registrationWrapper, workerEventLoop));
// have to check options.host for "0.0.0.0". we don't bind to "0.0.0.0", we bind to "null" to get the "any" address!
if (hostName.equals("0.0.0.0")) {
tcpBootstrap.localAddress(tcpPort);
}
else {
tcpBootstrap.localAddress(hostName, tcpPort);
}
// android screws up on this!!
tcpBootstrap.option(ChannelOption.TCP_NODELAY, !OS.isAndroid())
.childOption(ChannelOption.TCP_NODELAY, !OS.isAndroid());
}
if (udpBootstrap != null) {
if (OS.isAndroid()) {
// android ONLY supports OIO (not NIO)
udpBootstrap.channel(OioDatagramChannel.class);
}
else if (OS.isLinux() && NativeLibrary.isAvailable()) {
// epoll network stack is MUCH faster (but only on linux)
udpBootstrap.channel(EpollDatagramChannel.class);
}
else if (OS.isMacOsX() && NativeLibrary.isAvailable()) {
// KQueue network stack is MUCH faster (but only on macosx)
udpBootstrap.channel(KQueueDatagramChannel.class);
}
else {
// windows and linux/mac that are incompatible with the native implementations
udpBootstrap.channel(NioDatagramChannel.class);
}
// Netty4 has a default of 2048 bytes as upper limit for datagram packets, we want this to be whatever we specify
FixedRecvByteBufAllocator recvByteBufAllocator = new FixedRecvByteBufAllocator(EndPoint.udpMaxSize);
udpBootstrap.group(newEventLoop(1, threadName + "-UDP-BOSS"),
newEventLoop(1, threadName + "-UDP-REGISTRATION"))
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.option(ChannelOption.RCVBUF_ALLOCATOR, recvByteBufAllocator)
.option(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(WRITE_BUFF_LOW, WRITE_BUFF_HIGH))
// a non-root user can't receive a broadcast packet on *nix if the socket is bound on non-wildcard address.
// TODO: move broadcast to it's own handler, and have UDP server be able to be bound to a specific IP
// OF NOTE: At the end in my case I decided to bind to .255 broadcast address on Linux systems. (to receive broadcast packets)
.localAddress(udpPort) // if you bind to a specific interface, Linux will be unable to receive broadcast packets! see: http://developerweb.net/viewtopic.php?id=5722
.childHandler(new RegistrationRemoteHandlerServerUDP(threadName, (RegistrationWrapperServer) registrationWrapper, workerEventLoop));
// // have to check options.host for null. we don't bind to 0.0.0.0, we bind to "null" to get the "any" address!
// if (hostName.equals("0.0.0.0")) {
// udpBootstrap.localAddress(hostName, tcpPort);
// }
// else {
// udpBootstrap.localAddress(udpPort);
// }
// Enable to READ from MULTICAST data (ie, 192.168.1.0)
// in order to WRITE: write as normal, just make sure it ends in .255
// in order to LISTEN:
// InetAddress group = InetAddress.getByName("203.0.113.0");
// socket.joinGroup(group);
// THEN once done
// socket.leaveGroup(group), close the socket
// Enable to WRITE to MULTICAST data (ie, 192.168.1.0)
udpBootstrap.option(ChannelOption.SO_BROADCAST, false)
.option(ChannelOption.SO_SNDBUF, udpMaxSize);
throw new IOException(e);
}
}
@ -301,79 +125,49 @@ class Server<C extends Connection> extends EndPointServer {
@SuppressWarnings("AutoBoxing")
public
void bind(boolean blockUntilTerminate) {
// make sure we are not trying to connect during a close or stop event.
// This will wait until we have finished starting up/shutting down.
synchronized (shutdownInProgress) {
}
// The bootstraps will be accessed ONE AT A TIME, in this order!
ChannelFuture future;
// LOCAL
if (localBootstrap != null) {
try {
future = localBootstrap.bind();
future.await();
} catch (InterruptedException e) {
throw new IllegalArgumentException("Could not bind to LOCAL address '" + localChannelName + "' on the server.", e);
}
if (!future.isSuccess()) {
throw new IllegalArgumentException("Could not bind to LOCAL address '" + localChannelName + "' on the server.", future.cause());
}
logger.info("Listening on LOCAL address: [{}]", localChannelName);
manageForShutdown(future);
}
// TCP
if (tcpBootstrap != null) {
// Wait until the connection attempt succeeds or fails.
try {
future = tcpBootstrap.bind();
future.await();
} catch (Exception e) {
stop();
throw new IllegalArgumentException("Could not bind to address " + hostName + " TCP port " + tcpPort + " on the server.", e);
}
if (!future.isSuccess()) {
stop();
throw new IllegalArgumentException("Could not bind to address " + hostName + " TCP port " + tcpPort + " on the server.", future.cause());
}
logger.info("TCP server listen address [{}:{}]", hostName, tcpPort);
manageForShutdown(future);
}
// UDP
if (udpBootstrap != null) {
// Wait until the connection attempt succeeds or fails.
try {
future = udpBootstrap.bind();
future.await();
} catch (Exception e) {
throw new IllegalArgumentException("Could not bind to address " + hostName + " UDP port " + udpPort + " on the server.", e);
}
if (!future.isSuccess()) {
throw new IllegalArgumentException("Could not bind to address " + hostName + " UDP port " + udpPort + " on the server.",
future.cause());
}
logger.info("UDP server listen address [{}:{}]", hostName, udpPort);
manageForShutdown(future);
if (isRunning) {
logger.error("Unable to bind when the server is already running!");
return;
}
isRunning = true;
try (final Publication publication = this.setupAllClientsPublication()) {
try (final Subscription subscription = this.setupAllClientsSubscription()) {
/**
* 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 handler = new FragmentAssembler((buffer, offset, length, header)->this.onInitialClientMessage(
publication,
buffer,
offset,
length,
header));
while (true) {
this.executor.execute(()->{
subscription.poll(handler, 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();
}
}
}
}
// 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();
}
// if (blockUntilTerminate) {
// waitForShutdown();
// }
}
/**
@ -384,7 +178,7 @@ class Server<C extends Connection> extends EndPointServer {
* If there is nothing added to this list - then ALL are permitted
*/
public
void addIpFilter(IpFilterRule... rules) {
void addIpFilterRule(IpFilterRule... rules) {
ipFilterRules.addAll(Arrays.asList(rules));
}
@ -403,28 +197,10 @@ class Server<C extends Connection> extends EndPointServer {
* Uncompress : 0.641 micros/op; 6097.9 MB/s
*/
public
void addConnectionTypeFilter(ConnectionRule... rules) {
void addConnectionRules(ConnectionRule... rules) {
connectionRules.addAll(Arrays.asList(rules));
}
// called when we are stopped/shut down
@Override
protected
void stopExtraActions() {
isRunning = false;
// now WAIT until bind has released the socket
// wait a max of 10 tries
int tries = 10;
while (tries-- > 0 && isRunning(this.config)) {
logger.warn("Server has requested shutdown, but the socket is still bound. Waiting {} more times", tries);
try {
Thread.sleep(2000);
} catch (InterruptedException ignored) {
}
}
}
/**
* @return true if this server has successfully bound to an IP address and is running
*/
@ -445,41 +221,99 @@ class Server<C extends Connection> extends EndPointServer {
*/
public static
boolean isRunning(Configuration config) {
String host = config.host;
// 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";
// }
// for us, we want a "null" host to connect to the "any" interface.
if (host == null) {
host = "0.0.0.0";
}
if (config.tcpPort > 0) {
Socket sock = null;
// since we check the socket, if we cannot connect to a socket, then we're done.
try {
sock = new Socket(host, config.tcpPort);
// if we can connect to the socket, it means that we are already running.
return sock.isConnected();
} catch (Exception ignored) {
if (sock != null) {
try {
sock.close();
} catch (IOException ignored2) {
}
}
}
}
// create a client and see if it can connect
// use Broadcast to see if there is a UDP server connected
if (config.udpPort > 0) {
List<BroadcastResponse> broadcastResponses = null;
try {
broadcastResponses = Broadcast.discoverHosts0(null, config.udpPort, 500, true);
return !broadcastResponses.isEmpty();
} catch (IOException ignored) {
}
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);
}
});
}
/**
* Configure the publication for the "all-clients" channel.
*/
private
Publication setupAllClientsPublication() {
return EchoChannels.createPublicationDynamicMDC(this.aeron,
this.config.listenIpAddress,
this.config.controlPort,
UDP_STREAM_ID);
}
/**
* Configure the subscription for the "all-clients" channel.
*/
private
Subscription setupAllClientsSubscription() {
return EchoChannels.createSubscriptionWithHandlers(this.aeron,
this.config.listenIpAddress,
this.config.port,
UDP_STREAM_ID,
this::onInitialClientConnected,
this::onInitialClientDisconnected);
}
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,48 @@
/*
* 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,40 @@
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

@ -0,0 +1,295 @@
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

@ -0,0 +1,108 @@
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

@ -0,0 +1,99 @@
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

@ -0,0 +1,311 @@
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

@ -0,0 +1,78 @@
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

@ -0,0 +1,30 @@
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

@ -0,0 +1,97 @@
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,12 @@
package dorkbox.network.aeron.exceptions;
import java.io.IOException;
public final class ClientIOException extends EchoClientException
{
public
ClientIOException(final IOException cause)
{
super(cause);
}
}

View File

@ -0,0 +1,19 @@
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

@ -0,0 +1,32 @@
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

@ -0,0 +1,19 @@
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

@ -0,0 +1,19 @@
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

@ -0,0 +1,19 @@
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

@ -0,0 +1,32 @@
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

@ -0,0 +1,20 @@
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

@ -0,0 +1,21 @@
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,102 @@
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

@ -15,9 +15,6 @@
*/
package dorkbox.network.connection;
import dorkbox.network.connection.bridge.ConnectionBridge;
import dorkbox.network.connection.idle.IdleBridge;
import dorkbox.network.connection.idle.IdleSender;
import dorkbox.network.rmi.RemoteObject;
import dorkbox.network.rmi.RemoteObjectCallback;
import dorkbox.network.rmi.TimeoutException;
@ -42,56 +39,81 @@ interface Connection {
boolean isLoopback();
/**
* @return the connection (TCP or LOCAL) id of this connection.
* @return true if this connection is an IPC connection
*/
boolean isIPC();
/**
* @return true if this connection is a network connection
*/
boolean isNetwork();
/**
* @return the connection id of this connection.
*/
int id();
/**
* @return the connection (TCP or LOCAL) id of this connection as a HEX string.
* @return the connection id of this connection as a HEX string.
*/
String idAsHex();
/**
* @return true if this connection is also configured to use UDP
*/
boolean hasUDP();
/**
* Expose methods to send objects to a destination (such as a custom object or a standard ping)
*/
ConnectionBridge send();
/**
* 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 #send()}, followed by the protocol you wish to use.
* Safely sends objects to a destination.
*/
ConnectionPoint send(Object message);
/**
* Expose methods to send objects to a destination when the connection has become idle.
* 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.
*/
IdleBridge sendOnIdle(IdleSender<?, ?> sender);
ConnectionPoint send(Object message, byte priority);
/**
* Expose methods to send objects to a destination when the connection has become idle.
* Safely sends objects to a destination, but does not guarantee delivery
*/
IdleBridge sendOnIdle(Object message);
ConnectionPoint sendUnreliable(Object message);
/**
* Expose methods to modify the connection listeners.
* 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.
*
* @return Ping can have a listener attached, which will get called when the ping returns.
*/
Ping ping(); // TODO: USE AERON FOR THIS
/**
* Expose methods to modify the connection-specific listeners.
*/
Listeners listeners();
/**
* Closes the connection, but does not remove any listeners
* Closes the connection and removes all listeners
*/
void close();
/**
* Marks the connection to be closed as soon as possible. This is evaluated when the current thread execution returns to the network stack.
*/
void closeAsap();
// TODO: below should just be "new()" to create a new object, to mirror "new Object()"
// // RMI
// // client.get(5) -> gets from the server connection, if exists, then global.
// // on server, a connection local RMI object "uses" an id for global, so there will never be a conflict
// // 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.

View File

@ -25,9 +25,6 @@ import javax.crypto.SecretKey;
import dorkbox.network.Client;
import dorkbox.network.connection.bridge.ConnectionBridge;
import dorkbox.network.connection.idle.IdleBridge;
import dorkbox.network.connection.idle.IdleSender;
import dorkbox.network.connection.idle.IdleSenderFactory;
import dorkbox.network.connection.ping.PingFuture;
import dorkbox.network.connection.ping.PingMessage;
import dorkbox.network.connection.ping.PingTuple;
@ -39,19 +36,14 @@ import dorkbox.network.rmi.ConnectionRmiLocalSupport;
import dorkbox.network.rmi.ConnectionRmiNetworkSupport;
import dorkbox.network.rmi.ConnectionRmiSupport;
import dorkbox.network.rmi.RemoteObjectCallback;
import io.netty.bootstrap.DatagramSessionChannel;
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.EpollDatagramChannel;
import io.netty.channel.epoll.EpollSocketChannel;
import io.netty.channel.kqueue.KQueueDatagramChannel;
import io.netty.channel.kqueue.KQueueSocketChannel;
import io.netty.channel.local.LocalChannel;
import io.netty.channel.socket.nio.NioDatagramChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.channel.socket.oio.OioDatagramChannel;
import io.netty.channel.socket.oio.OioSocketChannel;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
@ -76,11 +68,7 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements Connection_
public static
boolean isUdpChannel(Class<? extends Channel> channelClass) {
return channelClass == OioDatagramChannel.class ||
channelClass == NioDatagramChannel.class ||
channelClass == KQueueDatagramChannel.class ||
channelClass == EpollDatagramChannel.class ||
channelClass == DatagramSessionChannel.class;
return false;
}
public static
@ -114,9 +102,6 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements Connection_
private final EndPoint endPoint;
// when true, the connection will be closed (either as RMI or as 'normal' listener execution) when the thread execution returns control
// back to the network stack
private boolean closeAsap = 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
@ -241,6 +226,18 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements Connection_
return channelWrapper.isLoopback();
}
@Override
public
boolean isIPC() {
return false;
}
@Override
public
boolean isNetwork() {
return false;
}
/**
* @return the endpoint associated with this connection
*/
@ -342,15 +339,6 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements Connection_
}
}
/**
* @return true if this connection is also configured to use UDP
*/
@Override
public final
boolean hasUDP() {
return this.channelWrapper.udp() != null;
}
@Override
public
void channelWritabilityChanged(final ChannelHandlerContext context) throws Exception {
@ -390,15 +378,6 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements Connection_
}
}
/**
* Expose methods to send objects to a destination.
*/
@Override
public final
ConnectionBridge send() {
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 #TCP(Object)}, {@link #UDP(Object)}, or {@link #self(Object)}.
@ -425,6 +404,24 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements Connection_
}
}
@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.
*/
@ -495,24 +492,6 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements Connection_
this.channelWrapper.flush();
}
/**
* Expose methods to modify the connection listeners.
*/
@Override
public final
IdleBridge sendOnIdle(@SuppressWarnings("rawtypes") IdleSender sender) {
return new IdleSenderFactory(this, sender);
}
/**
* Expose methods to modify the connection listeners.
*/
@Override
public final
IdleBridge sendOnIdle(Object message) {
return new IdleSenderFactory(this, message);
}
/**
* Invoked when a {@link Channel} has been idle for a while.
@ -566,9 +545,9 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements Connection_
// 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();
}
// if (closeAsap) {
// close();
// }
}
@Override
@ -691,26 +670,22 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements Connection_
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;
}
// 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 = this.endPoint.getIdleTimeout();
if (idleTimeoutMs == 0) {
// default is 2 second timeout, in milliseconds.
idleTimeoutMs = 2000;
}
int idleTimeoutMs = 2000;
// if we are in the middle of a message, hold off.
synchronized (this.messageInProgressLock) {
@ -759,16 +734,6 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements Connection_
}
}
/**
* Marks the connection to be closed as soon as possible. This is evaluated when the current
* thread execution returns to the network stack.
*/
@Override
public final
void closeAsap() {
closeAsap = true;
}
@Override
public
void exceptionCaught(ChannelHandlerContext context, Throwable cause) throws Exception {

View File

@ -35,7 +35,6 @@ import dorkbox.network.connection.ping.PingMessage;
import dorkbox.util.Property;
import dorkbox.util.collections.ConcurrentEntry;
import dorkbox.util.generics.ClassHelper;
import io.netty.bootstrap.DatagramCloseMessage;
import io.netty.util.concurrent.ImmediateEventExecutor;
import io.netty.util.concurrent.Promise;
import net.jodah.typetools.TypeResolver;
@ -330,9 +329,9 @@ class ConnectionManager<C extends Connection> implements Listeners, ISessionMana
}
// add the UDP "close hint" to close remote connections (internal use only!)
else if (messageClass == DatagramCloseMessage.class) {
connection.forceClose();
}
// else if (messageClass == DatagramCloseMessage.class) {
// connection.forceClose();
// }
else {
notifyOnMessage0(connection, message, false);
@ -720,8 +719,8 @@ class ConnectionManager<C extends Connection> implements Listeners, ISessionMana
current = current.next();
if (c != connection) {
c.send()
.TCP(message);
// c.send()
// .TCP(message);
}
}
return this;
@ -741,8 +740,8 @@ class ConnectionManager<C extends Connection> implements Listeners, ISessionMana
current = current.next();
if (c != connection) {
c.send()
.UDP(message);
// c.send()
// .UDP(message);
}
}
return this;
@ -777,8 +776,8 @@ class ConnectionManager<C extends Connection> implements Listeners, ISessionMana
c = current.getValue();
current = current.next();
c.send()
.TCP(message);
// c.send()
// .TCP(message);
}
return this;
}
@ -795,8 +794,8 @@ class ConnectionManager<C extends Connection> implements Listeners, ISessionMana
c = current.getValue();
current = current.next();
c.send()
.UDP(message);
// c.send()
// .UDP(message);
}
return this;
}

View File

@ -15,6 +15,9 @@
*/
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;
@ -28,9 +31,11 @@ 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.connection.bridge.ConnectionBridgeBase;
import dorkbox.network.ServerConfiguration;
import dorkbox.network.connection.registration.MetaChannel;
import dorkbox.network.connection.wrapper.ChannelLocalWrapper;
import dorkbox.network.connection.wrapper.ChannelNetworkWrapper;
@ -40,22 +45,37 @@ import dorkbox.network.serialization.NetworkSerializationManager;
import dorkbox.network.serialization.Serialization;
import dorkbox.network.store.NullSettingsStore;
import dorkbox.network.store.SettingsStore;
import dorkbox.util.Property;
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.handler.codec.ByteToMessageDecoder;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.resolver.DefaultNameResolver;
import io.netty.resolver.InetSocketAddressResolver;
import io.netty.util.NetUtil;
/**
* represents the base of a client/server end point
*/
public abstract
class EndPoint extends Shutdownable {
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.
//
@ -63,31 +83,6 @@ class EndPoint extends Shutdownable {
// also, a google search on just "INET97/proceedings/F3/F3_1.HTM" turns up interesting problems.
// Usually it's with ISPs.
// TODO: will also want an UDP keepalive? (TCP is already there b/c of socket options, but might need a heartbeat to detect dead connections?)
// routers sometimes need a heartbeat to keep the connection
// TODO: maybe some sort of STUN-like connection keep-alive??
static {
// have to load some classes early to prevent stack overflow issues on windows
ConnectionImpl.isTcpChannel(null);
ConnectionImpl.isUdpChannel(null);
Object clazz = ByteToMessageDecoder.class;
clazz = IdleStateHandler.class;
try {
// this class is a private, inner class to IdleStateHandler...
clazz = Class.forName("io.netty.handler.timeout.IdleStateHandler$AbstractIdleTask");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
clazz = DefaultNameResolver.class;
clazz = InetSocketAddressResolver.class;
}
public static
String getHostDetails(final SocketAddress socketAddress) {
StringBuilder builder = new StringBuilder();
@ -122,44 +117,20 @@ class EndPoint extends Shutdownable {
}
}
/**
* Defines if we are allowed to use the native OS-specific network interface (non-native to java) for boosted networking performance.
*/
@Property
public static boolean enableNativeLibrary = false;
public static final String LOCAL_CHANNEL = "local_channel";
/**
* The default size for UDP packets is 768 bytes.
* <p/>
* You could increase or decrease this value to avoid truncated packets
* or to improve memory footprint respectively.
* <p/>
* Please also note that a large UDP packet might be truncated or
* dropped by your router no matter how you configured this option.
* In UDP, a packet is truncated or dropped if it is larger than a
* certain size, depending on router configuration. IPv4 routers
* truncate and IPv6 routers drop a large packet. That's why it is
* safe to send small packets in UDP.
* <p/>
* To fit into that magic 576-byte MTU and avoid fragmentation, your
* UDP payload should be restricted by 576-60-8=508 bytes.
*
* This can be set higher on an internal lan!
*
* DON'T go higher that 1400 over the internet, but 9k is possible
* with jumbo frames on a local network (if it's supported)
*/
@Property
public static int udpMaxSize = 508;
protected final org.slf4j.Logger logger;
protected final Configuration config;
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;
// protected final RegistrationWrapper registrationWrapper;
final ECPrivateKeyParameters privateKey;
final ECPublicKeyParameters publicKey;
@ -175,32 +146,142 @@ class EndPoint extends Shutdownable {
SettingsStore propertyStore;
boolean disableRemoteKeyValidation;
/**
* in milliseconds. default is disabled!
*/
private volatile int idleTimeoutMs = 0;
// 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
*/
public
EndPoint(Class<? extends EndPoint> type, final Configuration config) throws SecurityException {
super(type);
this.config = config;
private
EndPoint(Class<?> type, final Configuration config) throws SecurityException, IOException {
this.type = type;
// make sure that 'localhost' is ALWAYS our specific loopback IP address
if (config.host != null && (config.host.equals("localhost") || config.host.startsWith("127."))) {
// localhost IP might not always be 127.0.0.1
config.host = NetUtil.LOCALHOST.getHostAddress();
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;
@ -215,11 +296,11 @@ class EndPoint extends Shutdownable {
// 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);
}
// 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!
@ -318,44 +399,6 @@ class EndPoint extends Shutdownable {
return (S) propertyStore;
}
/**
* 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)
*/
protected
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.
*/
protected
void startNextProtocolRegistration() {
}
/**
* The amount of milli-seconds that must elapse with no read or write before {@link Listener.OnIdle#idle(Connection)} }
* will be triggered
*/
public
int getIdleTimeout() {
return idleTimeoutMs;
}
/**
* The {@link Listener:idle()} will be triggered when neither read nor write
* has happened for the specified period of time (in milli-seconds)
* <br>
* Specify {@code 0} to disable (default).
*/
public
void setIdleTimeout(int idleTimeoutMs) {
this.idleTimeoutMs = idleTimeoutMs;
}
/**
* Returns the serialization wrapper if there is an object type that needs to be added outside of the basics.
*/
@ -444,19 +487,6 @@ class EndPoint extends Shutdownable {
return connectionManager.getConnections();
}
/**
* Expose methods to send objects to a destination.
*/
public abstract
ConnectionBridgeBase send();
/**
* 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 #send()}, followed by the protocol you wish to use.
*/
public abstract
ConnectionPoint send(final Object message);
/**
* Closes all connections ONLY (keeps the server/client running). To STOP the client/server, use stop().
* <p/>
@ -469,29 +499,47 @@ class EndPoint extends Shutdownable {
}
/**
* Starts the shutdown process during JVM shutdown, if necessary.
* </p>
* By default, we always can shutdown via the JVM shutdown hook.
* Creates a "global" RMI object for use by multiple connections.
*
* @return the ID assigned to this RMI object
*/
@Override
protected
boolean shouldShutdownHookRun() {
// connectionManager.shutdown accurately reflects the state of the app. Safe to use here
return (connectionManager != null && !connectionManager.shutdown.get());
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
protected
void shutdownChannelsPre() {
// this does a closeConnections + clear_listeners
connectionManager.stop();
public
String toString() {
return "EndPoint [" + getName() + "]";
}
@Override
protected
void stopExtraActionsInternal() {
// shutdown the database store
propertyStore.close();
/**
* @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
@ -537,26 +585,17 @@ class EndPoint extends Shutdownable {
return true;
}
/**
* 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);
}
@Override
public void close() {
if (aeron != null) {
aeron.close();
}
if (mediaDriver != null) {
mediaDriver.close();
}
/**
* 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);
if (propertyStore != null) {
propertyStore.close();
}
}
}

View File

@ -20,21 +20,16 @@ import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
import dorkbox.network.Client;
import dorkbox.network.Configuration;
import dorkbox.network.ClientConfiguration;
import dorkbox.network.connection.bridge.ConnectionBridge;
import dorkbox.util.exceptions.SecurityException;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
/**
* This serves the purpose of making sure that specific methods are not available to the end user.
*/
public
class EndPointClient extends EndPoint {
class EndPointClient extends EndPoint<ClientConfiguration> {
// is valid when there is a connection to the server, otherwise it is null
protected volatile Connection connection;
@ -52,144 +47,11 @@ class EndPointClient extends EndPoint {
private volatile ConnectionBridge connectionBridgeFlushAlways;
public
EndPointClient(Configuration config) throws SecurityException {
super(Client.class, config);
}
/**
* Internal call by the pipeline to start the client registering the different session protocols.
*/
protected
void startRegistration() throws IOException {
// make sure we WAIT 100ms BEFORE starting registrations again IF we recently called close...
while (previousClosedConnectionActivity > 0 &&
System.nanoTime() - previousClosedConnectionActivity < TimeUnit.MILLISECONDS.toNanos(200)) {
LockSupport.parkNanos(100L); // wait
}
synchronized (bootstrapLock) {
// always reset everything.
registration = new CountDownLatch(1);
bootstrapIterator = bootstraps.iterator();
doRegistration();
}
// have to BLOCK (must be outside of the synchronize call), we don't want the client to run before registration is complete
try {
if (connectionTimeout > 0) {
if (!registration.await(connectionTimeout, TimeUnit.MILLISECONDS)) {
closeConnection();
throw new IOException("Unable to complete registration within '" + connectionTimeout + "' milliseconds");
}
}
else {
registration.await();
}
} catch (InterruptedException e) {
if (connectionTimeout > 0) {
throw new IOException("Unable to complete registration within '" + connectionTimeout + "' milliseconds", e);
}
else {
throw new IOException("Unable to complete registration.", e);
}
}
EndPointClient(ClientConfiguration config) throws SecurityException, IOException {
super(config);
}
/**
* Internal call by the pipeline to notify the client to continue registering the different session protocols.
*
* @return true if we are done registering bootstraps
*/
@Override
protected
void startNextProtocolRegistration() {
logger.trace("Registered protocol from server.");
synchronized (bootstrapLock) {
if (hasMoreRegistrations()) {
doRegistration();
}
}
}
/**
* 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)
*/
@Override
protected
boolean hasMoreRegistrations() {
synchronized (bootstrapLock) {
return !(bootstrapIterator == null || !bootstrapIterator.hasNext());
}
}
/**
* this is called by 2 threads. The startup thread, and the registration-in-progress thread
*
* NOTE: must be inside synchronize(bootstrapLock)!
*/
private
void doRegistration() {
BootstrapWrapper bootstrapWrapper = bootstrapIterator.next();
ChannelFuture future;
if (connectionTimeout != 0) {
// must be before connect
bootstrapWrapper.bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectionTimeout);
}
try {
// UDP : When this is CONNECT, a udp socket will ONLY accept UDP traffic from the remote address (ip/port combo).
// If the reply isn't from the correct port, then the other end will receive a "Port Unreachable" exception.
future = bootstrapWrapper.bootstrap.connect();
future.await(connectionTimeout);
} catch (Exception e) {
String errorMessage =
"Could not connect to the " + bootstrapWrapper.type + " server at " + bootstrapWrapper.address + " on port: " +
bootstrapWrapper.port;
if (logger.isDebugEnabled()) {
// extra info if debug is enabled
logger.error(errorMessage, e);
}
else {
logger.error(errorMessage);
}
return;
}
if (!future.isSuccess()) {
Throwable cause = future.cause();
// extra space here is so it aligns with "Connecting to server:"
String errorMessage = "Connection refused :" + bootstrapWrapper.address + " at " + bootstrapWrapper.type + " port: " +
bootstrapWrapper.port;
if (cause instanceof java.net.ConnectException) {
if (cause.getMessage()
.contains("refused")) {
logger.error(errorMessage);
}
}
else {
logger.error(errorMessage, cause);
}
return;
}
logger.trace("Waiting for registration from server.");
manageForShutdown(future);
}
/**
* Internal (to the networking stack) to notify the client that registration has COMPLETED. This is necessary because the client
@ -265,30 +127,7 @@ class EndPointClient extends EndPoint {
}
}
/**
* AFTER registration is complete, if we are UDP only -- setup a heartbeat (must be the larger of 2x the idle timeout OR 10 seconds)
*
* If the server disconnects because of a heartbeat failure, the client has to be made aware of this when it tries to send data again
* (and it must go through it's entire reconnect protocol)
*/
protected
void startUdpHeartbeat() {
// TODO...
}
/**
* Expose methods to send objects to a destination.
* <p/>
* This returns a bridge that will flush after EVERY send! This is because sending data can occur on the client, outside
* of the normal eventloop patterns, and it is confusing to the user to have to manually flush the channel each time.
*/
@Override
public
ConnectionBridge send() {
return connectionBridgeFlushAlways;
}
@Override
public
ConnectionPoint send(final Object message) {
ConnectionPoint send = connection.send(message);
@ -300,38 +139,58 @@ class EndPointClient extends EndPoint {
return send;
}
/**
* 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();
}
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.
*/
@ -339,13 +198,13 @@ class EndPointClient extends EndPoint {
// make sure we're not waiting on registration
stopRegistration();
}
@Override
protected
void shutdownChannelsPre() {
closeConnection();
// this calls connectionManager.stop()
super.shutdownChannelsPre();
}
//
// @Override
// protected
// void shutdownChannelsPre() {
// closeConnection();
//
// // this calls connectionManager.stop()
// super.shutdownChannelsPre();
// }
}

View File

@ -15,25 +15,24 @@
*/
package dorkbox.network.connection;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.concurrent.CopyOnWriteArrayList;
import dorkbox.network.Configuration;
import dorkbox.network.Server;
import dorkbox.network.connection.bridge.ConnectionBridgeServer;
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;
import io.netty.handler.ipfilter.IpFilterRule;
import io.netty.handler.ipfilter.IpFilterRuleType;
import io.netty.util.NetUtil;
/**
* This serves the purpose of making sure that specific methods are not available to the end user.
*/
public
class EndPointServer extends EndPoint {
class EndPointServer extends EndPoint<ServerConfiguration> {
/**
* Maintains a thread-safe collection of rules to allow/deny connectivity to this server.
@ -46,30 +45,35 @@ class EndPointServer extends EndPoint {
protected final CopyOnWriteArrayList<ConnectionRule> connectionRules = new CopyOnWriteArrayList<>();
public
EndPointServer(final Configuration config) throws SecurityException {
super(Server.class, config);
EndPointServer(final ServerConfiguration config) throws SecurityException, IOException {
super(config);
}
/**
* Expose methods to send objects to a destination.
* Safely sends objects to a destination
*/
@Override
public
ConnectionBridgeServer send() {
return this.connectionManager;
}
/**
* 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 #send()}, followed by the protocol you wish to use.
*/
@Override
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.
@ -124,16 +128,6 @@ class EndPointServer extends EndPoint {
connectionManager.removeConnection(connection);
}
@Override
protected
void shutdownChannelsPre() {
// Sometimes there might be "lingering" connections (ie, halfway though registration) that need to be closed.
registrationWrapper.clearSessions();
// this calls connectionManager.stop()
super.shutdownChannelsPre();
}
// 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) {

View File

@ -49,7 +49,7 @@ import net.jpountz.lz4.LZ4FastDecompressor;
@SuppressWarnings("Duplicates")
public
class KryoExtra extends Kryo {
private static final int ABSOLUTE_MAX_SIZE_OBJECT = EndPoint.udpMaxSize * 1000; // by default, this is about 500k
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();

View File

@ -15,8 +15,6 @@
*/
package dorkbox.network.connection;
import java.io.IOException;
public
interface Listener {
/**
@ -61,17 +59,6 @@ interface Listener {
}
/**
* Called when the connection is idle for longer than the {@link EndPoint#setIdleTimeout(int)} idle threshold.
*/
interface OnIdle<C extends Connection> extends Listener {
/**
* Called when the connection is idle for longer than the {@link EndPoint#setIdleTimeout(int)} idle threshold.
*/
void idle(C connection) throws IOException;
}
/**
* 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.

View File

@ -112,7 +112,7 @@ class RegistrationWrapper {
*/
public
int getIdleTimeout() {
return this.endPoint.getIdleTimeout();
return 5;
}
/**

View File

@ -57,7 +57,7 @@ class RegistrationWrapperClient extends RegistrationWrapper {
*/
public
boolean hasMoreRegistrations() {
return this.endPoint.hasMoreRegistrations();
return false;
}
/**
@ -66,7 +66,6 @@ class RegistrationWrapperClient extends RegistrationWrapper {
*/
public
void startNextProtocolRegistration() {
this.endPoint.startNextProtocolRegistration();
}
public

View File

@ -14,12 +14,6 @@
*/
package dorkbox.network.connection;
import static dorkbox.network.pipeline.ConnectionType.EPOLL;
import static dorkbox.network.pipeline.ConnectionType.KQUEUE;
import static dorkbox.network.pipeline.ConnectionType.NIO;
import static dorkbox.network.pipeline.ConnectionType.OIO;
import java.security.AccessControlException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
@ -29,53 +23,22 @@ import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import dorkbox.network.NativeLibrary;
import dorkbox.network.pipeline.ConnectionType;
import dorkbox.util.NamedThreadFactory;
import dorkbox.util.OS;
import dorkbox.util.Property;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.DefaultEventLoopGroup;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.kqueue.KQueueEventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.oio.OioEventLoopGroup;
import io.netty.util.concurrent.EventExecutor;
import io.netty.util.concurrent.Future;
import io.netty.util.internal.PlatformDependent;
/**
* This is the highest level endpoint, for lifecycle support/management.
*/
public
class Shutdownable {
static {
//noinspection Duplicates
try {
// doesn't work when running from inside eclipse.
// Needed for NIO selectors on Android 2.2, and to force IPv4.
System.setProperty("java.net.preferIPv4Stack", Boolean.TRUE.toString());
System.setProperty("java.net.preferIPv6Addresses", Boolean.FALSE.toString());
// java6 has stack overflow problems when loading certain classes in it's classloader. The result is a StackOverflow when
// loading them normally. This calls AND FIXES this issue.
if (OS.javaVersion == 6) {
if (PlatformDependent.hasUnsafe()) {
//noinspection ResultOfMethodCallIgnored
PlatformDependent.newFixedMpscQueue(8);
}
}
} catch (AccessControlException ignored) {
}
}
protected static final String shutdownHookName = "::SHUTDOWN_HOOK::";
protected static final String stopTreadName = "::STOP_THREAD::";
public static final String THREADGROUP_NAME = "(Netty)";
/**
* The HIGH and LOW watermark points for connections
@ -91,18 +54,6 @@ class Shutdownable {
@Property
public static long maxShutdownWaitTimeInMilliSeconds = 2000L; // in milliseconds
/**
* Checks to see if we are running in the netty thread. This is (usually) to prevent potential deadlocks in code that CANNOT be run from
* inside a netty worker.
*/
public static
boolean isNettyThread() {
return Thread.currentThread()
.getThreadGroup()
.getName()
.contains(THREADGROUP_NAME);
}
/**
* Runs a runnable inside a NEW thread that is NOT in the same thread group as Netty
*/
@ -146,7 +97,7 @@ class Shutdownable {
threadGroup = new ThreadGroup(s != null
? s.getThreadGroup()
: Thread.currentThread()
.getThreadGroup(), type.getSimpleName() + " " + THREADGROUP_NAME);
.getThreadGroup(), type.getSimpleName());
threadGroup.setDaemon(true);
logger = org.slf4j.LoggerFactory.getLogger(type.getSimpleName());
@ -271,72 +222,6 @@ class Shutdownable {
}
/**
* Creates a new event loop based on the OS type and specified configuration
*
* @param threadCount number of threads for the event loop
*
* @return a new event loop group based on the specified parameters
*/
protected
EventLoopGroup newEventLoop(final int threadCount, final String threadName) {
if (OS.isAndroid()) {
// android ONLY supports OIO
return newEventLoop(OIO, threadCount, threadName);
}
else if (OS.isLinux() && NativeLibrary.isAvailable()) {
// epoll network stack is MUCH faster (but only on linux)
return newEventLoop(EPOLL, threadCount, threadName);
}
else if (OS.isMacOsX() && NativeLibrary.isAvailable()) {
// KQueue network stack is MUCH faster (but only on macosx)
return newEventLoop(KQUEUE, threadCount, threadName);
}
else {
return newEventLoop(NIO, threadCount, threadName);
}
}
/**
* Creates a new event loop based on the specified configuration
*
* @param connectionType LOCAL, NIO, EPOLL, etc...
* @param threadCount number of threads for the event loop
*
* @return a new event loop group based on the specified parameters
*/
protected
EventLoopGroup newEventLoop(final ConnectionType connectionType, final int threadCount, final String threadName) {
NamedThreadFactory threadFactory = new NamedThreadFactory(threadName, threadGroup);
EventLoopGroup group;
switch (connectionType) {
case LOCAL:
group = new DefaultEventLoopGroup(threadCount, threadFactory);
break;
case OIO:
group = new OioEventLoopGroup(threadCount, threadFactory);
break;
case NIO:
group = new NioEventLoopGroup(threadCount, threadFactory);
break;
case EPOLL:
group = new EpollEventLoopGroup(threadCount, threadFactory);
break;
case KQUEUE:
group = new KQueueEventLoopGroup(threadCount, threadFactory);
break;
default:
group = new DefaultEventLoopGroup(threadCount, threadFactory);
break;
}
manageForShutdown(group);
return group;
}
/**
* Check to see if the current thread is running from it's OWN thread, or from Netty... This is used to prevent deadlocks.
*

View File

@ -1,29 +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.idle;
public
interface IdleBridge {
/**
* Sends the object over the network using TCP (or via LOCAL when it's a local channel) when the socket is in an "idle" state.
*/
void TCP();
/**
* Sends the object over the network using UDP when the socket is in an "idle" state.
*/
void UDP();
}

View File

@ -1,27 +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.idle;
import dorkbox.network.connection.Connection;
import dorkbox.network.connection.Listener;
public
interface IdleListener<C extends Connection, M> extends Listener {
/**
* used by the Idle Sender
*/
void send(C connection, M message);
}

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.connection.idle;
import dorkbox.network.connection.Connection;
public
class IdleListenerTCP<C extends Connection, M> implements IdleListener<C, M> {
/**
* used by the Idle Sender
*/
public
IdleListenerTCP() {
}
/**
* used by the Idle Sender
*/
@Override
public
void send(C connection, M message) {
connection.send()
.TCP(message);
}
}

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.idle;
import dorkbox.network.connection.Connection;
public
class IdleObjectSender<C extends Connection, M> extends IdleSender<C, M> {
private final M message;
public
IdleObjectSender(final IdleListener<C, M> idleListener, M message) {
super(idleListener);
this.message = message;
}
@Override
public
void idle(C connection) {
if (!this.started) {
this.started = true;
start();
}
connection.listeners()
.remove(this);
this.idleListener.send(connection, this.message);
}
@Override
protected
M next() {
return null;
}
}

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.idle;
import dorkbox.network.connection.Connection;
import dorkbox.network.connection.Listener;
import java.io.IOException;
public abstract
class IdleSender<C extends Connection, M> implements Listener.OnIdle<C> {
final IdleListener<C, M> idleListener;
volatile boolean started;
public
IdleSender(final IdleListener<C, M> idleListener) {
this.idleListener = idleListener;
}
@Override
public
void idle(C connection) throws IOException {
if (!this.started) {
this.started = true;
start();
}
M message = next();
if (message == null) {
connection.listeners()
.remove(this);
}
else {
this.idleListener.send(connection, message);
}
}
/**
* Called once, before the first send. Subclasses can override this method to send something so the receiving side expects subsequent
* objects.
*/
protected
void start() {
}
/**
* Returns the next object to send, or null if no more objects will be sent.
*/
protected abstract
M next() throws IOException;
}

View File

@ -1,58 +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.idle;
import dorkbox.network.connection.Connection;
import dorkbox.network.connection.ConnectionImpl;
@SuppressWarnings({"unchecked", "rawtypes"})
public
class IdleSenderFactory<C extends Connection, M> implements IdleBridge {
private final ConnectionImpl connection;
private final Object message;
public
IdleSenderFactory(final ConnectionImpl connection, final Object message) {
this.connection = connection;
this.message = message;
}
@Override
public
void TCP() {
if (message instanceof IdleSender) {
connection.listeners()
.add((IdleSender) message);
}
else {
connection.listeners()
.add(new IdleObjectSender(new IdleListenerTCP<C, M>(), message));
}
}
@Override
public
void UDP() {
if (message instanceof IdleSender) {
connection.listeners()
.add((IdleSender) message);
}
else {
connection.listeners()
.add(new IdleObjectSender(new IdleListenerUDP<C, M>(), message));
}
}
}

View File

@ -1,57 +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.idle;
import dorkbox.network.connection.Connection;
import java.io.IOException;
import java.io.InputStream;
public abstract
class InputStreamSender<C extends Connection> extends IdleSender<C, byte[]> {
private final InputStream input;
private final byte[] chunk;
public
InputStreamSender(final IdleListener<C, byte[]> idleListener, InputStream input, int chunkSize) {
super(idleListener);
this.input = input;
this.chunk = new byte[chunkSize];
}
@Override
protected final
byte[] next() throws IOException {
int total = 0;
while (total < this.chunk.length) {
int count = this.input.read(this.chunk, total, this.chunk.length - total);
if (count < 0) {
if (total == 0) {
return null;
}
byte[] partial = new byte[total];
System.arraycopy(this.chunk, 0, partial, 0, total);
return onNext(partial);
}
total += count;
}
return onNext(this.chunk);
}
protected abstract
byte[] onNext(byte[] chunk);
}

View File

@ -22,7 +22,6 @@ import javax.crypto.spec.SecretKeySpec;
import dorkbox.network.connection.ConnectionImpl;
import dorkbox.network.connection.ConnectionPoint;
import dorkbox.network.connection.EndPoint;
import dorkbox.network.connection.ISessionManager;
import dorkbox.network.connection.registration.MetaChannel;
import io.netty.channel.Channel;
@ -117,13 +116,13 @@ class ChannelLocalWrapper implements ChannelWrapper, ConnectionPoint {
@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);
// 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

View File

@ -21,10 +21,8 @@ import javax.crypto.SecretKey;
import dorkbox.network.connection.ConnectionImpl;
import dorkbox.network.connection.ConnectionPoint;
import dorkbox.network.connection.EndPoint;
import dorkbox.network.connection.ISessionManager;
import dorkbox.network.connection.registration.MetaChannel;
import io.netty.bootstrap.DatagramCloseMessage;
import io.netty.util.NetUtil;
public
@ -126,32 +124,32 @@ class ChannelNetworkWrapper implements ChannelWrapper {
@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();
// 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

View File

@ -0,0 +1,36 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you 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.ipFilter;
import java.net.InetSocketAddress;
/**
* Implement this interface to create new rules.
*/
public interface IpFilterRule {
/**
* @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 IpFilterRule#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.
*/
IpFilterRuleType ruleType();
}

View File

@ -0,0 +1,24 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you 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.ipFilter;
/**
* Used in {@link IpFilterRule} to decide if a matching IP Address should be allowed or denied to connect.
*/
public enum IpFilterRuleType {
ACCEPT,
REJECT
}

View File

@ -0,0 +1,178 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you 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.ipFilter;
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.handler.ipfilter.RuleBasedIpFilter;
import io.netty.util.internal.ObjectUtil;
import io.netty.util.internal.SocketUtils;
/**
* Use this class to create rules for {@link RuleBasedIpFilter} that group IP addresses into subnets.
* Supports both, IPv4 and IPv6.
*/
public final class IpSubnetFilterRule implements IpFilterRule {
private final IpFilterRule filterRule;
public IpSubnetFilterRule(String ipAddress, int cidrPrefix, IpFilterRuleType ruleType) {
try {
filterRule = selectFilterRule(SocketUtils.addressByName(ipAddress), cidrPrefix, ruleType);
} catch (UnknownHostException e) {
throw new IllegalArgumentException("ipAddress", e);
}
}
public IpSubnetFilterRule(InetAddress ipAddress, int cidrPrefix, IpFilterRuleType ruleType) {
filterRule = selectFilterRule(ipAddress, cidrPrefix, ruleType);
}
private static
IpFilterRule selectFilterRule(InetAddress ipAddress, int cidrPrefix, IpFilterRuleType ruleType) {
ObjectUtil.checkNotNull(ipAddress, "ipAddress");
ObjectUtil.checkNotNull(ruleType, "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");
}
}
@Override
public boolean matches(InetSocketAddress remoteAddress) {
return filterRule.matches(remoteAddress);
}
@Override
public
IpFilterRuleType ruleType() {
return filterRule.ruleType();
}
private static final class Ip4SubnetFilterRule implements IpFilterRule {
private final int networkAddress;
private final int subnetMask;
private final IpFilterRuleType ruleType;
private Ip4SubnetFilterRule(Inet4Address ipAddress, int cidrPrefix, IpFilterRuleType 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
IpFilterRuleType 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 IpFilterRule {
private static final BigInteger MINUS_ONE = BigInteger.valueOf(-1);
private final BigInteger networkAddress;
private final BigInteger subnetMask;
private final IpFilterRuleType ruleType;
private Ip6SubnetFilterRule(Inet6Address ipAddress, int cidrPrefix, IpFilterRuleType 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
IpFilterRuleType 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

@ -21,7 +21,6 @@ import java.util.List;
import org.slf4j.LoggerFactory;
import dorkbox.network.connection.EndPoint;
import dorkbox.network.serialization.NetworkSerializationManager;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler.Sharable;
@ -33,7 +32,7 @@ import io.netty.handler.codec.MessageToMessageEncoder;
public
class KryoEncoderUdp extends MessageToMessageEncoder<Object> {
private static final int maxSize = EndPoint.udpMaxSize;
private static final int maxSize = 1000;
private final NetworkSerializationManager serializationManager;

View File

@ -20,9 +20,8 @@ import dorkbox.network.connection.ConnectionPoint;
import dorkbox.network.connection.Connection_;
import dorkbox.network.connection.EndPoint;
import dorkbox.network.connection.Listeners;
import dorkbox.network.connection.bridge.ConnectionBridge;
import dorkbox.network.connection.idle.IdleBridge;
import dorkbox.network.connection.idle.IdleSender;
import dorkbox.network.connection.Ping;
public
class RmiNopConnection implements Connection_ {
@ -44,6 +43,18 @@ class RmiNopConnection implements Connection_ {
return false;
}
@Override
public
boolean isIPC() {
return false;
}
@Override
public
boolean isNetwork() {
return false;
}
@Override
public
EndPoint getEndPoint() {
@ -62,18 +73,6 @@ class RmiNopConnection implements Connection_ {
return null;
}
@Override
public
boolean hasUDP() {
return false;
}
@Override
public
ConnectionBridge send() {
return null;
}
@Override
public
ConnectionPoint send(final Object message) {
@ -82,16 +81,29 @@ class RmiNopConnection implements Connection_ {
@Override
public
IdleBridge sendOnIdle(final IdleSender<?, ?> sender) {
ConnectionPoint send(final Object message, final byte priority) {
return null;
}
@Override
public
IdleBridge sendOnIdle(final Object message) {
ConnectionPoint sendUnreliable(final Object message) {
return null;
}
@Override
public
ConnectionPoint sendUnreliable(final Object message, final byte priority) {
return null;
}
@Override
public
Ping ping() {
return null;
}
@Override
public
Listeners listeners() {
@ -104,12 +116,6 @@ class RmiNopConnection implements Connection_ {
}
@Override
public
void closeAsap() {
}
@Override
public
<Iface> void createRemoteObject(final Class<Iface> interfaceClass, final RemoteObjectCallback<Iface> callback) {

View File

@ -0,0 +1,20 @@
package dorkbox.network.serialization;
/**
* Signals the remote end that certain things need to happen
*/
public
class ControlMessage {
public static final byte INVALID_STATUS = 0x0;
public static final byte CONNECTING = 0x2;
public static final byte CONNECTED = 0x3;
public static final byte DISCONNECT = 0x7F; // max signed byte value, 127
public byte command = INVALID_STATUS;
public byte payload = INVALID_STATUS;
public ControlMessage() {
}
}

View File

@ -65,7 +65,6 @@ import dorkbox.util.serialization.EccPrivateKeySerializer;
import dorkbox.util.serialization.EccPublicKeySerializer;
import dorkbox.util.serialization.IesParametersSerializer;
import dorkbox.util.serialization.IesWithCipherParametersSerializer;
import io.netty.bootstrap.DatagramCloseMessage;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
@ -118,8 +117,8 @@ class Serialization implements NetworkSerializationManager {
registrationRequired,
factory);
serialization.register(PingMessage.class);
serialization.register(DatagramCloseMessage.class);
serialization.register(ControlMessage.class);
serialization.register(PingMessage.class); // TODO this is built into aeron!
serialization.register(byte[].class);
serialization.register(IESParameters.class, new IesParametersSerializer());

View File

@ -15,6 +15,8 @@
*/
package dorkbox.network.store;
import java.io.Closeable;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.slf4j.Logger;
@ -31,7 +33,7 @@ import dorkbox.util.storage.Storage;
*/
@SuppressWarnings({"deprecation", "unused", "Duplicates"})
public abstract
class SettingsStore {
class SettingsStore implements Closeable {
/**
* Initialize the settingsStore with the provided serialization manager.
@ -339,6 +341,7 @@ class SettingsStore {
/**
* Take the proper steps to close the storage system.
*/
@Override
public abstract
void close();
}

View File

@ -1,22 +0,0 @@
/*
* Copyright 2018 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 io.netty.bootstrap;
/**
* Used as a hint to close remote UDP connections
*/
public
class DatagramCloseMessage {}

View File

@ -1,209 +0,0 @@
/*
* Copyright 2018 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 io.netty.bootstrap;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import io.netty.channel.AbstractChannel;
import io.netty.channel.Channel;
import io.netty.channel.ChannelConfig;
import io.netty.channel.ChannelMetadata;
import io.netty.channel.ChannelOutboundBuffer;
import io.netty.channel.ChannelPromise;
import io.netty.channel.EventLoop;
import io.netty.channel.socket.DatagramPacket;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.internal.RecyclableArrayList;
public
class DatagramSessionChannel extends AbstractChannel implements Channel {
private
class ChannelUnsafe extends AbstractUnsafe {
@Override
public
void connect(SocketAddress socketAddress, SocketAddress socketAddress2, ChannelPromise channelPromise) {
// Connect not supported by ServerChannel implementations
channelPromise.setFailure(new UnsupportedOperationException());
}
}
private static final ChannelMetadata METADATA = new ChannelMetadata(false, 16);
private final DatagramSessionChannelConfig config;
private SessionManager sessionManager;
private InetSocketAddress localAddress;
private InetSocketAddress remoteAddress;
private volatile boolean isOpen = true;
DatagramSessionChannel(final Channel parentChannel,
final SessionManager sessionManager,
final DatagramSessionChannelConfig sessionConfig,
final InetSocketAddress localAddress,
final InetSocketAddress remoteAddress) {
super(parentChannel);
this.sessionManager = sessionManager;
this.config = sessionConfig;
this.localAddress = localAddress;
this.remoteAddress = remoteAddress;
}
@Override
public
ChannelConfig config() {
return config;
}
@Override
protected
void doBeginRead() throws Exception {
}
@Override
protected
void doBind(SocketAddress addr) throws Exception {
throw new UnsupportedOperationException();
}
@Override
protected
void doClose() throws Exception {
isOpen = false;
sessionManager.doCloseChannel(this);
}
@Override
protected
void doDisconnect() throws Exception {
doClose();
}
@Override
protected
void doWrite(ChannelOutboundBuffer buffer) throws Exception {
//transfer all messages that are ready to be written to list
final RecyclableArrayList list = RecyclableArrayList.newInstance();
boolean free = true;
try {
DatagramPacket buf = null;
while ((buf = (DatagramPacket) buffer.current()) != null) {
list.add(buf.retain());
buffer.remove();
}
free = false;
} finally {
if (free) {
for (Object obj : list) {
ReferenceCountUtil.safeRelease(obj);
}
list.recycle();
}
}
//schedule a task that will write those entries
EventLoop eventLoop = parent().eventLoop();
if (eventLoop.inEventLoop()) {
write0(list);
}
else {
eventLoop.submit(new Runnable() {
@Override
public
void run() {
write0(list);
}
});
}
}
@Override
public
boolean isActive() {
return isOpen;
}
@Override
protected
boolean isCompatible(EventLoop eventloop) {
// compatible with all Datagram event loops where we are explicitly used
return true;
}
@Override
public
boolean isOpen() {
return isOpen;
}
@Override
public
InetSocketAddress localAddress() {
return (InetSocketAddress) localAddress0();
}
@Override
protected
SocketAddress localAddress0() {
return localAddress;
}
@Override
public
ChannelMetadata metadata() {
return METADATA;
}
@Override
protected
AbstractUnsafe newUnsafe() {
// cannot connect, so we make this be an error if we try.
return new ChannelUnsafe();
}
@Override
public
InetSocketAddress remoteAddress() {
return remoteAddress;
}
@Override
protected
InetSocketAddress remoteAddress0() {
return remoteAddress;
}
private
void write0(final RecyclableArrayList list) {
try {
Unsafe unsafe = super.parent().unsafe();
for (Object buf : list) {
unsafe.write(buf, voidPromise());
}
unsafe.flush();
} finally {
list.recycle();
}
}
}

View File

@ -1,207 +0,0 @@
/*
* Copyright 2018 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 io.netty.bootstrap;
import java.util.Map;
import dorkbox.network.connection.EndPoint;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.Channel;
import io.netty.channel.ChannelConfig;
import io.netty.channel.ChannelOption;
import io.netty.channel.DefaultMessageSizeEstimator;
import io.netty.channel.MessageSizeEstimator;
import io.netty.channel.RecvByteBufAllocator;
import io.netty.channel.WriteBufferWaterMark;
import io.netty.channel.socket.DatagramChannelConfig;
/**
* The default {@link DatagramChannelConfig} implementation.
*/
public class DatagramSessionChannelConfig implements ChannelConfig {
private static final MessageSizeEstimator DEFAULT_MSG_SIZE_ESTIMATOR = DefaultMessageSizeEstimator.DEFAULT;
private volatile MessageSizeEstimator msgSizeEstimator = DEFAULT_MSG_SIZE_ESTIMATOR;
private final Channel channel;
/**
* Creates a new instance.
*/
public
DatagramSessionChannelConfig(Channel channel) {
this.channel = channel;
}
@Override
public
Map<ChannelOption<?>, Object> getOptions() {
return null;
}
@Override
public
boolean setOptions(final Map<ChannelOption<?>, ?> options) {
return false;
}
@Override
public
<T> T getOption(final ChannelOption<T> option) {
return channel.config().getOption(option);
}
@Override
public
<T> boolean setOption(final ChannelOption<T> option, final T value) {
return false;
}
@Override
public
int getConnectTimeoutMillis() {
return 0;
}
@Override
public
ChannelConfig setConnectTimeoutMillis(final int connectTimeoutMillis) {
return this;
}
@Override
public
int getMaxMessagesPerRead() {
return 0;
}
@Override
public
ChannelConfig setMaxMessagesPerRead(final int maxMessagesPerRead) {
return this;
}
@Override
public
int getWriteSpinCount() {
return 0;
}
@Override
public
ChannelConfig setWriteSpinCount(final int writeSpinCount) {
return this;
}
@Override
public
ByteBufAllocator getAllocator() {
return channel.config().getAllocator();
}
@Override
public
ChannelConfig setAllocator(final ByteBufAllocator allocator) {
return this;
}
@Override
public
<T extends RecvByteBufAllocator> T getRecvByteBufAllocator() {
return channel.config().getRecvByteBufAllocator();
}
@Override
public
ChannelConfig setRecvByteBufAllocator(final RecvByteBufAllocator allocator) {
return this;
}
@Override
public
boolean isAutoRead() {
// we implement our own reading from within the DatagramServer context.
return false;
}
@Override
public
ChannelConfig setAutoRead(final boolean autoRead) {
return this;
}
@Override
public
boolean isAutoClose() {
return false;
}
@Override
public
ChannelConfig setAutoClose(final boolean autoClose) {
return this;
}
@Override
public
int getWriteBufferHighWaterMark() {
return EndPoint.udpMaxSize;
}
@Override
public
ChannelConfig setWriteBufferHighWaterMark(final int writeBufferHighWaterMark) {
return this;
}
@Override
public
int getWriteBufferLowWaterMark() {
return 0;
}
@Override
public
ChannelConfig setWriteBufferLowWaterMark(final int writeBufferLowWaterMark) {
return this;
}
@Override
public
MessageSizeEstimator getMessageSizeEstimator() {
return msgSizeEstimator;
}
@Override
public
ChannelConfig setMessageSizeEstimator(final MessageSizeEstimator estimator) {
this.msgSizeEstimator = estimator;
return this;
}
@Override
public
WriteBufferWaterMark getWriteBufferWaterMark() {
return null;
}
@Override
public
ChannelConfig setWriteBufferWaterMark(final WriteBufferWaterMark writeBufferWaterMark) {
return this;
}
}

View File

@ -1,287 +0,0 @@
/*
* Copyright 2018 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 io.netty.bootstrap;
import java.net.SocketAddress;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.resolver.AddressResolverGroup;
import io.netty.resolver.DefaultAddressResolverGroup;
import io.netty.util.AttributeKey;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
/**
* VERY similar to the ServerBootstrap class, with the change to having a "SessionManager" added to the pipeline on init (instead of
* the ServerBootstrapAcceptor getting added)
*/
public
class SessionBootstrap extends AbstractBootstrap<SessionBootstrap, Channel> {
private static final AddressResolverGroup<?> DEFAULT_RESOLVER = DefaultAddressResolverGroup.INSTANCE;
private static final InternalLogger logger = InternalLoggerFactory.getInstance(SessionBootstrap.class);
@SuppressWarnings("unchecked")
private static
Entry<AttributeKey<?>, Object>[] newAttrArray(int size) {
return new Entry[size];
}
@SuppressWarnings("unchecked")
private static
Map.Entry<ChannelOption<?>, Object>[] newOptionArray(int size) {
return new Map.Entry[size];
}
private final Map<ChannelOption<?>, Object> childOptions = new LinkedHashMap<ChannelOption<?>, Object>();
private final Map<AttributeKey<?>, Object> childAttrs = new LinkedHashMap<AttributeKey<?>, Object>();
private final SessionBootstrapConfig config = new SessionBootstrapConfig(this);
private volatile EventLoopGroup childGroup;
private volatile ChannelHandler childHandler;
@SuppressWarnings("unchecked")
private volatile AddressResolverGroup<SocketAddress> resolver = (AddressResolverGroup<SocketAddress>) DEFAULT_RESOLVER;
private final int tcpPort;
private final int udpPort;
public
SessionBootstrap(final int tcpPort, final int udpPort) {
this.tcpPort = tcpPort;
this.udpPort = udpPort;
}
private
SessionBootstrap(SessionBootstrap bootstrap) {
super(bootstrap);
resolver = bootstrap.resolver;
childGroup = bootstrap.childGroup;
childHandler = bootstrap.childHandler;
synchronized (bootstrap.childOptions) {
childOptions.putAll(bootstrap.childOptions);
}
synchronized (bootstrap.childAttrs) {
childAttrs.putAll(bootstrap.childAttrs);
}
this.tcpPort = bootstrap.tcpPort;
this.udpPort = bootstrap.udpPort;
}
/**
* Set the specific {@link AttributeKey} with the given value on every child {@link Channel}. If the value is
* {@code null} the {@link AttributeKey} is removed
*/
public
<T> SessionBootstrap childAttr(AttributeKey<T> childKey, T value) {
if (childKey == null) {
throw new NullPointerException("childKey");
}
if (value == null) {
childAttrs.remove(childKey);
}
else {
childAttrs.put(childKey, value);
}
return this;
}
final
Map<AttributeKey<?>, Object> childAttrs() {
return copiedMap(childAttrs);
}
/**
* Return the configured {@link EventLoopGroup} which will be used for the child channels or {@code null}
* if non is configured yet.
*
* @deprecated Use {@link #config()} instead.
*/
@Deprecated
public
EventLoopGroup childGroup() {
return childGroup;
}
/**
* Set the {@link ChannelHandler} which is used to serve the request for the {@link Channel}'s.
*/
public
SessionBootstrap childHandler(ChannelHandler childHandler) {
if (childHandler == null) {
throw new NullPointerException("childHandler");
}
this.childHandler = childHandler;
return this;
}
final
ChannelHandler childHandler() {
return childHandler;
}
/**
* Allow to specify a {@link ChannelOption} which is used for the {@link Channel} instances once they get created
* (after the acceptor accepted the {@link Channel}). Use a value of {@code null} to remove a previous set
* {@link ChannelOption}.
*/
public
<T> SessionBootstrap childOption(ChannelOption<T> childOption, T value) {
if (childOption == null) {
throw new NullPointerException("childOption");
}
if (value == null) {
synchronized (childOptions) {
childOptions.remove(childOption);
}
}
else {
synchronized (childOptions) {
childOptions.put(childOption, value);
}
}
return this;
}
final
Map<ChannelOption<?>, Object> childOptions() {
return copiedMap(childOptions);
}
@Override
@SuppressWarnings("CloneDoesntCallSuperClone")
public
SessionBootstrap clone() {
return new SessionBootstrap(this);
}
@Override
public final
SessionBootstrapConfig config() {
return config;
}
/**
* Specify the {@link EventLoopGroup} which is used for the parent (acceptor) and the child (client).
*/
@Override
public
SessionBootstrap group(EventLoopGroup group) {
return group(group, group);
}
/**
* Set the {@link EventLoopGroup} for the parent (acceptor) and the child (client). These
* {@link EventLoopGroup}'s are used to handle all the events and IO for {@link DatagramSessionChannel} and
* {@link Channel}'s.
*/
public
SessionBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
super.group(parentGroup);
if (childGroup == null) {
throw new NullPointerException("childGroup");
}
if (this.childGroup != null) {
throw new IllegalStateException("childGroup set already");
}
this.childGroup = childGroup;
return this;
}
@Override
void init(Channel channel) throws Exception {
setChannelOptions(channel, options0().entrySet().toArray(EMPTY_OPTION_ARRAY), logger);
setAttributes(channel, attrs0().entrySet().toArray(EMPTY_ATTRIBUTE_ARRAY));
ChannelPipeline p = channel.pipeline();
final EventLoopGroup currentChildGroup = childGroup;
final ChannelHandler currentChildHandler = childHandler;
final Entry<ChannelOption<?>, Object>[] currentChildOptions;
final Entry<AttributeKey<?>, Object>[] currentChildAttrs;
synchronized (childOptions) {
currentChildOptions = childOptions.entrySet()
.toArray(newOptionArray(childOptions.size()));
}
synchronized (childAttrs) {
currentChildAttrs = childAttrs.entrySet()
.toArray(newAttrArray(childAttrs.size()));
}
p.addLast(new ChannelInitializer<Channel>() {
@Override
public
void initChannel(final Channel ch) throws Exception {
final ChannelPipeline pipeline = ch.pipeline();
ChannelHandler handler = config.handler();
if (handler != null) {
pipeline.addLast(handler);
}
ch.eventLoop()
.execute(new Runnable() {
@Override
public
void run() {
pipeline.addLast(new SessionManager(tcpPort, udpPort,
ch, currentChildGroup,
currentChildHandler,
currentChildOptions,
currentChildAttrs));
}
});
}
});
}
public
SocketAddress remoteAddress() {
// the nature of this is that WE do not have a remote address, but our child channels DO have a remote address
return null;
}
public
AddressResolverGroup<?> resolver() {
return resolver;
}
@Override
public
SessionBootstrap validate() {
super.validate();
if (childHandler == null) {
throw new IllegalStateException("childHandler not set");
}
if (childGroup == null) {
logger.warn("childGroup is not set. Using parentGroup instead.");
childGroup = config.group();
}
return this;
}
}

View File

@ -1,64 +0,0 @@
/*
* Copyright 2018 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 io.netty.bootstrap;
import java.net.SocketAddress;
import io.netty.channel.Channel;
import io.netty.resolver.AddressResolverGroup;
/**
* Exposes the configuration of a {@link Bootstrap}.
*/
public final
class SessionBootstrapConfig extends AbstractBootstrapConfig<SessionBootstrap, Channel> {
SessionBootstrapConfig(SessionBootstrap bootstrap) {
super(bootstrap);
}
/**
* Returns the configured remote address or {@code null} if non is configured yet.
*/
public
SocketAddress remoteAddress() {
return bootstrap.remoteAddress();
}
/**
* Returns the configured {@link AddressResolverGroup} or the default if non is configured yet.
*/
public
AddressResolverGroup<?> resolver() {
return bootstrap.resolver();
}
@Override
public
String toString() {
StringBuilder buf = new StringBuilder(super.toString());
buf.setLength(buf.length() - 1);
buf.append(", resolver: ")
.append(resolver());
SocketAddress remoteAddress = remoteAddress();
if (remoteAddress != null) {
buf.append(", remoteAddress: ")
.append(remoteAddress);
}
return buf.append(')')
.toString();
}
}

View File

@ -1,253 +0,0 @@
/*
* Copyright 2018 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 io.netty.bootstrap;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import dorkbox.network.pipeline.discovery.BroadcastServer;
import dorkbox.util.bytes.BigEndian.Long_;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelConfig;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.socket.DatagramPacket;
import io.netty.util.AttributeKey;
import io.netty.util.collection.LongObjectHashMap;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
/**
*
*/
public
class SessionManager extends ChannelInboundHandlerAdapter {
private static final InternalLogger logger = InternalLoggerFactory.getInstance(SessionBootstrap.class);
private static
void forceClose(DatagramSessionChannel child, Throwable t) throws Exception {
child.unsafe()
.closeForcibly();
child.doClose();
logger.warn("Failed to register an accepted channel: {}", child, t);
}
private static
long getChannelId(final InetSocketAddress remoteAddress) {
int address = remoteAddress.getAddress()
.hashCode(); // we want it as an int
int port = remoteAddress.getPort(); // this is really just 2 bytes
byte[] combined = new byte[8];
combined[0] = (byte) ((port >>> 24) & 0xFF);
combined[1] = (byte) ((port >>> 16) & 0xFF);
combined[2] = (byte) ((port >>> 8) & 0xFF);
combined[3] = (byte) ((port) & 0xFF);
combined[4] = (byte) ((address >>> 24) & 0xFF);
combined[5] = (byte) ((address >>> 16) & 0xFF);
combined[6] = (byte) ((address >>> 8) & 0xFF);
combined[7] = (byte) ((address) & 0xFF);
return Long_.from(combined);
}
private final BroadcastServer broadcastServer;
// Does not need to be thread safe, because access only happens in the event loop
private final LongObjectHashMap<DatagramSessionChannel> datagramChannels = new LongObjectHashMap<DatagramSessionChannel>();
private final EventLoopGroup childGroup;
private final ChannelHandler childHandler;
private final Entry<ChannelOption<?>, Object>[] childOptions;
private final Entry<AttributeKey<?>, Object>[] childAttrs;
private final Runnable enableAutoReadTask;
private final DatagramSessionChannelConfig sessionConfig;
SessionManager(final int tcpPort, final int udpPort,
final Channel channel,
EventLoopGroup childGroup,
ChannelHandler childHandler,
Entry<ChannelOption<?>, Object>[] childOptions,
Entry<AttributeKey<?>, Object>[] childAttrs) {
this.sessionConfig = new DatagramSessionChannelConfig(channel);
this.childGroup = childGroup;
this.childHandler = childHandler;
this.childOptions = childOptions;
this.childAttrs = childAttrs;
// Task which is scheduled to re-enable auto-read.
// It's important to create this Runnable before we try to submit it as otherwise the URLClassLoader may
// not be able to load the class because of the file limit it already reached.
//
// See https://github.com/netty/netty/issues/1328
enableAutoReadTask = new Runnable() {
@Override
public
void run() {
channel.config()
.setAutoRead(true);
}
};
broadcastServer = new BroadcastServer(tcpPort, udpPort);
}
@Override
public
void channelInactive(final ChannelHandlerContext context) throws Exception {
super.channelInactive(context);
// have to close all of the "fake" DatagramChannels as well. Each one will remove itself from the channel map.
// We make a copy of this b/c of concurrent modification, in the event this is closed BEFORE the child-channels are closed
ArrayList<DatagramSessionChannel> channels = new ArrayList<DatagramSessionChannel>(datagramChannels.values());
for (DatagramSessionChannel channel : channels) {
if (channel.isActive() &&
!channel.eventLoop().isShutdown()) {
channel.close();
}
}
}
@Override
@SuppressWarnings("unchecked")
public
void channelRead(ChannelHandlerContext context, Object msg) {
Channel channel = context.channel();
DatagramPacket packet = ((DatagramPacket) msg);
ByteBuf content = packet.content();
InetSocketAddress localAddress = packet.recipient();
InetSocketAddress remoteAddress = packet.sender();
// check to see if it's a broadcast packet or not
if (broadcastServer.isDiscoveryRequest(channel, content, localAddress, remoteAddress)) {
// don't bother creating channels if this is a broadcast event. Just respond and be finished
return;
}
long channelId = getChannelId(remoteAddress);
// create a new channel or reuse existing one
DatagramSessionChannel sessionChannel = datagramChannels.get(channelId);
ChannelPipeline sessionPipeline;
if (sessionChannel == null) {
try {
sessionChannel = new DatagramSessionChannel(context.channel(), this, sessionConfig, localAddress, remoteAddress);
datagramChannels.put(channelId, sessionChannel);
sessionPipeline = sessionChannel.pipeline();
// add the child handler to the fake channel
sessionPipeline.addLast(childHandler);
// setup the channel options
AbstractBootstrap.setChannelOptions(sessionChannel, childOptions, logger);
for (Entry<AttributeKey<?>, Object> e : childAttrs) {
sessionChannel.attr((AttributeKey<Object>) e.getKey())
.set(e.getValue());
}
try {
final DatagramSessionChannel finalSessionChannel = sessionChannel;
// the childGroup is the HANDSHAKE group. Once handshake is done, this will be passed off to a worker group
childGroup.register(sessionChannel)
.addListener(new ChannelFutureListener() {
@Override
public
void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
forceClose(finalSessionChannel, future.cause());
}
}
});
} catch (Throwable t) {
forceClose(sessionChannel, t);
}
} catch (Throwable t) {
logger.warn("Failed to create a new datagram channel from a read operation.", t);
try {
if (sessionChannel != null) {
sessionChannel.close();
}
} catch (Throwable t2) {
logger.warn("Failed to close the datagram channel.", t2);
}
return;
}
} else {
sessionPipeline = sessionChannel.pipeline();
}
// immediately trigger a read in the session pipeline
sessionPipeline.fireChannelRead(packet);
// will flush the pipeline if necessary
sessionPipeline.fireChannelReadComplete();
}
/**
* ADDED to support closing a DatagramSessionChannel. Always called from the EventLoop
*/
public
void doCloseChannel(final DatagramSessionChannel datagramSessionChannel) {
InetSocketAddress remoteAddress = datagramSessionChannel.remoteAddress();
long channelId = getChannelId(remoteAddress);
datagramChannels.remove(channelId);
}
@Override
public
void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
final ChannelConfig config = ctx.channel()
.config();
if (config.isAutoRead()) {
// stop accept new connections for 1 second to allow the channel to recover
// See https://github.com/netty/netty/issues/1328
config.setAutoRead(false);
ctx.channel()
.eventLoop()
.schedule(enableAutoReadTask, 1, TimeUnit.SECONDS);
}
// still let the exceptionCaught event flow through the pipeline to give the user
// a chance to do something with it
ctx.fireExceptionCaught(cause);
}
}

View File

@ -20,7 +20,6 @@
package dorkbox.network;
import static dorkbox.network.connection.EndPoint.THREADGROUP_NAME;
import static org.junit.Assert.fail;
import java.lang.reflect.Field;
@ -162,21 +161,21 @@ class BaseTest {
.getThreadGroup();
final String name = threadGroup.getName();
if (name.contains(THREADGROUP_NAME)) {
// We have to ALWAYS run this in a new thread, BECAUSE if stopEndPoints() is called from a client/server thread, it will DEADLOCK
final Thread thread = new Thread(threadGroup.getParent(), new Runnable() {
@Override
public
void run() {
stopEndPoints_(stopAfterMillis);
}
}, "UnitTest shutdown"); // a different name for the thread
thread.setDaemon(true);
thread.start();
} else {
// if (name.contains(THREADGROUP_NAME)) {
// // We have to ALWAYS run this in a new thread, BECAUSE if stopEndPoints() is called from a client/server thread, it will DEADLOCK
// final Thread thread = new Thread(threadGroup.getParent(), new Runnable() {
// @Override
// public
// void run() {
// stopEndPoints_(stopAfterMillis);
// }
// }, "UnitTest shutdown"); // a different name for the thread
//
// thread.setDaemon(true);
// thread.start();
// } else {
stopEndPoints_(stopAfterMillis);
}
// }
}
private

Some files were not shown because too many files have changed in this diff Show More