before kotlin conversion
This commit is contained in:
parent
8a2cac9ed3
commit
794965348a
189
build.gradle.kts
189
build.gradle.kts
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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"));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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"));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -112,7 +112,7 @@ class RegistrationWrapper {
|
|||
*/
|
||||
public
|
||||
int getIdleTimeout() {
|
||||
return this.endPoint.getIdleTimeout();
|
||||
return 5;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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() {
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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 {}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue