Added the network project

This commit is contained in:
nathan 2014-08-20 23:44:59 +02:00
commit 6df9f268ef
151 changed files with 23903 additions and 0 deletions

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="test"/>
<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4"/>
<classpathentry kind="lib" path="/Dependencies/kryo/jsonbeans-0.2.jar"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
<classpathentry kind="lib" path="/Dependencies/kryo/objenesis-1.2.jar" sourcepath="/Dependencies/kryo/objenesis-1.2-sources.jar"/>
<classpathentry kind="lib" path="/Dependencies/javassist/javassist.jar"/>
<classpathentry kind="lib" path="/Dependencies/logging/slf4j-api-1.7.5.jar" sourcepath="/Dependencies/logging/slf4j-api-1.7.5-sources.jar"/>
<classpathentry kind="lib" path="/Dependencies/logging/logback-classic-1.0.13.jar" sourcepath="/Dependencies/logging/logback-classic-1.0.13-sources.jar"/>
<classpathentry kind="lib" path="/Dependencies/logging/logback-core-1.0.13.jar" sourcepath="/Dependencies/logging/logback-core-1.0.13-sources.jar"/>
<classpathentry kind="lib" path="/Dependencies/logging/minlog-1.2-to-slf4j.jar" sourcepath="/Dependencies/logging/minlog-1.2-to-slf4j-sources.jar"/>
<classpathentry kind="lib" path="/Dependencies/barchart-udt/barchart-udt-bundle-2.3.1.jar" sourcepath="/Dependencies/barchart-udt/barchart-udt-bundle-2.3.1-sources.jar"/>
<classpathentry kind="lib" path="/Dependencies/barchart-udt/barchart-udt-core-2.3.1.jar"/>
<classpathentry combineaccessrules="false" kind="src" path="/Dorkbox-Util"/>
<classpathentry kind="lib" path="/Dependencies/kryo/reflectasm.jar" sourcepath="/Dependencies/kryo/reflectasm-src.jar"/>
<classpathentry kind="lib" path="/Dependencies/BouncyCastleCrypto/bcpkix-jdk15on-151.jar" sourcepath="/Dependencies/BouncyCastleCrypto/bcpkix-jdk15on-151-src.zip"/>
<classpathentry kind="lib" path="/Dependencies/BouncyCastleCrypto/bcprov-debug-jdk15on-151.jar" sourcepath="/Dependencies/BouncyCastleCrypto/bcprov-jdk15on-151-src.zip"/>
<classpathentry kind="lib" path="/Dependencies/kryo/kryo2-debug.jar" sourcepath="/Dependencies/kryo/kryo2-source.jar"/>
<classpathentry kind="lib" path="/Dependencies/netty/netty-all-4.1.0.jar" sourcepath="/Dependencies/netty/netty-all-4.1.0-sources.jar">
<attributes>
<attribute name="javadoc_location" value="jar:platform:/resource/Dependencies/netty/netty-all-4.1.0-javadoc.jar!/"/>
</attributes>
</classpathentry>
<classpathentry kind="lib" path="/Dependencies/asm/asm-5.0.3.jar" sourcepath="/Dependencies/asm/asm-5.0.3-src.zip"/>
<classpathentry kind="output" path="classes"/>
</classpath>

4
Dorkbox-Network/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/classes/
*.crt
*.ini
*.dat

17
Dorkbox-Network/.project Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>Dorkbox-Network</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>

159
Dorkbox-Network/LICENSE.TXT Normal file
View File

@ -0,0 +1,159 @@
Legal:
- Copyright (c) 2010 and beyond, dorkbox llc. All rights reserved.
This software is a commercial product. Use, redistribution, and
modification without a license is NOT permitted. Contact
license@dorkbox.com for a license.
Neither the name of dorkbox, dorkbox llc, or dorkbox.com or the names of
its contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
THE POSSIBILITY OF SUCH DAMAGE.
THIS SOFTWARE IS NOT DESIGNED OR INTENDED FOR USE OR RESALE AS
ON-LINE CONTROL EQUIPMENT IN HAZARDOUS ENVIRONMENTS REQUIRING FAIL-SAFE
PERFORMANCE, SUCH AS IN THE OPERATION OF NUCLEAR FACILITIES, AIRCRAFT
NAVIGATION OR COMMUNICATION SYSTEMS, AIR TRAFFIC CONTROL, DIRECT LIFE
SUPPORT MACHINES, OR WEAPONS SYSTEMS, IN WHICH THE FAILURE OF THE SOFTWARE
COULD LEAD DIRECTLY TO DEATH, PERSONAL INJURY, OR SEVERE PHYSICAL OR
ENVIRONMENTAL DAMAGE ("HIGH RISK ACTIVITIES").
THIS AGREEMENT IS GOVERNED THE INTELLECTUAL PROPERTY LAWS OF THE UNITED
STATES OF AMERICA. NO PARTY TO THIS AGREEMENT WILL BRING A LEGAL ACTION
UNDER THIS AGREEMENT MORE THAN ONE YEAR AFTER THE CAUSE OF ACTION AROSE.
EACH PARTY WAIVES ITS RIGHTS TO A JURY TRIAL IN ANY RESULTING LITIGATION.
- This software product uses encryption algorithms that are controlled by
the United States Export Administration Regulations (EAR).
https://bxa.ntis.gov/
Details of the U.S. Commercial Encryption Export Controls can be found at
the Bureau of Industry and Security (BIS) web site.
http://www.bis.doc.gov/
PROHIBITED END USERS
ALL products are prohibited for export/reexport to the following:
- Any company or national of Cuba, Iran, North Korea, Sudan, and Syria.
Licenses to these countries and parties are presumed denied.
- Re-export to these countries is prohibited; if you "know or have reason
to know" that an illegal reshipment will take place, you may not ship to
such a user.
- Entities listed on any U.S. Government Denied Party/Person List. See BIS's
The Denied Persons List, the Office of Foreign Assets Control's Economic
and Trade sanctions list, (OFAC), and the Office of Defense Trade
Controls (DTC).
- Any customer you know or have reason to know, who is involved in the
design, development, manufacture or production of nuclear technology, or
nuclear, biological or chemical "weapons of mass destruction."
All products are subject to the U.S. Export Laws, and diversion contrary
to U.S. law is prohibited.
- ASM - Bytecode manipulation framework and utilities - New BSD license
http://asm.ow2.org/
Copyright (c) 2012 France Télécom
All rights reserved.
- BarchartUDT - BSD license
https://github.com/barchart/barchart-udt
Copyright Andrei Pozolotin and others at Barchart, Inc.
- BouncyCastle - MIT X11 License
http://www.bouncycastle.org
Copyright (c) 2000 - 2009 The Legion Of The Bouncy Castle
- JAVASSIST - Apache 2.0 license
http://www.csg.is.titech.ac.jp/~chiba/javassist/
Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.
Contributor(s): Bill Burke, Jason T. Greene
Note: it is licensed under the MPL/LGPL/Apache triple license
- JsonBeans, Kryo, Minlog (Minlog-SLF4J), ReflectASM - New BSD license
http://code.google.com/p/jsonbeans/
http://code.google.com/p/kryo/
http://code.google.com/p/minlog/
https://github.com/jdanbrown/minlog-slf4j
http://code.google.com/p/reflectasm/
Copyright (c) 2008, Nathan Sweet, All rights reserved.
- kryo-serializers - Apache 2.0 license
https://github.com/magro/kryo-serializers
Copyright 2010 Martin Grotzke
- Logback - EPL v1.0
http://logback.qos.ch/
Copyright (C) 1999-2012, QOS.ch. All rights reserved.
- MathUtils, IntArray, IntMap - Apache 2.0 license
http://github.com/libgdx/libgdx/
Copyright (c) 2013
Mario Zechner <badlogicgames@gmail.com>
Nathan Sweet <nathan.sweet@gmail.com>
- Netty - Apache 2.0 license
http://netty.io
Copyright (c) 2013 The Netty Project
- Objenesis - Apache 2.0 license
http://http://code.google.com/p/objenesis/
Copyright (c) 2003-2009, Joe Walnes, Henri Tremblay, Leonardo Mesquita
- SLF4J - MIT license.
http://www.slf4j.org/
Copyright (c) 2004-2008 QOS.ch
All rights reserved.
- UDT4 - BSD license
http://udt.sourceforge.net/
Copyright 2001 - 2009, The Board of Trustees of the University of Illinois.
All rights reserved.

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.eclipse.jdt.junit.launchconfig">
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/Dorkbox-Network"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="4"/>
</listAttribute>
<stringAttribute key="org.eclipse.jdt.junit.CONTAINER" value="=Dorkbox-Network"/>
<booleanAttribute key="org.eclipse.jdt.junit.KEEPRUNNING_ATTR" value="false"/>
<stringAttribute key="org.eclipse.jdt.junit.TESTNAME" value=""/>
<stringAttribute key="org.eclipse.jdt.junit.TEST_KIND" value="org.eclipse.jdt.junit.loader.junit4"/>
<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value=""/>
<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="Dorkbox-Network"/>
</launchConfiguration>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.eclipse.jdt.junit.launchconfig">
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/NettyWrapper/test"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="2"/>
</listAttribute>
<mapAttribute key="org.eclipse.debug.core.preferred_launchers"/>
<stringAttribute key="org.eclipse.jdt.junit.CONTAINER" value="=NettyWrapper/test"/>
<booleanAttribute key="org.eclipse.jdt.junit.KEEPRUNNING_ATTR" value="false"/>
<stringAttribute key="org.eclipse.jdt.junit.TESTNAME" value=""/>
<stringAttribute key="org.eclipse.jdt.junit.TEST_KIND" value="org.eclipse.jdt.junit.loader.junit4"/>
<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value=""/>
<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="NettyWrapper"/>
</launchConfiguration>

View File

@ -0,0 +1,20 @@
package dorkbox.network;
import io.netty.bootstrap.Bootstrap;
class BootstrapWrapper {
final String type;
final Bootstrap bootstrap;
final int port;
BootstrapWrapper(String type, int port, Bootstrap bootstrap) {
this.type = type;
this.port = port;
this.bootstrap = bootstrap;
}
@Override
public String toString() {
return "BootstrapWrapper [type=" + this.type + ", port=" + this.port + "]";
}
}

View File

@ -0,0 +1,226 @@
package dorkbox.network;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.DatagramPacket;
import io.netty.channel.socket.nio.NioDatagramChannel;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.concurrent.TimeUnit;
import dorkbox.network.pipeline.discovery.ClientDiscoverHostHandler;
import dorkbox.network.pipeline.discovery.ClientDiscoverHostInitializer;
public class Broadcast {
public static final byte broadcastID = (byte)42;
public static final byte broadcastResponseID = (byte)57;
private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger("Broadcast Host Discovery");
/**
* Broadcasts a UDP message on the LAN to discover any running servers. The
* address of the first server to respond is returned.
*
* From KryoNet
*
* @param udpPort
* The UDP port of the server.
* @param discoverTimeoutMillis
* The number of milliseconds to wait for a response.
*
* @return the first server found, or null if no server responded.
*/
public static String discoverHost(int udpPort, int discoverTimeoutMillis) {
InetAddress discoverHost = discoverHostAddress(udpPort, discoverTimeoutMillis);
if (discoverHost != null) {
return discoverHost.getHostAddress();
}
return null;
}
/**
* Broadcasts a UDP message on the LAN to discover any running servers. The
* address of the first server to respond is returned.
*
* @param udpPort
* The UDP port of the server.
* @param discoverTimeoutMillis
* The number of milliseconds to wait for a response.
*
* @return the first server found, or null if no server responded.
*/
public static final InetAddress discoverHostAddress(int udpPort, int discoverTimeoutMillis) {
List<InetAddress> servers = discoverHost0(udpPort, discoverTimeoutMillis, false);
if (servers.isEmpty()) {
return null;
} else {
return servers.get(0);
}
}
/**
* Broadcasts a UDP message on the LAN to discover all running servers.
*
* @param udpPort
* The UDP port of the server.
* @param timeoutMillis
* The number of milliseconds to wait for a response.
*
* @return the list of found servers (if they responded)
*/
public static List<InetAddress> discoverHosts2(int udpPort, int discoverTimeoutMillis) {
return discoverHost0(udpPort, discoverTimeoutMillis, true);
}
private static final List<InetAddress> discoverHost0(int udpPort, int discoverTimeoutMillis, boolean fetchAllServers) {
// fetch a buffer that contains the serialized object.
ByteBuf buffer = Unpooled.buffer(1);
buffer.writeByte(broadcastID);
List<InetAddress> servers = new ArrayList<InetAddress>();
logger.info("Searching for host on port: {}", udpPort);
Enumeration<NetworkInterface> networkInterfaces;
try {
networkInterfaces = NetworkInterface.getNetworkInterfaces();
} catch (SocketException e) {
logger.error("Host discovery failed.", e);
return new ArrayList<InetAddress>(0);
}
scan:
for (NetworkInterface networkInterface : Collections.list(networkInterfaces)) {
for (InterfaceAddress interfaceAddress : networkInterface.getInterfaceAddresses()) {
InetAddress address = interfaceAddress.getAddress();
InetAddress broadcast = interfaceAddress.getBroadcast();
// don't use IPv6!
if (address instanceof Inet6Address) {
logger.info("Not using IPv6 address: {}", address);
continue;
}
try {
NioEventLoopGroup group = new NioEventLoopGroup();
Bootstrap udpBootstrap = new Bootstrap()
.group(group)
.channel(NioDatagramChannel.class)
.option(ChannelOption.SO_BROADCAST, true)
.handler(new ClientDiscoverHostInitializer())
.localAddress(new InetSocketAddress(address, 0)); // pick random address. Not listen for broadcast.
// we don't care about RECEIVING a broadcast packet, we are only SENDING one.
ChannelFuture future;
try {
future = udpBootstrap.bind();
future.await();
} catch (InterruptedException e) {
logger.error("Could not bind to random UDP address on the server.", e.getCause());
throw new IllegalArgumentException();
}
if (!future.isSuccess()) {
logger.error("Could not bind to random UDP address on the server.", future.cause());
throw new IllegalArgumentException();
}
Channel channel1 = future.channel();
if (broadcast != null) {
// try the "defined" broadcast first if we have it (not always!)
channel1.writeAndFlush(new DatagramPacket(buffer, new InetSocketAddress(broadcast, udpPort)));
// response is received. If the channel is not closed within 5 seconds, move to the next one.
if (!channel1.closeFuture().awaitUninterruptibly(discoverTimeoutMillis)) {
logger.info("Host discovery timed out.");
} else {
InetSocketAddress attachment = channel1.attr(ClientDiscoverHostHandler.STATE).get();
servers.add(attachment.getAddress());
}
// keep going if we want to fetch all servers. Break if we found one.
if (!(fetchAllServers || servers.isEmpty())) {
channel1.close().await();
group.shutdownGracefully().await();
break scan;
}
}
// continue with "common" broadcast addresses.
// Java 1.5 doesn't support getting the subnet mask, so try them until we find one.
byte[] ip = address.getAddress();
for (int octect = 3; octect >= 0; octect--) {
ip[octect] = -1; // 255.255.255.0
// don't error out on one particular octect
try {
InetAddress byAddress = InetAddress.getByAddress(ip);
channel1.write(new DatagramPacket(buffer, new InetSocketAddress(byAddress, udpPort)));
// response is received. If the channel is not closed within 5 seconds, move to the next one.
if (!channel1.closeFuture().awaitUninterruptibly(discoverTimeoutMillis)) {
logger.info("Host discovery timed out.");
} else {
InetSocketAddress attachment = channel1.attr(ClientDiscoverHostHandler.STATE).get();
servers.add(attachment.getAddress());
if (!fetchAllServers) {
break;
}
}
} catch (Exception ignored) {
}
}
channel1.close().sync();
group.shutdownGracefully(0, discoverTimeoutMillis, TimeUnit.MILLISECONDS);
} catch (Exception ignored) {
}
// keep going if we want to fetch all servers. Break if we found one.
if (!(fetchAllServers || servers.isEmpty())) {
break scan;
}
}
}
if (logger.isInfoEnabled() && !servers.isEmpty()) {
if (fetchAllServers) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("Discovered servers: (").append(servers.size()).append(")");
for (InetAddress server : servers) {
stringBuilder.append("/n").append(server).append(":").append(udpPort);
}
logger.info(stringBuilder.toString());
} else {
logger.info("Discovered server: {}:{}", servers.get(0), udpPort);
}
}
return servers;
}
}

View File

@ -0,0 +1,384 @@
package dorkbox.network;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.DefaultEventLoopGroup;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.local.LocalAddress;
import io.netty.channel.local.LocalChannel;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.oio.OioEventLoopGroup;
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.util.internal.PlatformDependent;
import java.net.InetSocketAddress;
import java.util.LinkedList;
import java.util.List;
import dorkbox.network.connection.Connection;
import dorkbox.network.connection.ConnectionBridge;
import dorkbox.network.connection.ConnectionBridgeFlushAlways;
import dorkbox.network.connection.EndPointClient;
import dorkbox.network.connection.idle.IdleBridge;
import dorkbox.network.connection.idle.IdleSender;
import dorkbox.network.connection.ping.Ping;
import dorkbox.network.connection.registration.local.RegistrationLocalHandlerClient;
import dorkbox.network.connection.registration.remote.RegistrationRemoteHandlerClientTCP;
import dorkbox.network.connection.registration.remote.RegistrationRemoteHandlerClientUDP;
import dorkbox.network.connection.registration.remote.RegistrationRemoteHandlerClientUDT;
import dorkbox.network.util.InitializationException;
import dorkbox.network.util.NamedThreadFactory;
import dorkbox.network.util.SecurityException;
import dorkbox.network.util.SerializationManager;
import dorkbox.network.util.udt.UdtEndpointProxy;
/**
* The client is both SYNC and ASYNC, meaning that once the client is connected to the server, you can access it however you want.
* <p>
* Another way to put this: The client (like the server) can respond to EVENTS (ie, listeners), but you can also use it DIRECTLY, for
* example, send data to the server on keyboard input. This is because the client will BLOCK the calling thread until it's ready.
*/
public class Client extends EndPointClient {
private List<BootstrapWrapper> bootstraps = new LinkedList<BootstrapWrapper>();
private volatile boolean registrationInProgress = false;
private volatile int connectionTimeout = 5000; // default
/**
* Starts a LOCAL <b>only</b> client, with the default local channel name and serialization scheme
*/
public Client() throws InitializationException, SecurityException {
this(new ConnectionOptions(LOCAL_CHANNEL));
}
/**
* Starts a TCP & UDP client (or a LOCAL client), with the specified serialization scheme
*/
public Client(String host, int tcpPort, int udpPort, int udtPort, String localChannelName, SerializationManager serializationManager)
throws InitializationException, SecurityException {
this(new ConnectionOptions(host, tcpPort, udpPort, udtPort, localChannelName, serializationManager));
}
/**
* Starts a REMOTE <b>only</b> client, which will connect to the specified host using the specified Connections Options
*/
public Client(ConnectionOptions options) throws InitializationException, SecurityException {
super("Client", options);
if (options.localChannelName != null && (options.tcpPort > 0 || options.udpPort > 0 || options.host != null) ||
options.localChannelName == null && (options.tcpPort == 0 || options.udpPort == 0 || options.host == null)
) {
String msg = name + " Local channel use and TCP/UDP use are MUTUALLY exclusive. Unable to determine intent.";
logger.error(msg);
throw new IllegalArgumentException(msg);
}
boolean isAndroid = PlatformDependent.isAndroid();
if (isAndroid && options.udtPort > 0) {
// Android does not support UDT.
logger.info("Android does not support UDT.");
options.udtPort = -1;
}
// tcpBootstrap.setOption(SO_SNDBUF, 1048576);
// tcpBootstrap.setOption(SO_RCVBUF, 1048576);
// setup the thread group to easily ID what the following threads belong to (and their spawned threads...)
SecurityManager s = System.getSecurityManager();
ThreadGroup nettyGroup = new ThreadGroup(s != null ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(), name + " (Netty)");
if (options.localChannelName != null && options.tcpPort < 0 && options.udpPort < 0 && options.udtPort < 0) {
// no networked bootstraps. LOCAL connection only
Bootstrap localBootstrap = new Bootstrap();
bootstraps.add(new BootstrapWrapper("LOCAL", -1, localBootstrap));
EventLoopGroup boss;
boss = new DefaultEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(name + "-LOCAL", nettyGroup));
localBootstrap.group(boss)
.channel(LocalChannel.class)
.remoteAddress(new LocalAddress(options.localChannelName))
.handler(new RegistrationLocalHandlerClient(name, registrationWrapper));
manageForShutdown(boss);
}
else {
if (options.tcpPort > 0) {
Bootstrap tcpBootstrap = new Bootstrap();
bootstraps.add(new BootstrapWrapper("TCP", options.tcpPort, tcpBootstrap));
EventLoopGroup boss;
if (isAndroid) {
// android ONLY supports OIO (not NIO)
boss = new OioEventLoopGroup(0, new NamedThreadFactory(name + "-TCP", nettyGroup));
tcpBootstrap.channel(OioSocketChannel.class);
} else {
boss = new NioEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(name + "-TCP", nettyGroup));
tcpBootstrap.channel(NioSocketChannel.class);
}
tcpBootstrap.group(boss)
.remoteAddress(options.host, options.tcpPort)
.handler(new RegistrationRemoteHandlerClientTCP(name, registrationWrapper, serializationManager));
manageForShutdown(boss);
// android screws up on this!!
tcpBootstrap.option(ChannelOption.TCP_NODELAY, !isAndroid);
tcpBootstrap.option(ChannelOption.SO_KEEPALIVE, true);
}
if (options.udpPort > 0) {
Bootstrap udpBootstrap = new Bootstrap();
bootstraps.add(new BootstrapWrapper("UDP", options.udpPort, udpBootstrap));
EventLoopGroup boss;
if (isAndroid) {
// android ONLY supports OIO (not NIO)
boss = new OioEventLoopGroup(0, new NamedThreadFactory(name + "-UDP", nettyGroup));
udpBootstrap.channel(OioDatagramChannel.class);
} else {
boss = new NioEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(name + "-UDP", nettyGroup));
udpBootstrap.channel(NioDatagramChannel.class);
}
udpBootstrap.group(boss)
.localAddress(new InetSocketAddress(0))
.remoteAddress(new InetSocketAddress(options.host, options.udpPort))
.handler(new RegistrationRemoteHandlerClientUDP(name, registrationWrapper, serializationManager));
manageForShutdown(boss);
// 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);
udpBootstrap.option(ChannelOption.SO_SNDBUF, udpMaxSize);
}
if (options.udtPort > 0) {
// check to see if we have UDT available!
boolean udtAvailable = false;
try {
Class.forName("com.barchart.udt.nio.SelectorProviderUDT");
udtAvailable = true;
} catch (Throwable e) {
logger.error("Requested a UDT connection on port {}, but the barchart UDT libraries are not loaded.", options.udtPort);
}
if (udtAvailable) {
// all of this must be proxied to another class, so THIS class doesn't have unmet dependencies.
// Annoying and abusing the classloader, but it works well.
Bootstrap udtBootstrap = new Bootstrap();
bootstraps.add(new BootstrapWrapper("UDT", options.udtPort, udtBootstrap));
EventLoopGroup boss;
boss = UdtEndpointProxy.getClientWorker(DEFAULT_THREAD_POOL_SIZE, name, nettyGroup);
UdtEndpointProxy.setChannelFactory(udtBootstrap);
udtBootstrap.group(boss)
.remoteAddress(options.host, options.udtPort)
.handler(new RegistrationRemoteHandlerClientUDT(name, registrationWrapper, serializationManager));
manageForShutdown(boss);
}
}
}
// this thread will prevent the application from closing, since the JVM only exits when all non-daemon threads have ended.
// We will wait until Client.stop() is called before exiting.
// NOTE: if we are the webserver, then this method will be called for EVERY web connection made
// Thread exitThread = new Thread(new Runnable() {
// @Override
// public void run() {
// waitForStop();
// }
// });
// exitThread.setDaemon(false);
// exitThread.setName("Exit Monitor (Client)");
// exitThread.start();
}
/**
* Allows the client to reconnect to the last connected server
*/
public void reconnect() {
reconnect(connectionTimeout);
}
/**
* Allows the client to reconnect to the last connected server
*/
public void reconnect(int connectionTimeout) {
// close out all old connections
close();
connect(connectionTimeout);
}
/**
* will attempt to connect to the server, with a 30 second timeout.
*
* @param connectionTimeout wait for x milliseconds. 0 will wait indefinitely
*/
public void connect() {
connect(30000);
}
/**
* will attempt to connect to the server, and will the specified timeout.
*
* @param connectionTimeout wait for x milliseconds. 0 will wait indefinitely
*/
public void connect(int connectionTimeout) {
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) {
}
// have to BLOCK here, because we don't want sendTCP() called before registration is complete
synchronized (registrationLock) {
registrationInProgress = true;
// we will only do a local channel when NOT doing TCP/UDP channels. This is EXCLUSIVE. (XOR)
int size = bootstraps.size();
for (int i=0;i<size;i++) {
registrationComplete = i == size-1;
BootstrapWrapper bootstrapWrapper = bootstraps.get(i);
ChannelFuture future;
if (connectionTimeout != 0) {
// must be before connect
bootstrapWrapper.bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectionTimeout);
}
try {
// UDP : When this is CONNECT on 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();
} catch (Exception e) {
if (logger.isDebugEnabled()) {
logger.error("Could not connect to the {} server on port {}.", bootstrapWrapper.type, bootstrapWrapper.port, e.getCause());
} else {
logger.error("Could not connect to the {} server{}.", bootstrapWrapper.type, bootstrapWrapper.port);
}
registrationInProgress = false;
stop();
return;
}
if (!future.isSuccess()) {
if (logger.isDebugEnabled()) {
logger.error("Could not connect to the {} server.", bootstrapWrapper.type, future.cause());
} else {
logger.error("Could not connect to the {} server.", bootstrapWrapper.type);
}
registrationInProgress = false;
stop();
return;
}
logger.trace("Waiting for registration from server.");
manageForShutdown(future);
// WAIT for the next one to complete.
try {
registrationLock.wait();
} catch (InterruptedException e) {
}
}
registrationInProgress = false;
}
}
/**
* 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.
*/
public ConnectionBridge send() {
return new ConnectionBridgeFlushAlways(connectionManager.getConnection0().send());
}
/**
* Expose methods to send objects to a destination when the connection has become idle.
*/
public IdleBridge sendOnIdle(IdleSender<?, ?> sender) {
return connectionManager.getConnection0().sendOnIdle(sender);
}
/**
* Expose methods to send objects to a destination when the connection has become idle.
*/
public IdleBridge sendOnIdle(Object message) {
return connectionManager.getConnection0().sendOnIdle(message);
}
/**
* Returns a future that will have the last calculated return trip time.
*/
public Ping ping() {
return connectionManager.getConnection0().send().ping();
}
/**
* Fetches the connection used by the client.
* <p>
* Make <b>sure</b> that you only call this <b>after</b> the client connects!
* <p>
* This is preferred to {@link getConnections}, as it properly does some error checking
*/
public Connection getConnection() {
return connectionManager.getConnection0();
}
/**
* Closes all connections ONLY (keeps the server/client running)
*/
@Override
public void close() {
// in case a different thread is blocked waiting for registration.
synchronized (registrationLock) {
if (registrationInProgress) {
try {
registrationLock.wait();
} catch (InterruptedException e) {
}
}
// inside the sync block, because we DON'T want to allow a connect WHILE close is happening! Since connect is also
// in the sync bloc, we prevent it from happening.
super.close();
}
}
}

View File

@ -0,0 +1,37 @@
package dorkbox.network;
import dorkbox.network.util.SerializationManager;
import dorkbox.network.util.store.SettingsStore;
public class ConnectionOptions {
public String host = null;
public int tcpPort = -1;
/** UDP requires TCP to handshake */
public int udpPort = -1;
/** UDT requires TCP to handshake */
public int udtPort = -1;
public String localChannelName = null;
public SerializationManager serializationManager = null;
public SettingsStore settingsStore = null;
public ConnectionOptions() {
}
public ConnectionOptions(String localChannelName) {
this.localChannelName = localChannelName;
}
public ConnectionOptions(String host, int tcpPort, int udpPort, int udtPort, String localChannelName, SerializationManager serializationManager) {
this.host = host;
this.tcpPort = tcpPort;
this.udpPort = udpPort;
this.udtPort = udtPort;
this.localChannelName = localChannelName;
this.serializationManager = serializationManager;
}
}

View File

@ -0,0 +1,357 @@
package dorkbox.network;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.DefaultEventLoopGroup;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.local.LocalAddress;
import io.netty.channel.local.LocalServerChannel;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.oio.OioEventLoopGroup;
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 dorkbox.network.connection.EndPointServer;
import dorkbox.network.connection.registration.local.RegistrationLocalHandlerServer;
import dorkbox.network.connection.registration.remote.RegistrationRemoteHandlerServerTCP;
import dorkbox.network.connection.registration.remote.RegistrationRemoteHandlerServerUDP;
import dorkbox.network.connection.registration.remote.RegistrationRemoteHandlerServerUDT;
import dorkbox.network.util.InitializationException;
import dorkbox.network.util.NamedThreadFactory;
import dorkbox.network.util.SecurityException;
import dorkbox.network.util.udt.UdtEndpointProxy;
/**
* The server can only be accessed in an ASYNC manner. This means that the server can only be used in RESPONSE
* to events. If you access the server OUTSIDE of events, you will get inaccurate information from the server (such as getConnections())
* <p>
* To put it bluntly, ONLY have the server do work inside of a listener!
*/
public class Server extends EndPointServer {
/**
* 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.
*/
public static int backlogConnectionCount = 50;
private final ServerBootstrap localBootstrap;
private final ServerBootstrap tcpBootstrap;
private final Bootstrap udpBootstrap;
private final ServerBootstrap udtBootstrap;
private final int tcpPort;
private final int udpPort;
private final int udtPort;
private final String localChannelName;
/**
* Starts a LOCAL <b>only</b> server, with the default serialization scheme
*/
public Server() throws InitializationException, SecurityException {
this(new ConnectionOptions(LOCAL_CHANNEL));
}
/**
* Convenience method to starts a server with the specified Connection Options
*/
public Server(ConnectionOptions options) throws InitializationException, SecurityException {
// 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.serializatino
super("Server", options);
if (isAndroid && options.udtPort > 0) {
// Android does not support UDT.
logger.info("Android does not support UDT.");
options.udtPort = -1;
}
this.tcpPort = options.tcpPort;
this.udpPort = options.udpPort;
this.udtPort = options.udtPort;
this.localChannelName = options.localChannelName;
if (localChannelName != null ) {
localBootstrap = new ServerBootstrap();
} else {
localBootstrap = null;
}
if (tcpPort > 0) {
tcpBootstrap = new ServerBootstrap();
} else {
tcpBootstrap = null;
}
if (udpPort > 0) {
udpBootstrap = new Bootstrap();
} else {
udpBootstrap = null;
}
if (udtPort > 0) {
// check to see if we have UDT available!
boolean udtAvailable = false;
try {
Class.forName("com.barchart.udt.nio.SelectorProviderUDT");
udtAvailable = true;
} catch (Throwable e) {
logger.error("Requested a UDT service on port {}, but the barchart UDT libraries are not loaded.", udtPort);
}
if (udtAvailable) {
udtBootstrap = new ServerBootstrap();
} else {
udtBootstrap = null;
}
} else {
udtBootstrap = null;
}
//TODO: do we need to set the snd/rcv buffer?
// tcpBootstrap.setOption(SO_SNDBUF, 1048576);
// tcpBootstrap.setOption(SO_RCVBUF, 1048576);
// setup the thread group to easily ID what the following threads belong to (and their spawned threads...)
SecurityManager s = System.getSecurityManager();
ThreadGroup nettyGroup = new ThreadGroup(s != null ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(), name + " (Netty)");
// always use local channels on the server.
{
EventLoopGroup boss;
EventLoopGroup worker;
if (localBootstrap != null) {
boss = new DefaultEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(name + "-boss-LOCAL", nettyGroup));
worker = new DefaultEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(name + "-worker-LOCAL", nettyGroup));
localBootstrap.group(boss, worker)
.channel(LocalServerChannel.class)
.localAddress(new LocalAddress(this.localChannelName))
.childHandler(new RegistrationLocalHandlerServer(name, registrationWrapper));
manageForShutdown(boss);
manageForShutdown(worker);
}
}
if (tcpBootstrap != null) {
EventLoopGroup boss;
EventLoopGroup worker;
if (isAndroid) {
// android ONLY supports OIO (not NIO)
boss = new OioEventLoopGroup(0, new NamedThreadFactory(name + "-boss-TCP", nettyGroup));
worker = new OioEventLoopGroup(0, new NamedThreadFactory(name + "-worker-TCP", nettyGroup));
tcpBootstrap.channel(OioServerSocketChannel.class);
} else {
boss = new NioEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(name + "-boss-TCP", nettyGroup));
worker = new NioEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(name + "-worker-TCP", nettyGroup));
tcpBootstrap.channel(NioServerSocketChannel.class);
}
manageForShutdown(boss);
manageForShutdown(worker);
tcpBootstrap.group(boss, worker)
.option(ChannelOption.SO_BACKLOG, backlogConnectionCount)
.childHandler(new RegistrationRemoteHandlerServerTCP(name, registrationWrapper, serializationManager));
if (options.host != null) {
tcpBootstrap.localAddress(options.host, tcpPort);
} else {
tcpBootstrap.localAddress(tcpPort);
}
// android screws up on this!!
tcpBootstrap.option(ChannelOption.TCP_NODELAY, !isAndroid);
tcpBootstrap.childOption(ChannelOption.TCP_NODELAY, !isAndroid);
tcpBootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
}
if (udpBootstrap != null) {
EventLoopGroup worker;
if (isAndroid) {
// android ONLY supports OIO (not NIO)
worker = new OioEventLoopGroup(0, new NamedThreadFactory(name + "-worker-UDP", nettyGroup));
udpBootstrap.channel(OioDatagramChannel.class);
} else {
worker = new NioEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(name + "-worker-UDP", nettyGroup));
udpBootstrap.channel(NioDatagramChannel.class);
}
manageForShutdown(worker);
udpBootstrap.group(worker)
// not binding to specific address, since it's driven by TCP, and that can be bound to a specific address
.localAddress(udpPort) // if you bind to a specific interface, Linux will be unable to receive broadcast packets!
.handler(new RegistrationRemoteHandlerServerUDP(name, registrationWrapper, serializationManager));
// 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);
udpBootstrap.option(ChannelOption.SO_SNDBUF, udpMaxSize);
}
if (udtBootstrap != null) {
EventLoopGroup boss;
EventLoopGroup worker;
// all of this must be proxied to another class, so THIS class doesn't have unmet dependencies.
// Annoying and abusing the classloader, but it works well.
boss = UdtEndpointProxy.getServerBoss(DEFAULT_THREAD_POOL_SIZE, name, nettyGroup);
worker = UdtEndpointProxy.getServerWorker(DEFAULT_THREAD_POOL_SIZE, name, nettyGroup);
UdtEndpointProxy.setChannelFactory(udtBootstrap);
udtBootstrap.group(boss, worker)
.option(ChannelOption.SO_BACKLOG, backlogConnectionCount)
// not binding to specific address, since it's driven by TCP, and that can be bound to a specific address
.localAddress(udtPort)
.childHandler(new RegistrationRemoteHandlerServerUDT(name, registrationWrapper, serializationManager));
manageForShutdown(boss);
manageForShutdown(worker);
}
}
/**
* Binds the server to the configured, underlying protocols.
* <p>
* This method will also BLOCK until the stop method is called, and if
* you want to continue running code after this method invocation, bind should be called in a separate, non-daemon thread.
*/
public void bind() {
bind(true);
}
/**
* Binds the server to the configured, underlying protocols.
* <p>
* This is a more advanced method, and you should consider calling <code>bind()</code> instead.
*
* @param blockUntilTerminate will BLOCK until the server stop method is called, and if
* you want to continue running code after this method invocation, bind should be called in a separate,
* non-daemon thread - or with false as the parameter.
*/
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) {
}
// Note: 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) {
logger.error("Could not bind to LOCAL address on the server.", e.getCause());
stop();
throw new IllegalArgumentException();
}
if (!future.isSuccess()) {
logger.error("Could not bind to LOCAL address on the server.", future.cause());
stop();
throw new IllegalArgumentException();
}
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) {
logger.error("Could not bind to TCP port {} on the server.", tcpPort, e.getCause());
stop();
throw new IllegalArgumentException("Could not bind to TCP port");
}
if (!future.isSuccess()) {
logger.error("Could not bind to TCP port {} on the server.", tcpPort , future.cause());
stop();
throw new IllegalArgumentException("Could not bind to TCP port");
}
logger.info("Listening on TCP port: {}", tcpPort);
manageForShutdown(future);
}
// UDP
if (udpBootstrap != null) {
// Wait until the connection attempt succeeds or fails.
try {
future = udpBootstrap.bind();
future.await();
} catch (Exception e) {
logger.error("Could not bind to UDP port {} on the server.", udpPort, e.getCause());
stop();
throw new IllegalArgumentException("Could not bind to UDP port");
}
if (!future.isSuccess()) {
logger.error("Could not bind to UDP port {} on the server.", udpPort, future.cause());
stop();
throw new IllegalArgumentException("Could not bind to UDP port");
}
logger.info("Listening on UDP port: {}", udpPort);
manageForShutdown(future);
}
// UDT
if (udtBootstrap != null) {
// Wait until the connection attempt succeeds or fails.
try {
future = udtBootstrap.bind();
future.await();
} catch (Exception e) {
logger.error("Could not bind to UDT port {} on the server.", udtPort, e.getCause());
stop();
throw new IllegalArgumentException("Could not bind to UDT port");
}
if (!future.isSuccess()) {
logger.error("Could not bind to UDT port {} on the server.", udtPort, future.cause());
stop();
throw new IllegalArgumentException("Could not bind to UDT port");
}
logger.info("Listening on UDT port: {}", udtPort);
manageForShutdown(future);
}
// 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.
waitForStop(blockUntilTerminate);
}
}

View File

@ -0,0 +1,14 @@
package dorkbox.network.connection;
import dorkbox.network.connection.wrapper.ChannelWrapper;
public class Bridge {
final ChannelWrapper channelWrapper;
final ISessionManager sessionManager;
Bridge(ChannelWrapper channelWrapper, ISessionManager sessionManager) {
this.channelWrapper = channelWrapper;
this.sessionManager = sessionManager;
}
}

View File

@ -0,0 +1,92 @@
package dorkbox.network.connection;
import org.bouncycastle.crypto.params.ParametersWithIV;
import dorkbox.network.connection.idle.IdleBridge;
import dorkbox.network.connection.idle.IdleSender;
public interface Connection {
public static final String connection = "connection";
/**
* Initialize the connection with any extra info that is needed but was unavailable at the channel construction.
* <p>
* This happens BEFORE prep.
*/
public void init(EndPointWithSerialization endPoint, Bridge bridge);
/**
* Prepare the channel wrapper, since it doesn't have access to certain fields during it's initialization.
* <p>
* This happens AFTER init.
*/
public void prep();
/**
* @return the AES key/IV, etc associated with this connection
*/
public ParametersWithIV getCryptoParameters();
/**
* Has the remote ECC public key changed. This can be useful if specific actions are necessary when the key has changed.
*/
public boolean hasRemoteKeyChanged();
/**
* @return the remote address, as a string.
*/
public String getRemoteHost();
/**
* @return the name used by the connection
*/
public String getName();
/**
* @return the connection (TCP or LOCAL) id of this connection.
*/
public int id();
/**
* @return the connection (TCP or LOCAL) id of this connection as a HEX
* string.
*/
public String idAsHex();
/**
* @return true if this connection is also configured to use UDP
*/
public boolean hasUdp();
/**
* @return true if this connection is also configured to use UDT
*/
public boolean hasUdt();
/**
* Expose methods to send objects to a destination (such as a custom object or a standard ping)
*/
public ConnectionBridge send();
/**
* Expose methods to send objects to a destination when the connection has become idle.
*/
public IdleBridge sendOnIdle(IdleSender<?, ?> sender);
/**
* Expose methods to send objects to a destination when the connection has become idle.
*/
public IdleBridge sendOnIdle(Object message);
/**
* Expose methods to modify the connection listeners.
*/
public ListenerBridge listeners();
/**
* Closes the connection
*/
public void close();
}

View File

@ -0,0 +1,42 @@
package dorkbox.network.connection;
import dorkbox.network.connection.ping.Ping;
public interface ConnectionBridge {
/**
* Sends the message to other listeners INSIDE this endpoint. It does not
* send it to a remote address.
*/
public void self(Object message);
/**
* Sends the message over the network using TCP. (or via LOCAL when it's a
* local channel).
*/
public ConnectionPoint TCP(Object message);
/**
* Sends the message over the network using UDP (or via LOCAL when it's a
* local channel).
*/
public ConnectionPoint UDP(Object message);
/**
* Sends the message over the network using UDT. (or via LOCAL when it's a
* local channel).
*/
public ConnectionPoint UDT(Object message);
/**
* Sends a "ping" packet, trying UDP, then UDT, then TCP (in that order) to measure round trip time to the remote connection.
*
* @return Ping can have a listener attached, which will get called when the ping returns.
*/
public Ping ping();
/**
* Flushes the contents of the TCP/UDP/UDT/etc pipes to the actual transport.
*/
public void flush();
}

View File

@ -0,0 +1,52 @@
package dorkbox.network.connection;
import dorkbox.network.connection.ping.Ping;
public class ConnectionBridgeFlushAlways implements ConnectionBridge {
private final ConnectionBridge originalBridge;
public ConnectionBridgeFlushAlways(ConnectionBridge originalBridge) {
this.originalBridge = originalBridge;
}
@Override
public void self(Object message) {
originalBridge.self(message);
flush();
}
@Override
public ConnectionPoint TCP(Object message) {
ConnectionPoint connection = originalBridge.TCP(message);
connection.flush();
return connection;
}
@Override
public ConnectionPoint UDP(Object message) {
ConnectionPoint connection = originalBridge.UDP(message);
connection.flush();
return connection;
}
@Override
public ConnectionPoint UDT(Object message) {
ConnectionPoint connection = originalBridge.UDT(message);
connection.flush();
return connection;
}
@Override
public Ping ping() {
Ping ping = originalBridge.ping();
flush();
return ping;
}
@Override
public void flush() {
originalBridge.flush();
}
}

View File

@ -0,0 +1,29 @@
package dorkbox.network.connection;
public interface ConnectionBridgeServer {
/**
* Sends the object all server connections over the network using TCP. (or
* via LOCAL when it's a local channel).
*/
public void TCP(Object message);
/**
* Sends the object all server connections over the network using UDP (or
* via LOCAL when it's a local channel).
*/
public void UDP(Object message);
/**
* Sends the object all server connections over the network using UDT. (or
* via LOCAL when it's a local channel).
*/
public void UDT(Object message);
/**
* Exposes methods to send the object to all server connections (except the specified one)
* over the network. (or via LOCAL when it's a local channel).
*/
public ConnectionExceptsBridgeServer except();
}

View File

@ -0,0 +1,23 @@
package dorkbox.network.connection;
public interface ConnectionExceptsBridgeServer {
/**
* Sends the object to all server connections (except the specified one)
* over the network using TCP. (or via LOCAL when it's a local channel).
*/
public void TCP(Connection connection, Object message);
/**
* Sends the object to all server connections (except the specified one)
* over the network using UDP (or via LOCAL when it's a local channel).
*/
public void UDP(Connection connection, Object message);
/**
* Sends the object to all server connections (except the specified one)
* over the network using UDT. (or via LOCAL when it's a local channel).
*/
public void UDT(Connection connection, Object message);
}

View File

@ -0,0 +1,650 @@
package dorkbox.network.connection;
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.local.LocalChannel;
import io.netty.channel.socket.nio.NioDatagramChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.channel.udt.nio.NioUdtByteConnectorChannel;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.ReferenceCountUtil;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
import org.bouncycastle.crypto.params.ParametersWithIV;
import dorkbox.network.connection.idle.IdleBridge;
import dorkbox.network.connection.idle.IdleObjectSender;
import dorkbox.network.connection.idle.IdleSender;
import dorkbox.network.connection.ping.Ping;
import dorkbox.network.connection.ping.PingFuture;
import dorkbox.network.connection.ping.PingMessage;
import dorkbox.network.connection.ping.PingUtil;
import dorkbox.network.connection.wrapper.ChannelNetworkWrapper;
import dorkbox.network.connection.wrapper.ChannelWrapper;
/**
* The "network connection" is established once the registration is validated for TCP/UDP/UDT
*/
@Sharable
public class ConnectionImpl extends ChannelInboundHandlerAdapter
implements Connection, ListenerBridge, ConnectionBridge {
private final org.slf4j.Logger logger;
private final String name;
private AtomicBoolean closeInProgress = new AtomicBoolean(false);
private AtomicBoolean alreadyClosed = new AtomicBoolean(false);
private AtomicBoolean messageInProgress = new AtomicBoolean(false);
private final Object closeInProgressLock = new Object();
private final Object messageInProgressLock = new Object();
private ISessionManager sessionManager;
private ChannelWrapper channelWrapper;
private EndPointWithSerialization endPoint;
private final PingUtil pingUtil;
private volatile PingFuture pingFuture = null;
// used to store connection local listeners (instead of global listeners). Only possible on the server.
private volatile ConnectionManager localListenerManager;
// while on the CLIENT, if the SERVER's ecc key has changed, the client will abort and show an error.
private boolean remoteKeyChanged;
public ConnectionImpl(String name) {
this.name = name;
this.pingUtil = new PingUtil();
this.logger = org.slf4j.LoggerFactory.getLogger(name);
}
/**
* Initialize the connection with any extra info that is needed but was unavailable at the channel construction.
*/
@Override
public void init(EndPointWithSerialization endPoint, Bridge bridge) {
this.endPoint = endPoint;
if (bridge != null) {
this.sessionManager = bridge.sessionManager;
this.channelWrapper = bridge.channelWrapper;
} else {
this.sessionManager = null;
this.channelWrapper = null;
}
if (this.channelWrapper instanceof ChannelNetworkWrapper) {
this.remoteKeyChanged = ((ChannelNetworkWrapper)this.channelWrapper).remoteKeyChanged();
} else {
this.remoteKeyChanged = false;
}
}
/**
* Prepare the channel wrapper, since it doesn't have access to certain fields during it's construction.
*/
@Override
public void prep() {
if (this.channelWrapper != null) {
this.channelWrapper.init();
}
}
/**
* @return the AES key/IV, etc associated with this connection
*/
@Override
public final ParametersWithIV getCryptoParameters() {
return this.channelWrapper.cryptoParameters();
}
/**
* Has the remote ECC public key changed. This can be useful if specific actions are necessary when the key has changed.
*/
@Override
public boolean hasRemoteKeyChanged() {
return this.remoteKeyChanged;
}
/**
* @return the remote address, as a string.
*/
@Override
public String getRemoteHost() {
return this.channelWrapper.getRemoteHost();
}
/**
* @return the name used by the connection
*/
@Override
public String getName() {
return this.name;
}
/**
* @return the connection (TCP or LOCAL) id of this connection.
*/
@Override
public int id() {
return this.channelWrapper.id();
}
/**
* @return the connection (TCP or LOCAL) id of this connection as a HEX string.
*/
@Override
public String idAsHex() {
return Integer.toHexString(this.channelWrapper.id());
}
/**
* Updates the ping times for this connection.
*/
public final void updatePingResponse(PingMessage ping) {
synchronized (this.pingUtil) {
this.pingUtil.updatePing(ping);
if (this.pingFuture != null) {
this.pingFuture.setSuccess(this.pingUtil);
}
}
}
/**
* Sends a "ping" packet, trying UDP, then UDT, then TCP (in that order) to measure round trip time to the remote connection.
*
* @return Ping can have a listener attached, which will get called when the ping returns.
*/
@Override
public final Ping ping() {
synchronized (this.pingUtil) {
if (this.pingFuture != null) {
this.pingFuture.cancel();
}
this.pingFuture = this.channelWrapper.pingFuture();
}
ping0(this.pingUtil.pingMessage());
return this.pingFuture;
}
/**
* INTERNAL USE ONLY. Used to initiate a ping, and to return a ping.
* Sends a ping message attempted in the following order: UDP, UDT, TCP
*/
public final void ping0(PingMessage ping) {
if (this.channelWrapper.udp() != null) {
send().UDP(ping).flush();
} else if (this.channelWrapper.udt() != null) {
send().UDT(ping).flush();
} else {
send().TCP(ping).flush();
}
}
/**
* Returns the last calculated TCP return trip time, or -1 if {@link #updateReturnTripTime()} has never been called or the
* {@link PingMessage} response has not yet been received.
*/
public final int getLastRoundTripTime() {
if (this.pingFuture != null) {
synchronized (this.pingUtil) {
return this.pingFuture.getResponse();
}
} else {
return -1;
}
}
/**
* @return true if this connection is also configured to use UDP
*/
@Override
public final boolean hasUdp() {
return this.channelWrapper.udp() != null;
}
/**
* @return true if this connection is also configured to use UDT
*/
@Override
public final boolean hasUdt() {
return this.channelWrapper.udt() != null;
}
/**
* Expose methods to send objects to a destination.
*/
@Override
public final ConnectionBridge send() {
return this;
}
/**
* Sends the object to other listeners INSIDE this endpoint. It does not send it to a remote address.
*/
@Override
public final void self(Object message) {
this.logger.trace("Sending LOCAL {}", message);
this.sessionManager.notifyOnMessage(this, message);
}
/**
* Sends the object over the network using TCP. (LOCAL channels do not care if its TCP or UDP)
*/
@Override
public final ConnectionPoint TCP(Object message) {
if (!this.closeInProgress.get()) {
this.logger.trace("Sending TCP {}", message);
ConnectionPoint tcp = this.channelWrapper.tcp();
tcp.write(message);
return tcp;
} else {
this.logger.debug("writing TCP while closed: {}", message);
return null;
}
}
/**
* Sends the object over the network using UDP (or via LOCAL when it's a local channel).
*/
@Override
public ConnectionPoint UDP(Object message) {
if (!this.closeInProgress.get()) {
this.logger.trace("Sending UDP {}", message);
ConnectionPoint udp = this.channelWrapper.udp();
udp.write(message);
return udp;
} else {
this.logger.debug("writing UDP while closed: {}", message);
return null;
}
}
/**
* Sends the object over the network using TCP. (LOCAL channels do not care if its TCP or UDP)
*/
@Override
public final ConnectionPoint UDT(Object message) {
if (!this.closeInProgress.get()) {
this.logger.trace("Sending UDT {}", message);
ConnectionPoint udt = this.channelWrapper.udt();
udt.write(message);
return udt;
} else {
this.logger.debug("writing UDT while closed: {}", message);
return null;
}
}
/**
* Flushes the contents of the TCP/UDP/UDT/etc pipes to the actual transport.
*/
@Override
public final void flush() {
this.channelWrapper.flush();
}
/**
* Expose methods to modify the connection listeners.
*/
@Override
public final IdleBridge sendOnIdle(@SuppressWarnings("rawtypes") IdleSender sender) {
listeners().add(sender);
return sender;
}
/**
* Expose methods to modify the connection listeners.
*/
@Override
public final IdleBridge sendOnIdle(Object message) {
@SuppressWarnings({"rawtypes","unchecked"})
IdleObjectSender sender = new IdleObjectSender(message);
listeners().add(sender);
return sender;
}
/**
* Invoked when a {@link Channel} has been idle for a while.
*/
@Override
public void userEventTriggered(ChannelHandlerContext context, Object event) throws Exception {
// if (e.getState() == IdleState.READER_IDLE) {
// e.getChannel().close();
// } else if (e.getState() == IdleState.WRITER_IDLE) {
// e.getChannel().write(new Object());
// } else
if (event instanceof IdleStateEvent) {
if (((IdleStateEvent) event).state() == IdleState.ALL_IDLE) {
this.sessionManager.notifyOnIdle(this);
}
}
super.userEventTriggered(context, event);
}
@Override
public void channelRead(ChannelHandlerContext context, Object message) throws Exception {
channelRead(message);
ReferenceCountUtil.release(message);
}
public void channelRead(Object object) throws Exception {
// prevent close from occurring SMACK in the middle of a message in progress.
// delay close until it's finished.
this.messageInProgress.set(true);
this.sessionManager.notifyOnMessage(this, object);
this.messageInProgress.set(false);
// if we are in the middle of closing, and waiting for the message, it's safe to notify it to continue.
if (this.closeInProgress.get()) {
synchronized (this.messageInProgressLock) {
this.messageInProgressLock.notifyAll();
}
}
}
@Override
public void channelInactive(ChannelHandlerContext context) throws Exception {
// if we are in the middle of a message, hold off.
if (this.messageInProgress.get()) {
synchronized (this.messageInProgressLock) {
try {
this.messageInProgressLock.wait();
} catch (InterruptedException e) {
}
}
}
Channel channel = context.channel();
if (this.logger.isInfoEnabled()) {
String type;
if (channel instanceof NioSocketChannel) {
type = "TCP";
} else if (channel instanceof NioDatagramChannel) {
type = "UDP";
} else if (channel instanceof NioUdtByteConnectorChannel) {
type = "UDT";
} else if (channel instanceof LocalChannel) {
type = "LOCAL";
} else {
type = "UNKNOWN";
}
this.logger.info("Closed remote {} connection: {}", type, channel.remoteAddress().toString());
}
// our master channels are TCP/LOCAL (which are mutually exclusive). Only key disconnect events based on the status of them.
if (channel instanceof NioSocketChannel || channel instanceof LocalChannel) {
// this is because channelInactive can ONLY happen when netty shuts down the channel.
// and connection.close() can be called by the user.
this.sessionManager.connectionDisconnected(this);
// close TCP/UDP/UDT together!
close();
}
synchronized (this.closeInProgressLock) {
this.alreadyClosed.set(true);
this.closeInProgressLock.notify();
}
}
/**
* Closes the connection
*/
@Override
public final void close() {
// only close if we aren't already in the middle of closing.
if (this.closeInProgress.compareAndSet(false, true)) {
int idleTimeout = this.endPoint.getIdleTimeout();
if (idleTimeout == 0) {
// default is 2 second timeout, in milliseconds.
idleTimeout = 2000;
}
// if we are in the middle of a message, hold off.
synchronized (this.messageInProgressLock) {
if (this.messageInProgress.get()) {
try {
this.messageInProgressLock.wait(idleTimeout);
} catch (InterruptedException e) {
}
}
}
// flush any pending messages
this.channelWrapper.flush();
this.channelWrapper.close(this, this.sessionManager);
// want to wait for the "channelInactive" method to FINISH before allowing our current thread to continue!
synchronized (this.closeInProgressLock) {
if (!this.alreadyClosed.get()) {
try {
this.closeInProgressLock.wait(idleTimeout);
} catch (Exception e) {
}
}
}
}
}
@Override
public void exceptionCaught(ChannelHandlerContext context, Throwable cause) throws Exception {
if (!(cause instanceof IOException)) {
Channel channel = context.channel();
// safe to ignore, since it's thrown when we try to interact with a closed socket. Race conditions cause this, and
// it is still safe to ignore.
this.logger.error("Unexpected exception while receiving data from {}", channel.remoteAddress(), cause);
this.sessionManager.connectionError(this, cause);
// the ONLY sockets that can call this are:
// CLIENT TCP or UDP
// SERVER TCP
if (channel.isOpen()) {
channel.close();
}
}
}
/**
* Expose methods to modify the connection listeners.
*/
@Override
public final ListenerBridge listeners() {
return this;
}
/**
* Adds a listener to this connection/endpoint to be notified of
* connect/disconnect/idle/receive(object) events.
* <p>
* If the listener already exists, it is not added again.
* <p>
* When called by a server, NORMALLY listeners are added at the GLOBAL level
* (meaning, I add one listener, and ALL connections are notified of that
* listener.
* <p>
* It is POSSIBLE to add a server connection ONLY (ie, not global) listener
* (via connection.addListener), meaning that ONLY that listener attached to
* the connection is notified on that event (ie, admin type listeners)
*/
@SuppressWarnings("rawtypes")
@Override
public final void add(Listener listener) {
if (this.endPoint instanceof EndPointServer) {
// when we are a server, NORMALLY listeners are added at the GLOBAL level (meaning, I add one listener, and ALL connections
// are notified of that listener.
// it is POSSIBLE to add a local listener (via connection.addListener), meaning that ONLY
// that listener is notified on that event (ie, admin type listeners)
// synchronized because this should be uncommon, and we want to make sure that when the manager
// is empty, we can remove it from this connection.
synchronized (this) {
if (this.localListenerManager == null) {
this.localListenerManager = ((EndPointServer)this.endPoint).addListenerManager(this);
}
this.localListenerManager.add(listener);
}
} else {
this.endPoint.listeners().add(listener);
}
}
/**
* Removes a listener from this connection/endpoint to NO LONGER be notified
* of connect/disconnect/idle/receive(object) events.
* <p>
* When called by a server, NORMALLY listeners are added at the GLOBAL level
* (meaning, I add one listener, and ALL connections are notified of that
* listener.
* <p>
* It is POSSIBLE to remove a server-connection 'non-global' listener (via
* connection.removeListener), meaning that ONLY that listener attached to
* the connection is removed
*/
@SuppressWarnings("rawtypes")
@Override
public final void remove(Listener listener) {
if (this.endPoint instanceof EndPointServer) {
// when we are a server, NORMALLY listeners are added at the GLOBAL level (meaning, I add one listener, and ALL connections
// are notified of that listener.
// it is POSSIBLE to add a local listener (via connection.addListener), meaning that ONLY
// that listener is notified on that event (ie, admin type listeners)
// synchronized because this should be uncommon, and we want to make sure that when the manager
// is empty, we can remove it from this connection.
synchronized (this) {
if (this.localListenerManager != null) {
this.localListenerManager.remove(listener);
if (!this.localListenerManager.hasListeners()) {
((EndPointServer)this.endPoint).removeListenerManager(this);
}
}
}
} else {
this.endPoint.listeners().remove(listener);
}
}
/**
* Removes all registered listeners from this connection/endpoint to NO
* LONGER be notified of connect/disconnect/idle/receive(object) events.
*/
@Override
public final void removeAll() {
if (this.endPoint instanceof EndPointServer) {
// when we are a server, NORMALLY listeners are added at the GLOBAL level (meaning, I add one listener, and ALL connections
// are notified of that listener.
// it is POSSIBLE to add a local listener (via connection.addListener), meaning that ONLY
// that listener is notified on that event (ie, admin type listeners)
// synchronized because this should be uncommon, and we want to make sure that when the manager
// is empty, we can remove it from this connection.
synchronized (this) {
if (this.localListenerManager != null) {
this.localListenerManager.removeAll();
this.localListenerManager = null;
((EndPointServer)this.endPoint).removeListenerManager(this);
}
}
} else {
this.endPoint.listeners().removeAll();
}
}
/**
* Removes all registered listeners (of the object type) from this
* connection/endpoint to NO LONGER be notified of
* connect/disconnect/idle/receive(object) events.
*/
@Override
public final void removeAll(Class<?> classType) {
if (this.endPoint instanceof EndPointServer) {
// when we are a server, NORMALLY listeners are added at the GLOBAL level (meaning, I add one listener, and ALL connections
// are notified of that listener.
// it is POSSIBLE to add a local listener (via connection.addListener), meaning that ONLY
// that listener is notified on that event (ie, admin type listeners)
// synchronized because this should be uncommon, and we want to make sure that when the manager
// is empty, we can remove it from this connection.
synchronized (this) {
if (this.localListenerManager != null) {
this.localListenerManager.removeAll(classType);
if (!this.localListenerManager.hasListeners()) {
this.localListenerManager = null;
((EndPointServer)this.endPoint).removeListenerManager(this);
}
}
}
} else {
this.endPoint.listeners().removeAll(classType);
}
}
@Override
public String toString() {
return this.channelWrapper.toString();
}
@Override
public int hashCode() {
return id();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
ConnectionImpl other = (ConnectionImpl) obj;
if (this.channelWrapper == null) {
if (other.channelWrapper != null) {
return false;
}
} else if (!this.channelWrapper.equals(other.channelWrapper)) {
return false;
}
if (this.name == null) {
if (other.name != null) {
return false;
}
} else if (!this.name.equals(other.name)) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,476 @@
package dorkbox.network.connection;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import dorkbox.network.rmi.RmiMessages;
import dorkbox.network.util.ConcurrentHashMapFactory;
import dorkbox.util.ClassHelper;
//note that we specifically DO NOT implement equals/hashCode, because we cannot create two separate
// objects that are somehow equal to each other.
public class ConnectionManager implements ListenerBridge, ISessionManager {
private volatile ConcurrentHashMapFactory<Type, CopyOnWriteArrayList<Listener<Connection, Object>>> listeners;
private volatile ConcurrentHashMapFactory<Connection, ConnectionManager> localManagers;
private volatile CopyOnWriteArrayList<Connection> connections = new CopyOnWriteArrayList<Connection>();
/** Used by the listener subsystem to determine types. */
private final Class<?> baseClass;
protected final org.slf4j.Logger logger;
private final String name;
volatile boolean shutdown = false;
public ConnectionManager(String name, Class<?> baseClass) {
this.name = name;
logger = org.slf4j.LoggerFactory.getLogger(name);
this.baseClass = baseClass;
listeners = new ConcurrentHashMapFactory<Type, CopyOnWriteArrayList<Listener<Connection, Object>>>() {
private static final long serialVersionUID = 8404650379739727012L;
@Override
public CopyOnWriteArrayList<Listener<Connection, Object>> createNewOject(Object... args) {
return new CopyOnWriteArrayList<Listener<Connection, Object>>();
}
};
localManagers = new ConcurrentHashMapFactory<Connection, ConnectionManager>() {
private static final long serialVersionUID = -1656860453153611896L;
@Override
public ConnectionManager createNewOject(Object... args) {
return new ConnectionManager(ConnectionManager.this.name + "-" + args[0] + " Specific", ConnectionManager.this.baseClass);
}
};
}
/**
* Adds a listener to this connection/endpoint to be notified of
* connect/disconnect/idle/receive(object) events.
* <p>
* If the listener already exists, it is not added again.
* <p>
* When called by a server, NORMALLY listeners are added at the GLOBAL level
* (meaning, I add one listener, and ALL connections are notified of that
* listener.
* <p>
* It is POSSIBLE to add a server connection ONLY (ie, not global) listener
* (via connection.addListener), meaning that ONLY that listener attached to
* the connection is notified on that event (ie, admin type listeners)
*/
@SuppressWarnings("rawtypes")
@Override
public final void add(Listener listener) {
if (listener == null) {
throw new IllegalArgumentException("listener cannot be null.");
}
// find the class that uses Listener.class.
Class<?> clazz = listener.getClass();
while (clazz.getSuperclass() != Listener.class) {
clazz = clazz.getSuperclass();
}
// this is the connection generic parameter for the listener
Class<?> genericClass = ClassHelper.getGenericParameterAsClassForSuperClass(clazz, 0);
// if we are null, it means that we have no generics specified for our listener!
if (genericClass == baseClass || genericClass == null) {
// we are the base class, so we are fine.
addListener0(listener);
return;
} else if (ClassHelper.hasInterface(Connection.class, genericClass) &&
!ClassHelper.hasParentClass(baseClass, genericClass)) {
// now we must make sure that the PARENT class is NOT the base class. ONLY the base class is allowed!
addListener0(listener);
return;
}
// didn't successfully add the listener.
throw new RuntimeException("Unable to add incompatible connection types as a listener!");
}
/**
* INTERNAL USE ONLY
*/
@SuppressWarnings({"unchecked","rawtypes"})
private final void addListener0(Listener listener) {
Class<?> type = listener.getObjectType();
CopyOnWriteArrayList<Listener<Connection, Object>> list = listeners.getOrCreate(type);
list.addIfAbsent(listener);
logger.trace("listener added: {} <{}>", listener.getClass().getName(), listener.getObjectType());
}
/**
* Removes a listener from this connection/endpoint to NO LONGER be notified
* of connect/disconnect/idle/receive(object) events.
* <p>
* When called by a server, NORMALLY listeners are added at the GLOBAL level
* (meaning, I add one listener, and ALL connections are notified of that
* listener.
* <p>
* It is POSSIBLE to remove a server-connection 'non-global' listener (via
* connection.removeListener), meaning that ONLY that listener attached to
* the connection is removed
*/
@SuppressWarnings("rawtypes")
@Override
public final void remove(Listener listener) {
if (listener == null) {
throw new IllegalArgumentException("listener cannot be null.");
}
Class<?> type = listener.getObjectType();
CopyOnWriteArrayList<Listener<Connection, Object>> list = listeners.get(type);
if (list != null) {
list.remove(listener);
}
logger.trace("listener removed: {} <{}>", listener.getClass().getName(), listener.getObjectType());
}
/**
* Removes all registered listeners from this connection/endpoint to NO
* LONGER be notified of connect/disconnect/idle/receive(object) events.
*/
@Override
public final void removeAll() {
listeners.clear();
logger.trace("all listeners removed !!");
}
/**
* Removes all registered listeners (of the object type) from this
* connection/endpoint to NO LONGER be notified of
* connect/disconnect/idle/receive(object) events.
*/
@Override
public final void removeAll(Class<?> classType) {
if (classType == null) {
throw new IllegalArgumentException("classType cannot be null.");
}
listeners.remove(classType);
logger.trace("all listeners removed for type: {}", classType.getClass().getName());
}
/**
* Invoked when a message object was received from a remote peer.
* <p>
* If data is sent in response to this event, the connection data is automatically flushed to the wire. If the data is sent in a separate thread,
* {@link connection.send().flush()} must be called manually.
*
* {@link ISessionManager}
*/
@Override
public final void notifyOnMessage(Connection connection, Object message) {
notifyOnMessage(connection, message, false);
}
private final void notifyOnMessage(Connection connection, Object message, boolean foundListener) {
Class<?> objectType = message.getClass();
// this is the GLOBAL version (unless it's the call from below, then it's the connection scoped version)
CopyOnWriteArrayList<Listener<Connection, Object>> list = listeners.get(objectType);
if (list != null) {
for (Listener<Connection, Object> listener : list) {
if (shutdown) {
return;
}
listener.received(connection, message);
}
foundListener = true;
}
if (!(message instanceof RmiMessages)) {
// we march through all super types of the object, and find the FIRST set
// of listeners that are registered and cast it as that, and notify the method.
// NOTICE: we do NOT call ALL TYPE -- meaning, if we have Object->Foo->Bar
// and have listeners for Object and Foo
// we will call Bar (from the above code)
// we will call Foo (from this code)
// we will NOT call Object (since we called Foo). If Foo was not registered, THEN we would call object!
list = null;
objectType = objectType.getSuperclass();
while (objectType != null) {
// check to see if we have what we are looking for in our CURRENT class
list = listeners.get(objectType);
if (list != null) {
break;
}
// NO MATCH, so walk up.
objectType = objectType.getSuperclass();
}
if (list != null) {
for (Listener<Connection, Object> listener : list) {
if (shutdown) {
return;
}
listener.received(connection, message);
foundListener = true;
}
} else if (!foundListener) {
logger.debug("----------- LISTENER NOT REGISTERED FOR TYPE: {}", message.getClass().getSimpleName());
}
}
// only run a flush once
if (foundListener) {
connection.send().flush();
}
// now have to account for additional connection listener managers (non-global).
ConnectionManager localManager = localManagers.get(connection);
if (localManager != null) {
// if we found a listener during THIS method call, we need to let the NEXT method call know,
// so it doesn't spit out error for not handling a message (since that message MIGHT have
// been found in this method).
localManager.notifyOnMessage(connection, message, foundListener);
}
}
/**
* Invoked when a Connection has been idle for a while.
*
* {@link ISessionManager}
*/
@Override
public final void notifyOnIdle(Connection connection) {
Set<Entry<Type, CopyOnWriteArrayList<Listener<Connection, Object>>>> entrySet = listeners.entrySet();
CopyOnWriteArrayList<Listener<Connection,Object>> list;
for (Entry<Type, CopyOnWriteArrayList<Listener<Connection, Object>>> entry : entrySet) {
list = entry.getValue();
if (list != null) {
for (Listener<Connection,Object> listener : list) {
if (shutdown) {
return;
}
listener.idle(connection);
}
connection.send().flush();
}
}
// now have to account for additional (local) listener managers.
ConnectionManager localManager = localManagers.get(connection);
if (localManager != null) {
localManager.notifyOnIdle(connection);
}
}
/**
* Invoked when a {@link Channel} is open, bound to a local address, and connected to a remote address.
*
* {@link ISessionManager}
*/
@Override
public void connectionConnected(Connection connection) {
// only TCP channels are passed in.
// create a new connection!
connections.add(connection);
try {
Set<Entry<Type, CopyOnWriteArrayList<Listener<Connection, Object>>>> entrySet = listeners.entrySet();
CopyOnWriteArrayList<Listener<Connection,Object>> list;
for (Entry<Type, CopyOnWriteArrayList<Listener<Connection, Object>>> entry : entrySet) {
list = entry.getValue();
if (list != null) {
for (Listener<Connection,Object> listener : list) {
if (shutdown) {
return;
}
listener.connected(connection);
}
connection.send().flush();
}
}
// now have to account for additional (local) listener managers.
ConnectionManager localManager = localManagers.get(connection);
if (localManager != null) {
localManager.connectionConnected(connection);
}
} catch (Throwable t) {
connectionError(connection, t);
}
}
/**
* Invoked when a {@link Channel} was disconnected from its remote peer.
*
* {@link ISessionManager}
*/
@Override
public void connectionDisconnected(Connection connection) {
Set<Entry<Type, CopyOnWriteArrayList<Listener<Connection, Object>>>> entrySet = listeners.entrySet();
CopyOnWriteArrayList<Listener<Connection,Object>> list;
for (Entry<Type, CopyOnWriteArrayList<Listener<Connection, Object>>> entry : entrySet) {
list = entry.getValue();
if (list != null) {
for (Listener<Connection, Object> listener : list) {
if (shutdown) {
return;
}
listener.disconnected(connection);
}
}
}
// now have to account for additional (local) listener managers.
ConnectionManager localManager = localManagers.get(connection);
if (localManager != null) {
localManager.connectionDisconnected(connection);
// remove myself from the "global" listeners so we can have our memory cleaned up.
localManagers.remove(connection);
}
connections.remove(connection);
}
/**
* Invoked when there is an error of some kind during the up/down stream process
*
* {@link ISessionManager}
*/
@Override
public void connectionError(Connection connection, Throwable throwable) {
Set<Entry<Type, CopyOnWriteArrayList<Listener<Connection, Object>>>> entrySet = listeners.entrySet();
CopyOnWriteArrayList<Listener<Connection,Object>> list;
for (Entry<Type, CopyOnWriteArrayList<Listener<Connection, Object>>> entry : entrySet) {
list = entry.getValue();
if (list != null) {
for (Listener<Connection, Object> listener : list) {
if (shutdown) {
return;
}
listener.error(connection, throwable);
}
connection.send().flush();
}
}
// now have to account for additional (local) listener managers.
ConnectionManager localManager = localManagers.get(connection);
if (localManager != null) {
localManager.connectionError(connection, throwable);
}
}
/**
* Returns a non-modifiable list of active connections
*
* {@link ISessionManager}
*/
@Override
public List<Connection> getConnections() {
return Collections.unmodifiableList(connections);
}
final ConnectionManager addListenerManager(Connection connection) {
// when we are a server, NORMALLY listeners are added at the GLOBAL level (meaning, I add one listener, and ALL connections
// are notified of that listener.
// it is POSSIBLE to add a connection-specfic listener (via connection.addListener), meaning that ONLY
// that listener is notified on that event (ie, admin type listeners)
ConnectionManager lm = localManagers.getOrCreate(connection, connection.toString());
logger.debug("Connection specific Listener Manager added on connection: {}", connection);
return lm;
}
final void removeListenerManager(Connection connection) {
localManagers.remove(connection);
}
/**
* BE CAREFUL! Only for internal use!
*
* @return Returns a FAST list of active connections.
*/
public final Collection<Connection> getConnections0() {
return connections;
}
/**
* BE CAREFUL! Only for internal use!
*
* @return Returns a FAST first connection (for client!).
*/
public final Connection getConnection0() {
if (connections.iterator().hasNext()) {
return connections.iterator().next();
} else {
throw new RuntimeException("Not connected to a remote computer. Unable to continue!");
}
}
/**
* BE CAREFUL! Only for internal use!
*
* @return a boolean indicating if there are any listeners registered with this manager.
*/
final boolean hasListeners() {
return listeners.isEmpty();
}
/**
* Closes all associated resources/threads/connections
*/
final void stop() {
shutdown = true;
// disconnect the sessions
closeConnections();
listeners.clear();
}
/**
* Close all connections ONLY
*/
final void closeConnections() {
// close the sessions
Iterator<Connection> iterator = connections.iterator();
while (iterator.hasNext()) {
Connection connection = iterator.next();
// Close the connection. Make sure the close operation ends because
// all I/O operations are asynchronous in Netty.
// Necessary otherwise workers won't close.
connection.close();
}
connections.clear();
}
}

View File

@ -0,0 +1,14 @@
package dorkbox.network.connection;
public interface ConnectionPoint {
/**
* Writes data to the pipe. <b>DOES NOT FLUSH</b> the pipes to the wire!
*/
public void write(Object object);
/**
* Flushes the contents of the TCP/UDP/UDT/etc pipes to the wire.
*/
public void flush();
}

View File

@ -0,0 +1,499 @@
package dorkbox.network.connection;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.util.concurrent.EventExecutor;
import io.netty.util.concurrent.Future;
import io.netty.util.internal.PlatformDependent;
import java.security.AccessControlException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import dorkbox.network.ConnectionOptions;
import dorkbox.network.connection.registration.MetaChannel;
import dorkbox.network.rmi.RmiBridge;
import dorkbox.network.util.InitializationException;
import dorkbox.network.util.SecurityException;
import dorkbox.network.util.entropy.Entropy;
import dorkbox.network.util.entropy.SimpleEntropy;
import dorkbox.network.util.primativeCollections.IntMap;
import dorkbox.network.util.primativeCollections.IntMap.Entries;
import dorkbox.network.util.store.NullSettingsStore;
import dorkbox.network.util.store.SettingsStore;
import dorkbox.network.util.udt.UdtEndpointProxy;
import dorkbox.util.crypto.Crypto;
/** represents the base of a client/server end point */
public abstract class EndPoint {
// If TCP and UDP both fill the pipe, THERE WILL BE FRAGMENTATION and dropped UDP packets!
// it results in severe UDP packet loss and contention.
//
// http://www.isoc.org/INET97/proceedings/F3/F3_1.HTM
// also, a google search on just "INET97/proceedings/F3/F3_1.HTM" turns up interesting problems.
// Usually it's with ISPs.
// 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??
protected static final String shutdownHookName = "::SHUTDOWN_HOOK::";
protected static final String stopTreadName = "::STOP_THREAD::";
/**
* this can be changed to a more specialized value, if necessary
*/
public static int DEFAULT_THREAD_POOL_SIZE = Runtime.getRuntime().availableProcessors() * 2;
public static final String LOCAL_CHANNEL = "local_channel";
/**
* The amount of time in milli-seconds to wait for this endpoint to close all
* {@link Channel}s and shutdown gracefully.
*/
public static long maxShutdownWaitTimeInMilliSeconds = 2000L; // in milliseconds
/**
* The default size for UDP packets is 768 bytes.
*
* You could increase or decrease this value to avoid truncated packets
* or to improve memory footprint respectively.
*
* 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.
*
* 512 is recommended to prevent fragmentation.
* This can be set higher on an internal lan! (or use UDT to make UDP transfers easy)
*/
public static int udpMaxSize = 512;
public static final boolean isAndroid;
static {
isAndroid = PlatformDependent.isAndroid();
try {
// doesn't work in 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());
// need to make sure UDT uses our loader instead of the default loader
try {
// all of this must be proxied to another class, so THIS class doesn't have unmet dependencies.
// Annoying and abusing the classloader, but it works well.
Class.forName("com.barchart.udt.nio.SelectorProviderUDT");
UdtEndpointProxy.setLibraryLoaderClassName();
} catch (Throwable ignored) {}
} catch (AccessControlException ignored) {}
}
protected final org.slf4j.Logger logger;
protected final String name;
// make sure that the endpoint is closed on JVM shutdown (if it's still open at that point in time)
protected Thread shutdownHook;
protected final RegistrationWrapper registrationWrapper;
// The remote object space is used by RMI.
protected RmiBridge remoteObjectSpace = null;
// the eventLoop groups are used to track and manage the event loops for startup/shutdown
private List<EventLoopGroup> eventLoopGroups = new ArrayList<EventLoopGroup>(8);
private List<ChannelFuture> shutdownChannelList = new LinkedList<ChannelFuture>();
private final Semaphore blockUntilDone = new Semaphore(0);
protected final Object shutdownInProgress = new Object();
protected AtomicBoolean isConnected = new AtomicBoolean(false);
/** in milliseconds. default is disabled! */
private volatile int idleTimeout = 0;
final ECPrivateKeyParameters privateKey;
final ECPublicKeyParameters publicKey;
final SecureRandom secureRandom;
SettingsStore propertyStore;
public EndPoint(String name, ConnectionOptions options) throws InitializationException, SecurityException {
this.name = name;
this.logger = org.slf4j.LoggerFactory.getLogger(name);
this.registrationWrapper = new RegistrationWrapper(this);
// we have to be able to specify WHAT property store we want to use, since it can change!
if (options.settingsStore == null) {
this.propertyStore = new PropertyStore();
} else {
this.propertyStore = options.settingsStore;
}
// null it out, since it is sensitive!
options.settingsStore = null;
if (!(this.propertyStore instanceof NullSettingsStore)) {
// initialize the private/public keys used for negotiating ECC handshakes
// these are ONLY used for IP connections. LOCAL connections do not need a handshake!
ECPrivateKeyParameters privateKey = this.propertyStore.getPrivateKey();
ECPublicKeyParameters publicKey = this.propertyStore.getPublicKey();
if (privateKey == null || publicKey == null) {
try {
Object entropy = Entropy.init(SimpleEntropy.class);
if (!(entropy instanceof SimpleEntropy)) {
System.err.println("There are no ECC keys for the " + name + " yet. Please press keyboard keys (numbers/letters/etc) to generate entropy.");
System.err.flush();
}
// seed our RNG based off of this and create our ECC keys
byte[] seedBytes = Entropy.get();
SecureRandom secureRandom = new SecureRandom(seedBytes);
secureRandom.nextBytes(seedBytes);
System.err.println("Now generating ECC (" + Crypto.ECC.p521_curve + ") keys. Please wait!");
AsymmetricCipherKeyPair generateKeyPair = Crypto.ECC.generateKeyPair(Crypto.ECC.p521_curve, new SecureRandom(seedBytes));
privateKey = (ECPrivateKeyParameters) generateKeyPair.getPrivate();
publicKey = (ECPublicKeyParameters) generateKeyPair.getPublic();
// save to properties file
this.propertyStore.savePrivateKey(privateKey);
this.propertyStore.savePublicKey(publicKey);
System.err.println("Done with ECC keys!");
} catch (Exception e) {
String message = "Unable to initialize/generate ECC keys. FORCED SHUTDOWN.";
this.logger.error(message);
throw new InitializationException(message);
}
}
this.privateKey = privateKey;
this.publicKey = publicKey;
} else {
this.privateKey = null;
this.publicKey = null;
}
this.secureRandom = new SecureRandom(this.propertyStore.getSalt());
this.shutdownHook = new Thread() {
@Override
public void run() {
EndPoint.this.stop();
}
};
this.shutdownHook.setName(shutdownHookName);
Runtime.getRuntime().addShutdownHook(this.shutdownHook);
}
/**
* Returns the property store used by this endpoint. The property store can store via properties,
* a database, etc, or can be a "null" property store, which does nothing
*/
@SuppressWarnings("unchecked")
public <T extends SettingsStore> T getPropertyStore() {
return (T) this.propertyStore;
}
/**
* TODO maybe remove this? method call is used by jetty ssl
* @return the ECC public key in use by this endpoint
*/
public ECPrivateKeyParameters getPrivateKey() {
return this.privateKey;
}
/**
* TODO maybe remove this? method call is used by jetty ssl
* @return the ECC private key in use by this endpoint
*/
public ECPublicKeyParameters getPublicKey() {
return this.publicKey;
}
/**
* Internal call by the pipeline to notify the client to continue registering the different session protocols.
* The server does not use this.
*/
protected boolean continueRegistration0() {
return true;
}
/**
* 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 idleTimeout) {
this.idleTimeout = idleTimeout;
}
/**
* The amount of milli-seconds that must elapse with no read or write before {@link Listener.idle()}
* will be triggered
*/
public int getIdleTimeout() {
return this.idleTimeout;
}
/**
* Return the connection status of this endpoint.
* <p>
* Once a server has connected to ANY client, it will always return true.
*/
public boolean isConnected() {
return this.isConnected.get();
}
/**
* Add a channel future to be tracked and managed for shutdown.
*/
protected final void manageForShutdown(ChannelFuture future) {
this.shutdownChannelList.add(future);
}
/**
* Add an eventloop group to be tracked & managed for shutdown
*/
protected final void manageForShutdown(EventLoopGroup loopGroup) {
this.eventLoopGroups.add(loopGroup);
}
/**
* Closes all connections ONLY (keeps the server/client running)
*/
public void close() {
// give a chance to other threads.
Thread.yield();
this.isConnected.set(false);
}
/**
* Closes all associated resources/threads/connections
*/
public final void stop() {
// check to make sure we are in our OWN thread, otherwise, this thread will never exit -- because it will wait indefinitely
// for itself to finish (since it blocks itself).
// This occurs when calling stop from within a listener callback.
Thread currentThread = Thread.currentThread();
String threadName = currentThread.getName();
boolean inEventThread = !threadName.equals(shutdownHookName) && !threadName.equals(stopTreadName);
// we must also account for the shutdown hook calling this!
// if we are in the shutdown hook, then we cannot possibly be in our event thread
if (inEventThread) {
inEventThread = false;
// we need to test to see if our current thread is in ANY of the event group threads. If it IS, then we risk deadlocking!
synchronized (this.eventLoopGroups) {
for (EventLoopGroup loopGroup : this.eventLoopGroups) {
if (!inEventThread) {
inEventThread = checkInEventGroup(currentThread, loopGroup);
break;
}
}
}
}
if (!inEventThread) {
stopInThread();
} else {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
EndPoint.this.stopInThread();
}
});
thread.setDaemon(false);
thread.setName(stopTreadName);
thread.start();
}
}
// This actually does the "stopping", since there is some logic to making sure we don't deadlock, this is important
private final void stopInThread () {
// tell the blocked "bind" method that it may continue (and exit)
this.blockUntilDone.release();
// make sure we are not trying to stop during a startup procedure.
// This will wait until we have finished starting up/shutting down.
synchronized (this.shutdownInProgress) {
close();
// there is no need to call "stop" again if we close the connection.
// however, if this is called WHILE from the shutdown hook, blammo! problems!
// Also, you can call client/server.stop from another thread, which is run when the JVM is shutting down
// (as there is nothing left to do), and also have problems.
if (!Thread.currentThread().getName().equals(shutdownHookName)) {
try {
Runtime.getRuntime().removeShutdownHook(this.shutdownHook);
} catch (Exception e) {
// ignore
}
}
stopExtraActions();
// Sometimes there might be "lingering" connections (ie, halfway though registration) that need to be closed.
long maxShutdownWaitTimeInMilliSeconds = EndPoint.maxShutdownWaitTimeInMilliSeconds;
try {
IntMap<MetaChannel> channelMap = this.registrationWrapper.getAndLockChannelMap();
Entries<MetaChannel> entries = channelMap.entries();
while (entries.hasNext()) {
MetaChannel metaChannel = entries.next().value;
metaChannel.close(maxShutdownWaitTimeInMilliSeconds);
}
channelMap.clear();
} finally {
this.registrationWrapper.releaseChannelMap();
}
// shutdown the database store
this.propertyStore.shutdown();
// now we stop all of our channels
for (ChannelFuture f : this.shutdownChannelList) {
Channel channel = f.channel();
channel.close().awaitUninterruptibly(maxShutdownWaitTimeInMilliSeconds);
channel.closeFuture().syncUninterruptibly();
}
// we have to clear the shutdown list.
this.shutdownChannelList.clear();
// we want to WAIT until after the event executors have completed shutting down.
List<Future<?>> shutdownThreadList = new LinkedList<Future<?>>();
for (EventLoopGroup loopGroup : this.eventLoopGroups) {
shutdownThreadList.add(loopGroup.shutdownGracefully());
}
// now wait for them to finish!
for (Future<?> f : shutdownThreadList) {
f.awaitUninterruptibly(maxShutdownWaitTimeInMilliSeconds);
}
// when the eventloop closes, the associated selectors are ALSO closed!
}
}
/**
* Extra actions to perform when stopping this endpoint.
*/
protected void stopExtraActions() {
}
public String getName() {
return this.name;
}
/**
* Determines if the specified thread (usually the current thread) is a member of a group's threads.
*/
protected static final boolean checkInEventGroup(Thread currentThread, EventLoopGroup group) {
if (group != null) {
Set<EventExecutor> children = group.children();
for (EventExecutor e : children) {
if (e.inEventLoop(currentThread)) {
return true;
}
}
}
return false;
}
/**
* Blocks the current thread until the client has been stopped.
* @param blockUntilTerminate if TRUE, then this endpoint will block until STOP is called, otherwise it will not block
*/
public final void waitForStop(boolean blockUntilTerminate) {
if (blockUntilTerminate) {
// we now BLOCK until the stop method is called.
try {
this.blockUntilDone.acquire();
} catch (InterruptedException e) {
}
}
}
@Override
public String toString() {
return "EndPoint [" + this.name + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (this.name == null ? 0 : this.name.hashCode());
result = prime * result + (this.privateKey == null ? 0 : this.privateKey.hashCode());
result = prime * result + (this.publicKey == null ? 0 : this.publicKey.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
EndPoint other = (EndPoint) obj;
if (this.name == null) {
if (other.name != null) {
return false;
}
} else if (!this.name.equals(other.name)) {
return false;
}
if (this.privateKey == null) {
if (other.privateKey != null) {
return false;
}
} else if (!Crypto.ECC.compare(this.privateKey, other.privateKey)) {
return false;
}
if (this.publicKey == null) {
if (other.publicKey != null) {
return false;
}
} else if (!Crypto.ECC.compare(this.publicKey, other.publicKey)) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,56 @@
package dorkbox.network.connection;
import dorkbox.network.ConnectionOptions;
import dorkbox.network.util.InitializationException;
import dorkbox.network.util.SecurityException;
/**
* This serves the purpose of making sure that specific methods are not available to the end user.
*/
public class EndPointClient extends EndPointWithSerialization {
protected final Object registrationLock = new Object();
protected volatile boolean registrationComplete = false;
public EndPointClient(String name, ConnectionOptions options) throws InitializationException, SecurityException {
super(name, options);
}
/**
* 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 boolean continueRegistration0() {
// we need to cache the value, since it can change in a different thread before we have the chance to return the value.
boolean complete = registrationComplete;
// notify the block, but only if we are not ready.
if (!complete) {
synchronized (registrationLock) {
registrationLock.notifyAll();
}
}
logger.trace("Registered protocol from server.");
// only let us continue with connections (this starts up the client/server implementations) once ALL of the
// bootstraps have connected
return complete;
}
/**
* Internal (to the networking stack) to notify the client that registration has completed. This is necessary because the client
* will BLOCK until it has successfully registered it's connections.
*/
@Override
protected final void connectionConnected0(Connection connection) {
// invokes the listener.connection() method, and initialize the connection channels with whatever extra info they might need.
super.connectionConnected0(connection);
// notify the block
synchronized (registrationLock) {
registrationLock.notifyAll();
}
}
}

View File

@ -0,0 +1,52 @@
package dorkbox.network.connection;
import dorkbox.network.ConnectionOptions;
import dorkbox.network.util.InitializationException;
import dorkbox.network.util.SecurityException;
/**
* This serves the purpose of making sure that specific methods are not available to the end user.
*/
public class EndPointServer extends EndPointWithSerialization {
private ServerConnectionBridge serverConnections;
public EndPointServer(String name, ConnectionOptions options) throws InitializationException, SecurityException {
super(name, options);
serverConnections = new ServerConnectionBridge(connectionManager);
}
/**
* Expose methods to send objects to a destination.
*/
public ConnectionBridgeServer send() {
return serverConnections;
}
/**
* When called by a server, NORMALLY listeners are added at the GLOBAL level (meaning, I add one listener,
* and ALL connections are notified of that listener.
* <br>
* It is POSSIBLE to add a server-connection 'local' listener (via connection.addListener), meaning that ONLY
* that listener attached to the connection is notified on that event (ie, admin type listeners)
*
* @return a newly created listener manager for the connection
*/
final ConnectionManager addListenerManager(Connection connection) {
return connectionManager.addListenerManager(connection);
}
/**
* When called by a server, NORMALLY listeners are added at the GLOBAL level (meaning, I add one listener,
* and ALL connections are notified of that listener.
* <br>
* It is POSSIBLE to remove a server-connection 'local' listener (via connection.removeListener), meaning that ONLY
* that listener attached to the connection is removed
*
* This removes the listener manager for that specific connection
*/
final void removeListenerManager(Connection connection) {
connectionManager.removeListenerManager(connection);
}
}

View File

@ -0,0 +1,228 @@
package dorkbox.network.connection;
import java.util.Collection;
import java.util.List;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.IESParameters;
import org.bouncycastle.crypto.params.IESWithCipherParameters;
import dorkbox.network.ConnectionOptions;
import dorkbox.network.connection.ping.PingListener;
import dorkbox.network.connection.ping.PingMessage;
import dorkbox.network.connection.registration.MetaChannel;
import dorkbox.network.connection.registration.Registration;
import dorkbox.network.connection.wrapper.ChannelLocalWrapper;
import dorkbox.network.connection.wrapper.ChannelNetworkWrapper;
import dorkbox.network.connection.wrapper.ChannelWrapper;
import dorkbox.network.pipeline.KryoEncoder;
import dorkbox.network.pipeline.KryoEncoderCrypto;
import dorkbox.network.rmi.RmiBridge;
import dorkbox.network.util.InitializationException;
import dorkbox.network.util.KryoSerializationManager;
import dorkbox.network.util.SecurityException;
import dorkbox.network.util.SerializationManager;
import dorkbox.util.crypto.serialization.EccPrivateKeySerializer;
import dorkbox.util.crypto.serialization.EccPublicKeySerializer;
import dorkbox.util.crypto.serialization.IesParametersSerializer;
import dorkbox.util.crypto.serialization.IesWithCipherParametersSerializer;
public class EndPointWithSerialization extends EndPoint {
protected final ConnectionManager connectionManager;
protected final SerializationManager serializationManager;
public EndPointWithSerialization(String name, ConnectionOptions options) throws InitializationException, SecurityException {
super(name, options);
if (options.serializationManager != null) {
this.serializationManager = options.serializationManager;
} else {
this.serializationManager = new KryoSerializationManager();
}
// we don't care about un-instantiated/constructed members, since the class type is the only interest.
connectionManager = new ConnectionManager(name, connection0(null).getClass());
// setup our TCP kryo encoders
registrationWrapper.setKryoTcpEncoder(new KryoEncoder(serializationManager));
registrationWrapper.setKryoTcpCryptoEncoder(new KryoEncoderCrypto(serializationManager));
this.serializationManager.setReferences(false);
this.serializationManager.setRegistrationRequired(true);
this.serializationManager.register(PingMessage.class);
this.serializationManager.register(byte[].class);
this.serializationManager.register(IESParameters.class, new IesParametersSerializer());
this.serializationManager.register(IESWithCipherParameters.class, new IesWithCipherParametersSerializer());
this.serializationManager.register(ECPublicKeyParameters.class, new EccPublicKeySerializer());
this.serializationManager.register(ECPrivateKeyParameters.class, new EccPrivateKeySerializer());
this.serializationManager.register(Registration.class);
// add the ping listener (internal use only!)
connectionManager.add(new PingListener(name));
Runtime.getRuntime().removeShutdownHook(shutdownHook);
shutdownHook = new Thread() {
@Override
public void run() {
// connectionManager.shutdown accurately reflects the state of the app. Safe to use here
if (connectionManager != null && !connectionManager.shutdown) {
EndPointWithSerialization.this.stop();
}
}
};
}
/**
* Returns the serialization wrapper if there is an object type that needs to be added outside of the basics.
*/
public SerializationManager getSerialization() {
return serializationManager;
}
/**
* Creates the remote (RMI) object space for this endpoint.
* <p>
* This method is safe, and is recommended. Make sure to call it BEFORE a connection is established, as
* there is some housekeeping that is necessary BEFORE a connection is actually connected..
*/
public RmiBridge getRmiBridge() {
synchronized (this) {
if (remoteObjectSpace == null) {
if (isConnected()) {
throw new RuntimeException("Cannot create a remote object space after the remote endpoint has already connected!");
}
remoteObjectSpace = new RmiBridge(name);
}
}
return remoteObjectSpace;
}
/**
* This method allows the connections used by the client/server to be subclassed (custom implementations).
* <p>
* As this is for the network stack, the new connection type MUST subclass {@link Connection}
*
* @param bridge null when retrieving the subclass type (internal use only). Non-null when creating a new (and real) connection.
* @return a new network connection
*/
public ConnectionImpl newConnection(String name) {
return new ConnectionImpl(name);
}
/**
* Internal call by the pipeline when:
* - creating a new network connection
* - when determining the baseClass for listeners
*
* @param metaChannel can be NULL (when getting the baseClass)
*/
protected final ConnectionImpl connection0(MetaChannel metaChannel) {
ConnectionImpl connection;
// setup the extras needed by the network connection.
// These properties are ASSGINED in the same thread that CREATED the object. Only the AES info needs to be
// volatile since it is the only thing that changes.
if (metaChannel != null) {
ChannelWrapper wrapper;
if (metaChannel.localChannel != null) {
wrapper = new ChannelLocalWrapper(metaChannel);
} else {
if (this instanceof EndPointServer) {
wrapper = new ChannelNetworkWrapper(metaChannel, registrationWrapper);
} else {
wrapper = new ChannelNetworkWrapper(metaChannel, null);
}
}
connection = newConnection(name);
// now initialize the connection channels with whatever extra info they might need.
connection.init(this, new Bridge(wrapper, connectionManager));
metaChannel.connection = connection;
// notify our remote object space that it is able to receive method calls.
synchronized (this) {
if (remoteObjectSpace != null) {
remoteObjectSpace.addConnection(connection);
}
}
} else {
// getting the baseClass
// have to add the networkAssociate to a map of "connected" computers
connection = newConnection(name);
}
return connection;
}
/**
* Internal call by the pipeline to notify the "Connection" object that it has "connected", meaning that modifications
* to the pipeline are finished.
*
* Only the CLIENT injects in front of this)
*/
protected void connectionConnected0(Connection connection) {
isConnected.set(true);
// prep the channel wrapper
connection.prep();
connectionManager.connectionConnected(connection);
}
/**
* Expose methods to modify the listeners (connect/disconnect/idle/receive events).
*/
public final ListenerBridge listeners() {
return connectionManager;
}
/**
* Returns a non-modifiable list of active connections
*/
public List<Connection> getConnections() {
return connectionManager.getConnections();
}
/**
* Returns a non-modifiable list of active connections
*/
@SuppressWarnings("unchecked")
public <C extends Connection> Collection<C> getConnectionsAs() {
return (Collection<C>) connectionManager.getConnections();
}
/**
* Closes all connections ONLY (keeps the server/client running)
*/
@Override
public void close() {
// stop does the same as this + more
connectionManager.closeConnections();
super.close();
}
/**
* Extra actions to perform when stopping this endpoint.
*/
@Override
protected void stopExtraActions() {
connectionManager.stop();
}
}

View File

@ -0,0 +1,23 @@
package dorkbox.network.connection;
import java.util.Collection;
public interface ISessionManager {
/** Called when a message is received*/
public void notifyOnMessage(Connection connection, Object message);
/** Called when the connection has been idle (read & write) for 2 seconds */
public void notifyOnIdle(Connection connection);
public void connectionConnected(Connection connection);
public void connectionDisconnected(Connection connection);
/** Called when there is an error of some kind during the up/down stream process */
public void connectionError(Connection connection, Throwable throwable);
/** Returns a non-modifiable list of active connections */
public Collection<Connection> getConnections();
}

View File

@ -0,0 +1,82 @@
package dorkbox.network.connection;
import dorkbox.util.ClassHelper;
// note that we specifically DO NOT implement equals/hashCode, because we cannot create two separate
// listeners that are somehow equal to each other.
public abstract class Listener<C extends Connection, M extends Object> {
private final Class<?> objectType;
// for compile time code. The generic type parameter #2 (index 1) is pulled from type arguments.
// generic parameters cannot be primitive types
public Listener() {
this(1);
}
// for sub-classed listeners, we might have to specify which parameter to use.
protected Listener(int lastParameterIndex) {
if (lastParameterIndex > -1) {
Class<?> objectType = ClassHelper.getGenericParameterAsClassForSuperClass(getClass(), lastParameterIndex);
if (objectType != null) {
this.objectType = objectType;
} else {
this.objectType = Object.class;
}
} else {
// for when we want to override it
this.objectType = Object.class;
}
}
/**
* Gets the referenced object type.
*
* non-final so this can be overridden by listeners that aren't able to define their type as a generic parameter
*/
public Class<?> getObjectType() {
return this.objectType;
}
/**
* Called when the remote end has been connected. This will be invoked before any objects are received by the network.
* This method should not block for long periods as other network activity will not be processed
* until it returns.
*/
public void connected(C connection) {
}
/**
* Called when the remote end is no longer connected. There is no guarantee as to what thread will invoke this method.
* <p>
* Do not write data in this method! The channel can be closed, resulting in an error if you attempt to do so.
*/
public void disconnected(C connection) {
}
/**
* 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.
*/
public void received(C connection, M message) {
}
/**
* Called when the connection is idle for longer than the {@link EndPoint#setIdleTimeout(idle) idle threshold}.
*/
public void idle(C connection) {
}
/**
* Called when there is an error of some kind during the up/down stream process (to/from the socket or otherwise)
*/
public void error(C connection, Throwable throwable) {
throwable.printStackTrace();
}
@Override
public String toString() {
return "Listener [type=" + getObjectType() + "]";
}
}

View File

@ -0,0 +1,55 @@
package dorkbox.network.connection;
/**
* Generic types are in place to make sure that users of the application do not
* accidentally add an incompatible connection type. This is at runtime.
* <p>
* There should always be just a SINGLE connection type for the client or server
*/
public interface ListenerBridge {
/**
* Adds a listener to this connection/endpoint to be notified of
* connect/disconnect/idle/receive(object) events.
* <p>
* If the listener already exists, it is not added again.
* <p>
* When called by a server, NORMALLY listeners are added at the GLOBAL level
* (meaning, I add one listener, and ALL connections are notified of that
* listener.
* <p>
* It is POSSIBLE to add a server connection ONLY (ie, not global) listener
* (via connection.addListener), meaning that ONLY that listener attached to
* the connection is notified on that event (ie, admin type listeners)
*/
@SuppressWarnings("rawtypes")
public void add(Listener listener);
/**
* Removes a listener from this connection/endpoint to NO LONGER be notified
* of connect/disconnect/idle/receive(object) events.
* <p>
* When called by a server, NORMALLY listeners are added at the GLOBAL level
* (meaning, I add one listener, and ALL connections are notified of that
* listener.
* <p>
* It is POSSIBLE to remove a server-connection 'non-global' listener (via
* connection.removeListener), meaning that ONLY that listener attached to
* the connection is removed
*/
@SuppressWarnings("rawtypes")
public void remove(Listener listener);
/**
* Removes all registered listeners from this connection/endpoint to NO
* LONGER be notified of connect/disconnect/idle/receive(object) events.
*/
public void removeAll();
/**
* Removes all registered listeners (of the object type) from this
* connection/endpoint to NO LONGER be notified of
* connect/disconnect/idle/receive(object) events.
*/
public void removeAll(Class<?> classType);
}

View File

@ -0,0 +1,159 @@
package dorkbox.network.connection;
import java.io.File;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Map;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import dorkbox.network.util.ByteArrayWrapper;
import dorkbox.network.util.store.SettingsStore;
import dorkbox.util.Storage;
import dorkbox.util.properties.PropertiesProvider;
/**
* The property store is the DEFAULT type of store for the network stack.
* This is package private.
*/
class PropertyStore extends SettingsStore {
private static class Props {
private volatile ECPrivateKeyParameters serverPrivateKey = null;
private volatile ECPublicKeyParameters serverPublicKey = null;
private volatile byte[] salt = null;
private volatile Map<ByteArrayWrapper, ECPublicKeyParameters> registeredServer = new HashMap<ByteArrayWrapper, ECPublicKeyParameters>(16);
private Props() {
}
}
// the name of the file that contains the saved properties
private static final String SETTINGS_FILE_NAME = "settings.dat";
private final Storage storage;
private Props props = new Props();
// Method of preference for creating/getting this connection store. Private since only the ConnectionStoreProxy calls this
public PropertyStore() {
File propertiesFile;
if (PropertiesProvider.basePath.isEmpty()) {
propertiesFile = new File(SETTINGS_FILE_NAME);
} else {
// sometimes we want to change the base path location
propertiesFile = new File(PropertiesProvider.basePath, SETTINGS_FILE_NAME);
}
propertiesFile = propertiesFile.getAbsoluteFile();
// loads the saved data into the props
this.storage = Storage.load(propertiesFile, this.props);
}
/**
* Simple, property based method to getting the private key of the server
*/
@Override
public synchronized ECPrivateKeyParameters getPrivateKey() throws dorkbox.network.util.SecurityException {
checkAccess(EndPoint.class);
return this.props.serverPrivateKey;
}
/**
* Simple, property based method for saving the private key of the server
*/
@Override
public synchronized void savePrivateKey(ECPrivateKeyParameters serverPrivateKey) throws dorkbox.network.util.SecurityException {
checkAccess(EndPoint.class);
this.props.serverPrivateKey = serverPrivateKey;
this.storage.save();
}
/**
* Simple, property based method to getting the public key of the server
*/
@Override
public synchronized ECPublicKeyParameters getPublicKey() throws dorkbox.network.util.SecurityException {
checkAccess(EndPoint.class);
return this.props.serverPublicKey;
}
/**
* Simple, property based method for saving the public key of the server
*/
@Override
public synchronized void savePublicKey(ECPublicKeyParameters serverPublicKey) throws dorkbox.network.util.SecurityException {
checkAccess(EndPoint.class);
this.props.serverPublicKey = serverPublicKey;
this.storage.save();
}
/**
* Simple, property based method to getting the server salt
*/
@Override
public synchronized byte[] getSalt() {
// we don't care who gets the server salt
if (this.props.salt == null) {
SecureRandom secureRandom = new SecureRandom();
// server salt is used to salt usernames and other various connection handshake parameters
this.props.salt = new byte[256];
secureRandom.nextBytes(this.props.salt);
this.storage.save();
return this.props.salt;
}
return this.props.salt;
}
/**
* Simple, property based method to getting a connected computer by host IP address
*/
@Override
public synchronized ECPublicKeyParameters getRegisteredServerKey(byte[] hostAddress) throws dorkbox.network.util.SecurityException {
checkAccess(RegistrationWrapper.class);
return this.props.registeredServer.get(new ByteArrayWrapper(hostAddress));
}
/**
* Saves a connected computer by host IP address and public key
*/
@Override
public synchronized void addRegisteredServerKey(byte[] hostAddress, ECPublicKeyParameters publicKey) throws dorkbox.network.util.SecurityException {
checkAccess(RegistrationWrapper.class);
this.props.registeredServer.put(new ByteArrayWrapper(hostAddress), publicKey);
this.storage.save();
}
/**
* Deletes a registered computer by host IP address
*/
@Override
public synchronized boolean removeRegisteredServerKey(byte[] hostAddress) throws dorkbox.network.util.SecurityException {
checkAccess(RegistrationWrapper.class);
ECPublicKeyParameters remove = this.props.registeredServer.remove(new ByteArrayWrapper(hostAddress));
this.storage.save();
return remove != null;
}
@Override
public void shutdown() {
Storage.shutdown();
}
}

View File

@ -0,0 +1,213 @@
package dorkbox.network.connection;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.security.SecureRandom;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.ReentrantLock;
import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import dorkbox.network.connection.registration.MetaChannel;
import dorkbox.network.pipeline.KryoEncoder;
import dorkbox.network.pipeline.KryoEncoderCrypto;
import dorkbox.network.util.SecurityException;
import dorkbox.network.util.primativeCollections.IntMap;
import dorkbox.util.crypto.Crypto;
/**
* Just wraps common/needed methods of the client/server endpoint by the registration stage/handshake.
*
* This is in the connection package, so it can access the endpoint methods that it needs to.
*/
public class RegistrationWrapper implements UdpServer {
private final org.slf4j.Logger logger;
private final EndPoint endPoint;
// keeps track of connections (TCP/UDT/UDP-client)
private ReentrantLock channelMapLock = new ReentrantLock();
private IntMap<MetaChannel> channelMap = new IntMap<MetaChannel>();
// keeps track of connections (UDP-server)
private volatile ConcurrentMap<InetSocketAddress, ConnectionImpl> udpRemoteMap;
private KryoEncoder kryoTcpEncoder;
private KryoEncoderCrypto kryoTcpCryptoEncoder;
public RegistrationWrapper(EndPoint endPoint) {
this.endPoint = endPoint;
this.logger = org.slf4j.LoggerFactory.getLogger(endPoint.name);
if (endPoint instanceof EndPointServer) {
this.udpRemoteMap = new ConcurrentHashMap<InetSocketAddress, ConnectionImpl>();
} else {
this.udpRemoteMap = null;
}
}
public void setKryoTcpEncoder(KryoEncoder kryoTcpEncoder) {
this.kryoTcpEncoder = kryoTcpEncoder;
}
public void setKryoTcpCryptoEncoder(KryoEncoderCrypto kryoTcpCryptoEncoder) {
this.kryoTcpCryptoEncoder = kryoTcpCryptoEncoder;
}
public KryoEncoder getKryoTcpEncoder() {
return this.kryoTcpEncoder;
}
public KryoEncoderCrypto getKryoTcpCryptoEncoder() {
return this.kryoTcpCryptoEncoder;
}
/**
* Locks, and then returns the channelMap used by the registration process.
* <p>
* Make SURE to use this in a try/finally block with releaseChannelMap in the finally block!
*/
public IntMap<MetaChannel> getAndLockChannelMap() {
// try to lock access
this.channelMapLock.lock();
return this.channelMap;
}
public void releaseChannelMap() {
// try to unlocal access
this.channelMapLock.unlock();
}
/**
* The amount of milli-seconds that must elapse with no read or write before {@link Listener:idle()}
* will be triggered
*/
public int getIdleTimeout() {
return this.endPoint.getIdleTimeout();
}
/**
* Internal call by the pipeline to notify the client to continue registering the different session protocols.
* The server does not use this.
* @return true if we are done registering bootstraps
*/
public boolean continueRegistration0() {
return this.endPoint.continueRegistration0();
}
/**
* Internal call by the pipeline to notify the "Connection" object that it has "connected", meaning that modifications
* to the pipeline are finished.
*/
public void connectionConnected0(Connection networkConnection) {
if (this.endPoint instanceof EndPointWithSerialization) {
((EndPointWithSerialization)this.endPoint).connectionConnected0(networkConnection);
}
}
/**
* Internal call by the pipeline when:
* - creating a new network connection
* - when determining the baseClass for generics
*
* @param metaChannel can be NULL (when getting the baseClass)
*/
public ConnectionImpl connection0(MetaChannel metaChannel) {
if (this.endPoint instanceof EndPointWithSerialization) {
return ((EndPointWithSerialization)this.endPoint).connection0(metaChannel);
}
return null;
}
public SecureRandom getSecureRandom() {
return this.endPoint.secureRandom;
}
public ECPublicKeyParameters getPublicKey() {
return this.endPoint.publicKey;
}
public CipherParameters getPrivateKey() {
return this.endPoint.privateKey;
}
public boolean validateRemoteServerAddress(InetSocketAddress tcpRemoteServer, ECPublicKeyParameters publicKey) throws SecurityException {
InetAddress address = tcpRemoteServer.getAddress();
byte[] hostAddress = address.getAddress();
ECPublicKeyParameters savedPublicKey = this.endPoint.propertyStore.getRegisteredServerKey(hostAddress);
if (savedPublicKey == null) {
this.logger.debug("Adding new remote IP address key for {}", address.getHostAddress());
this.endPoint.propertyStore.addRegisteredServerKey(hostAddress, publicKey);
} else {
// COMPARE!
if (!Crypto.ECC.compare(publicKey, savedPublicKey)) {
String byAddress;
try {
byAddress = InetAddress.getByAddress(hostAddress).getHostAddress();
} catch (UnknownHostException e) {
byAddress = "Unknown";
}
//whoa! abort since something messed up!
this.logger.error("Invalid or non-matching public key from remote server. Their public key has changed. To fix, remove entry for: {}", byAddress);
return false;
}
}
return true;
}
public void removeRegisteredServerKey(byte[] hostAddress) throws SecurityException {
ECPublicKeyParameters savedPublicKey = this.endPoint.propertyStore.getRegisteredServerKey(hostAddress);
if (savedPublicKey != null) {
this.logger.debug("Deleteing remote IP address key {}.{}.{}.{}", hostAddress[0], hostAddress[1], hostAddress[2], hostAddress[3]);
this.endPoint.propertyStore.removeRegisteredServerKey(hostAddress);
}
}
/**
* ONLY SERVER SIDE CALLS THIS
* Called when creating a connection.
* Only called if we have a UDP channel
*/
@Override
public final void registerServerUDP(MetaChannel metaChannel) {
if (metaChannel != null && metaChannel.udpRemoteAddress != null) {
this.udpRemoteMap.put(metaChannel.udpRemoteAddress, metaChannel.connection);
this.logger.debug("Connected to remote UDP connection. [{} <== {}]",
metaChannel.udpChannel.localAddress().toString(),
metaChannel.udpRemoteAddress.toString());
}
}
/**
* ONLY SERVER SIDE CALLS THIS
* Called when closing a connection.
*/
@Override
public final void unRegisterServerUDP(InetSocketAddress udpRemoteAddress) {
if (udpRemoteAddress != null) {
this.udpRemoteMap.remove(udpRemoteAddress);
this.logger.info("Closed remote UDP connection: {}", udpRemoteAddress.toString());
}
}
/**
* ONLY SERVER SIDE CALLS THIS
*/
@Override
public ConnectionImpl getServerUDP(InetSocketAddress udpRemoteAddress) {
if (udpRemoteAddress != null) {
return this.udpRemoteMap.get(udpRemoteAddress);
} else {
return null;
}
}
}

View File

@ -0,0 +1,100 @@
package dorkbox.network.connection;
import java.util.Collection;
public class ServerConnectionBridge implements ConnectionBridgeServer, ConnectionExceptsBridgeServer {
private final ConnectionManager connectionManager;
public ServerConnectionBridge(ConnectionManager connectionManager) {
this.connectionManager = connectionManager;
}
/**
* Sends the object all server connections over the network using TCP. (or
* via LOCAL when it's a local channel).
*/
@Override
public void TCP(Object message) {
Collection<Connection> connections0 = connectionManager.getConnections0();
for (Connection c : connections0) {
c.send().TCP(message);
}
}
/**
* Sends the object all server connections over the network using UDP. (or
* via LOCAL when it's a local channel).
*/
@Override
public void UDP(Object message) {
Collection<Connection> connections0 = connectionManager.getConnections0();
for (Connection c : connections0) {
c.send().UDP(message);
}
}
/**
* Sends the object all server connections over the network using UDT. (or
* via LOCAL when it's a local channel).
*/
@Override
public void UDT(Object message) {
Collection<Connection> connections0 = connectionManager.getConnections0();
for (Connection c : connections0) {
c.send().UDT(message);
}
}
/**
* Exposes methods to send the object to all server connections (except the specified one)
* over the network. (or via LOCAL when it's a local channel).
*/
@Override
public ConnectionExceptsBridgeServer except() {
return this;
}
/**
* Sends the object to all server connections (except the specified one)
* over the network using TCP. (or via LOCAL when it's a local channel).
*/
@Override
public void TCP(Connection connection, Object message) {
Collection<Connection> connections0 = connectionManager.getConnections0();
for (Connection c : connections0) {
if (c != connection) {
c.send().TCP(message);
}
}
}
/**
* Sends the object to all server connections (except the specified one)
* over the network using UDP (or via LOCAL when it's a local channel).
*/
@Override
public void UDP(Connection connection, Object message) {
Collection<Connection> connections0 = connectionManager.getConnections0();
for (Connection c : connections0) {
if (c != connection) {
c.send().UDP(message);
}
}
}
/**
* Sends the object to all server connections (except the specified one)
* over the network using UDT. (or via LOCAL when it's a local channel).
*/
@Override
public void UDT(Connection connection, Object message) {
Collection<Connection> connections0 = connectionManager.getConnections0();
for (Connection c : connections0) {
if (c != connection) {
c.send().UDT(message);
}
}
}
}

View File

@ -0,0 +1,23 @@
package dorkbox.network.connection;
import java.net.InetSocketAddress;
import dorkbox.network.connection.registration.MetaChannel;
public interface UdpServer {
/**
* Called when creating a connection.
*/
public void registerServerUDP(MetaChannel metaChannel);
/**
* Called when closing a connection.
*/
public void unRegisterServerUDP(InetSocketAddress udpRemoteAddress);
/**
* @return the connection for a remote UDP address
*/
public ConnectionImpl getServerUDP(InetSocketAddress udpRemoteAddress);
}

View File

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

View File

@ -0,0 +1,12 @@
package dorkbox.network.connection.idle;
import dorkbox.network.connection.Connection;
import dorkbox.network.connection.Listener;
abstract class IdleListener<C extends Connection, M> extends Listener<C, M> {
/**
* used by the Idle Sender
*/
abstract void send(C connection, M message);
}

View File

@ -0,0 +1,20 @@
package dorkbox.network.connection.idle;
import dorkbox.network.connection.Connection;
public class IdleListenerTCP<C extends Connection, M> extends IdleListener<C, M> {
/**
* used by the Idle Sender
*/
IdleListenerTCP() {
}
/**
* used by the Idle Sender
*/
@Override
void send(C connection, M message) {
connection.send().TCP(message);
}
}

View File

@ -0,0 +1,20 @@
package dorkbox.network.connection.idle;
import dorkbox.network.connection.Connection;
public class IdleListenerUDP<C extends Connection, M> extends IdleListener<C, M> {
/**
* used by the Idle Sender
*/
IdleListenerUDP() {
}
/**
* used by the Idle Sender
*/
@Override
void send(C connection, M message) {
connection.send().UDP(message);
}
}

View File

@ -0,0 +1,20 @@
package dorkbox.network.connection.idle;
import dorkbox.network.connection.Connection;
public class IdleListenerUDT<C extends Connection, M> extends IdleListener<C, M> {
/**
* used by the Idle Sender
*/
IdleListenerUDT() {
}
/**
* used by the Idle Sender
*/
@Override
void send(C connection, M message) {
connection.send().UDT(message);
}
}

View File

@ -0,0 +1,32 @@
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(M message) {
this.message = message;
}
@Override
public void idle(C connection) {
if (!started) {
started = true;
start();
}
connection.listeners().remove(this);
if (idleListener != null) {
idleListener.send(connection, message);
} else {
throw new RuntimeException("Invlaid idle listener. Please specify .TCP(), .UDP(), or .UDT()");
}
}
@Override
protected M next() {
return null;
}
}

View File

@ -0,0 +1,55 @@
package dorkbox.network.connection.idle;
import dorkbox.network.connection.Connection;
import dorkbox.network.connection.Listener;
abstract public class IdleSender<C extends Connection, M> extends Listener<C, M> implements IdleBridge {
boolean started;
IdleListener<C, M> idleListener;
@Override
public void idle(C connection) {
if (!started) {
started = true;
start();
}
M message = next();
if (message == null) {
connection.listeners().remove(this);
} else {
if (idleListener != null) {
idleListener.send(connection, message);
} else {
throw new RuntimeException("Invalid idle listener. Please specify .TCP(), .UDP(), or .UDT()");
}
}
}
@Override
public void TCP() {
idleListener = new IdleListenerTCP<C, M>();
}
@Override
public void UDP() {
idleListener = new IdleListenerUDP<C, M>();
}
@Override
public void UDT() {
idleListener = new IdleListenerUDT<C, M>();
}
/** 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. */
abstract protected M next ();
}

View File

@ -0,0 +1,44 @@
package dorkbox.network.connection.idle;
import java.io.IOException;
import java.io.InputStream;
import dorkbox.network.connection.Connection;
import dorkbox.network.util.NetException;
abstract public class InputStreamSender<C extends Connection> extends IdleSender<C,byte[]> {
private final InputStream input;
private final byte[] chunk;
public InputStreamSender(InputStream input, int chunkSize) {
this.input = input;
this.chunk = new byte[chunkSize];
}
@Override
protected final byte[] next() {
try {
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;
}
} catch (IOException ex) {
throw new NetException(ex);
}
return onNext(this.chunk);
}
abstract protected byte[] onNext (byte[] chunk);
}

View File

@ -0,0 +1,31 @@
package dorkbox.network.connection.ping;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
public interface Ping {
/**
* Wait for the ping to return, and returns the ping response time or -1 if it failed failed.
*/
public int getResponse() ;
/**
* Adds the specified listener to this future. The specified listener is
* notified when this future is done. If this future is already completed,
* the specified listener is notified immediately.
*/
public void addListener(GenericFutureListener<? extends Future<? super Object>> listener);
/**
* Removes the specified listener from this future. The specified listener
* is no longer notified when this future is done. If the specified listener
* is not associated with this future, this method does nothing and returns
* silently.
*/
public void removeListener(GenericFutureListener<? extends Future<? super Object>> listener);
/**
* Cancel this Ping.
*/
public void cancel();
}

View File

@ -0,0 +1,10 @@
package dorkbox.network.connection.ping;
public class PingCanceledException extends RuntimeException {
private static final long serialVersionUID = 9045461384091038605L;
public PingCanceledException() {
super("Ping request has been canceled.");
}
}

View File

@ -0,0 +1,73 @@
package dorkbox.network.connection.ping;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
import io.netty.util.concurrent.Promise;
import java.util.concurrent.ExecutionException;
public class PingFuture implements Ping {
private final Promise<Integer> promise;
/**
* Protected constructor for when we are completely overriding this class. (Used by the "local" connection for instant pings)
*/
protected PingFuture() {
promise = null;
}
public PingFuture(Promise<Integer> promise) {
this.promise = promise;
}
/**
* Wait for the ping to return, and returns the ping response time or -1 if it failed failed.
*/
@Override
public int getResponse() {
try {
return promise.syncUninterruptibly().get();
} catch (InterruptedException e) {
} catch (ExecutionException e) {
}
return -1;
}
/**
* Tells this ping future, that it was successful
*/
public void setSuccess(PingUtil pingUtil) {
promise.setSuccess(pingUtil.getReturnTripTime());
}
/**
* Adds the specified listener to this future. The specified listener is
* notified when this future is done. If this future is already completed,
* the specified listener is notified immediately.
*/
@Override
public void addListener(GenericFutureListener<? extends Future<? super Object>> listener) {
promise.addListener(listener);
}
/**
* Removes the specified listener from this future. The specified listener
* is no longer notified when this future is done. If the specified listener
* is not associated with this future, this method does nothing and returns
* silently.
*/
@Override
public void removeListener(GenericFutureListener<? extends Future<? super Object>> listener) {
promise.removeListener(listener);
}
/**
* Cancel this Ping.
*/
@Override
public void cancel() {
promise.tryFailure(new PingCanceledException());
}
}

View File

@ -0,0 +1,56 @@
package dorkbox.network.connection.ping;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
public class PingFutureLocal extends PingFuture {
public PingFutureLocal() {
}
/**
* Wait for the ping to return, and returns the ping response time or -1 if it failed failed.
*/
@Override
public int getResponse() {
return 0;
}
/**
* Tells this ping future, that it was successful
*/
@Override
public void setSuccess(PingUtil pingUtil) {
}
/**
* Adds the specified listener to this future. The specified listener is
* notified when this future is done. If this future is already completed,
* the specified listener is notified immediately.
*/
@Override
public void addListener(GenericFutureListener<? extends Future<? super Object>> listener) {
try {
listener.operationComplete(null);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Removes the specified listener from this future. The specified listener
* is no longer notified when this future is done. If the specified listener
* is not associated with this future, this method does nothing and returns
* silently.
*/
@Override
public void removeListener(GenericFutureListener<? extends Future<? super Object>> listener) {
}
/**
* Cancel this Ping.
*/
@Override
public void cancel() {
}
}

View File

@ -0,0 +1,25 @@
package dorkbox.network.connection.ping;
import dorkbox.network.connection.ConnectionImpl;
import dorkbox.network.connection.Listener;
public class PingListener extends Listener<ConnectionImpl, PingMessage> {
private final org.slf4j.Logger logger;
public PingListener(String name) {
logger = org.slf4j.LoggerFactory.getLogger(name);
}
@Override
public void received(ConnectionImpl connection, PingMessage ping) {
if (ping.isReply) {
logger.trace("Received a reply to my issued ping request.");
connection.updatePingResponse(ping);
} else {
logger.trace( "Received a ping from {}", connection);
ping.isReply = true;
connection.ping0(ping);
}
}
}

View File

@ -0,0 +1,12 @@
package dorkbox.network.connection.ping;
/**
* Internal message to determine round trip time.
*/
public class PingMessage {
public int id;
public boolean isReply;
/** The ping round-trip time in milliseconds */
public transient int time;
}

View File

@ -0,0 +1,37 @@
package dorkbox.network.connection.ping;
public class PingUtil {
private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(PingUtil.class);
//all methods are protected by this!
private int lastPingID = 0;
private long lastPingSendTime;
// if something takes longer than 2billion seconds (signed int) and the connection doesn't time out. We have problems.
/** The ping round-trip time in nanoseconds */
private int returnTripTime;
public PingUtil() {
}
public final synchronized PingMessage pingMessage() {
PingMessage ping = new PingMessage();
ping.id = lastPingID++;
lastPingSendTime = System.currentTimeMillis();
return ping;
}
public final synchronized int getReturnTripTime() {
return returnTripTime;
}
public final synchronized void updatePing(PingMessage ping) {
if (ping.id == lastPingID - 1) {
ping.time = returnTripTime = (int)(System.currentTimeMillis() - lastPingSendTime);
logger.trace("Return trip time: {}", returnTripTime);
}
}
}

View File

@ -0,0 +1,93 @@
package dorkbox.network.connection.registration;
import io.netty.channel.Channel;
import java.net.InetSocketAddress;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import dorkbox.network.connection.ConnectionImpl;
public class MetaChannel {
// how long between receiving data over TCP. This is used to determine how long to wait before notifying the APP,
// so the registration message has time to arrive to the other endpoint.
private volatile long nanoSecBetweenTCP = 0L;
public Integer connectionID = null; // only used during the registration process
public Channel localChannel = null; // only available for local "in jvm" channels. XOR with tcp/udp channels with CLIENT.
public Channel tcpChannel = null;
// channel here (on server or socket.bind connections) doesn't have the remote address available.
// It is apart of the inbound message, however.
// ALSO not necessary to close it, since the server handles that.
public Channel udpChannel = null;
public InetSocketAddress udpRemoteAddress = null; // SERVER ONLY. needed to be aware of the remote address to send UDP replies to
public Channel udtChannel = null;
public ConnectionImpl connection; // only needed until the connection has been notified.
public ECPublicKeyParameters publicKey; // used for ECC crypto + handshake on NETWORK (remote) connections. This is the remote public key.
public AsymmetricCipherKeyPair ecdhKey; // used for ECC Diffie-Hellman-Merkle key exchanges: see http://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange
public byte[] aesKey;
public byte[] aesIV;
// indicates if the remote ECC key has changed for an IP address. If the client detects this, it will not connect.
// If the server detects this, it has the option for additional security (two-factor auth, perhaps?)
public boolean changedRemoteKey = false;
public void close() {
if (localChannel != null) {
localChannel.close();
}
if (tcpChannel != null) {
tcpChannel.close();
}
if (udtChannel != null) {
udtChannel.close();
}
// only the CLIENT will have this.
if (udpChannel != null && udpRemoteAddress == null) {
udpChannel.close();
}
}
public void close(long maxShutdownWaitTimeInMilliSeconds) {
if (localChannel != null) {
localChannel.close();
}
if (tcpChannel != null) {
tcpChannel.close().awaitUninterruptibly(maxShutdownWaitTimeInMilliSeconds);
}
if (udtChannel != null) {
udtChannel.close().awaitUninterruptibly(maxShutdownWaitTimeInMilliSeconds);
}
// only the CLIENT will have this.
if (udpChannel != null && udpRemoteAddress == null) {
udpChannel.close().awaitUninterruptibly(maxShutdownWaitTimeInMilliSeconds);
}
}
/**
* Update the TCP round trip time. Make sure to REFRESH this every time you SEND TCP data!!
*/
public void updateTcpRoundTripTime() {
nanoSecBetweenTCP = System.nanoTime() - nanoSecBetweenTCP;
}
public long getNanoSecBetweenTCP() {
return nanoSecBetweenTCP;
}
}

View File

@ -0,0 +1,18 @@
package dorkbox.network.connection.registration;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.IESParameters;
/**
* Internal message to handle the TCP/UDP registration process
*/
public class Registration {
public ECPublicKeyParameters publicKey;
public IESParameters eccParameters;
public byte[] aesKey;
public byte[] aesIV;
public byte[] payload;
}

View File

@ -0,0 +1,94 @@
package dorkbox.network.connection.registration;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import dorkbox.network.connection.RegistrationWrapper;
import dorkbox.network.util.primativeCollections.IntMap;
import dorkbox.network.util.primativeCollections.IntMap.Entries;
@Sharable
public abstract class RegistrationHandler extends ChannelInboundHandlerAdapter {
protected static final String CONNECTION_HANDLER = "connectionHandler";
protected final RegistrationWrapper registrationWrapper;
protected final org.slf4j.Logger logger;
protected final String name;
public RegistrationHandler(String name, RegistrationWrapper registrationWrapper) {
this.name = name + " Discovery/Registration";
logger = org.slf4j.LoggerFactory.getLogger(this.name);
this.registrationWrapper = registrationWrapper;
}
protected void initChannel(Channel channel) {
}
@Override
public final void channelRegistered(ChannelHandlerContext context) throws Exception {
boolean success = false;
try {
initChannel(context.channel());
context.fireChannelRegistered();
success = true;
} catch (Throwable t) {
logger.error("Failed to initialize a channel. Closing: {}", context.channel(), t);
} finally {
if (!success) {
context.close();
}
}
}
@Override
public void channelActive(ChannelHandlerContext context) throws Exception {
logger.error("ChannelActive NOT IMPLEMENTED!");
}
@Override
public void channelRead(ChannelHandlerContext context, Object message) throws Exception {
logger.error("MessageReceived NOT IMPLEMENTED!");
}
@Override
public void channelReadComplete(ChannelHandlerContext context) throws Exception {
context.flush();
}
@Override
public abstract void exceptionCaught(ChannelHandlerContext context, Throwable cause) throws Exception;
public MetaChannel shutdown(RegistrationWrapper registrationWrapper, Channel channel) {
logger.error("SHUTDOWN HANDLER REACHED! SOMETHING MESSED UP! TRYING TO ABORT");
// shutdown. Something messed up. Only reach this is something messed up.
// properly shutdown the TCP/UDP channels.
if (channel.isOpen()) {
channel.close();
}
// also, once we notify, we unregister this.
if (registrationWrapper != null) {
try {
IntMap<MetaChannel> channelMap = registrationWrapper.getAndLockChannelMap();
Entries<MetaChannel> entries = channelMap.entries();
while (entries.hasNext()) {
MetaChannel metaChannel = entries.next().value;
if (metaChannel.localChannel == channel || metaChannel.tcpChannel == channel || metaChannel.udpChannel == channel) {
entries.remove();
metaChannel.close();
return metaChannel;
}
}
} finally {
registrationWrapper.releaseChannelMap();
}
}
return null;
}
}

View File

@ -0,0 +1,108 @@
package dorkbox.network.connection.registration.local;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPipeline;
import dorkbox.network.connection.EndPoint;
import dorkbox.network.connection.RegistrationWrapper;
import dorkbox.network.connection.registration.MetaChannel;
import dorkbox.network.connection.registration.RegistrationHandler;
import dorkbox.network.util.primativeCollections.IntMap;
import dorkbox.network.util.primativeCollections.IntMap.Entries;
public abstract class RegistrationLocalHandler extends RegistrationHandler {
public RegistrationLocalHandler(String name, RegistrationWrapper registrationWrapper) {
super(name, registrationWrapper);
}
/**
* STEP 1: Channel is first created
*/
@Override
protected void initChannel(Channel channel) {
MetaChannel metaChannel = new MetaChannel();
metaChannel.localChannel = channel;
try {
IntMap<MetaChannel> channelMap = registrationWrapper.getAndLockChannelMap();
channelMap.put(channel.hashCode(), metaChannel);
} finally {
registrationWrapper.releaseChannelMap();
}
logger.trace("New LOCAL connection.");
registrationWrapper.connection0(metaChannel);
// have to setup connection handler
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast(CONNECTION_HANDLER, metaChannel.connection);
}
/**
* STEP 2: Channel is now active. Start the registration process
*/
@Override
public void channelActive(ChannelHandlerContext context) throws Exception {
if (logger.isDebugEnabled()) {
Channel channel = context.channel();
StringBuilder builder = new StringBuilder(76);
builder.append("Connected to LOCAL connection. [");
builder.append(context.channel().localAddress());
builder.append(getConnectionDirection());
builder.append(channel.remoteAddress());
builder.append("]");
logger.debug(builder.toString());
}
}
/**
* @return the direction that traffic is going to this handler (" <== " or " ==> ")
*/
protected abstract String getConnectionDirection();
// this SHOULDN'T ever happen, but we might shutdown in the middle of registration
@Override
public final void channelInactive(ChannelHandlerContext context) throws Exception {
Channel channel = context.channel();
logger.info("Closed LOCAL connection: {}", channel.remoteAddress());
long maxShutdownWaitTimeInMilliSeconds = EndPoint.maxShutdownWaitTimeInMilliSeconds;
// also, once we notify, we unregister this.
try {
IntMap<MetaChannel> channelMap = registrationWrapper.getAndLockChannelMap();
Entries<MetaChannel> entries = channelMap.entries();
while (entries.hasNext()) {
MetaChannel metaChannel = entries.next().value;
if (metaChannel.localChannel == channel) {
metaChannel.close(maxShutdownWaitTimeInMilliSeconds);
entries.remove();
break;
}
}
} finally {
registrationWrapper.releaseChannelMap();
}
super.channelInactive(context);
}
@Override
public void exceptionCaught(ChannelHandlerContext context, Throwable cause) throws Exception {
Channel channel = context.channel();
logger.error("Unexpected exception while trying to receive data on LOCAL channel. ({})" + System.getProperty("line.separator"), channel.remoteAddress(), cause);
if (channel.isOpen()) {
channel.close();
}
}
}

View File

@ -0,0 +1,77 @@
package dorkbox.network.connection.registration.local;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.ReferenceCountUtil;
import dorkbox.network.connection.Connection;
import dorkbox.network.connection.RegistrationWrapper;
import dorkbox.network.connection.registration.MetaChannel;
import dorkbox.network.connection.registration.Registration;
import dorkbox.network.util.primativeCollections.IntMap;
public class RegistrationLocalHandlerClient extends RegistrationLocalHandler {
public RegistrationLocalHandlerClient(String name, RegistrationWrapper registrationWrapper) {
super(name, registrationWrapper);
}
/**
* STEP 1: Channel is first created
*/
// Calls the super class to init the local channel
// @Override
// protected void initChannel(Channel channel) {
// }
/**
* STEP 2: Channel is now active. Start the registration process
*/
@Override
public void channelActive(ChannelHandlerContext context) throws Exception {
if (logger.isDebugEnabled()) {
super.channelActive(context);
}
Channel channel = context.channel();
channel.writeAndFlush(new Registration());
}
/**
* @return the direction that traffic is going to this handler (" <== " or " ==> ")
*/
@Override
protected String getConnectionDirection() {
return " ==> ";
}
@Override
public void channelRead(ChannelHandlerContext context, Object message) throws Exception {
ReferenceCountUtil.release(message);
Channel channel = context.channel();
MetaChannel metaChannel = null;
try {
IntMap<MetaChannel> channelMap = registrationWrapper.getAndLockChannelMap();
metaChannel = channelMap.remove(channel.hashCode());
} finally {
registrationWrapper.releaseChannelMap();
}
// have to setup new listeners
if (metaChannel != null) {
channel.pipeline().remove(this);
// Event though a local channel is XOR with everything else, we still have to make the client clean up it's state.
registrationWrapper.continueRegistration0();
Connection connection = metaChannel.connection;
registrationWrapper.connectionConnected0(connection);
} else {
// this should NEVER happen!
logger.error("Error registering LOCAL channel! MetaChannel is null!");
shutdown(registrationWrapper, channel);
}
}
}

View File

@ -0,0 +1,56 @@
package dorkbox.network.connection.registration.local;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPipeline;
import io.netty.util.ReferenceCountUtil;
import dorkbox.network.connection.Connection;
import dorkbox.network.connection.RegistrationWrapper;
import dorkbox.network.connection.registration.MetaChannel;
import dorkbox.network.util.primativeCollections.IntMap;
public class RegistrationLocalHandlerServer extends RegistrationLocalHandler {
public RegistrationLocalHandlerServer(String name, RegistrationWrapper registrationWrapper) {
super(name, registrationWrapper);
}
@Override
public void channelRead(ChannelHandlerContext context, Object message) throws Exception {
Channel channel = context.channel();
ChannelPipeline pipeline = channel.pipeline();
// have to remove the pipeline FIRST, since if we don't, and we expect to receive a message --- when we REMOVE "this" from the pipeline,
// we will ALSO REMOVE all it's messages, which we want to receive!
pipeline.remove(this);
channel.writeAndFlush(message);
ReferenceCountUtil.release(message);
logger.trace("Sent registration");
Connection connection = null;
try {
IntMap<MetaChannel> channelMap = registrationWrapper.getAndLockChannelMap();
MetaChannel metaChannel = channelMap.remove(channel.hashCode());
if (metaChannel != null) {
connection = metaChannel.connection;
}
} finally {
registrationWrapper.releaseChannelMap();
}
if (connection != null) {
registrationWrapper.connectionConnected0(connection);
}
}
/**
* @return the direction that traffic is going to this handler (" <== " or " ==> ")
*/
@Override
protected String getConnectionDirection() {
return " <== ";
}
}

View File

@ -0,0 +1,354 @@
package dorkbox.network.connection.registration.remote;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.nio.NioDatagramChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.channel.udt.nio.NioUdtByteConnectorChannel;
import io.netty.handler.timeout.IdleStateHandler;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import org.bouncycastle.crypto.engines.AESFastEngine;
import org.bouncycastle.crypto.modes.GCMBlockCipher;
import dorkbox.network.connection.ConnectionImpl;
import dorkbox.network.connection.EndPoint;
import dorkbox.network.connection.RegistrationWrapper;
import dorkbox.network.connection.registration.MetaChannel;
import dorkbox.network.connection.registration.RegistrationHandler;
import dorkbox.network.pipeline.KryoDecoder;
import dorkbox.network.pipeline.KryoDecoderCrypto;
import dorkbox.network.pipeline.udp.KryoDecoderUdpCrypto;
import dorkbox.network.pipeline.udp.KryoEncoderUdpCrypto;
import dorkbox.network.util.SerializationManager;
import dorkbox.network.util.primativeCollections.IntMap;
import dorkbox.network.util.primativeCollections.IntMap.Entries;
public abstract class RegistrationRemoteHandler extends RegistrationHandler {
private static final String IDLE_HANDLER_FULL = "idleHandlerFull";
protected static final String KRYO_ENCODER = "kryoEncoder";
protected static final String KRYO_DECODER = "kryoDecoder";
private static final String FRAME_AND_KRYO_ENCODER = "frameAndKryoEncoder";
private static final String FRAME_AND_KRYO_DECODER = "frameAndKryoDecoder";
private static final String FRAME_AND_KRYO_CRYPTO_ENCODER = "frameAndKryoCryptoEncoder";
private static final String FRAME_AND_KRYO_CRYPTO_DECODER = "frameAndKryoCryptoDecoder";
private static final String KRYO_CRYPTO_ENCODER = "kryoCryptoEncoder";
private static final String KRYO_CRYPTO_DECODER = "kryoCryptoDecoder";
private static final String IDLE_HANDLER = "idleHandler";
protected final SerializationManager serializationManager;
private static ThreadLocal<GCMBlockCipher> aesEngineLocal = new ThreadLocal<GCMBlockCipher>();
public RegistrationRemoteHandler(String name, RegistrationWrapper registrationWrapper, SerializationManager serializationManager) {
super(name, registrationWrapper);
this.serializationManager = serializationManager;
}
protected static final GCMBlockCipher getAesEngine() {
GCMBlockCipher aesEngine = aesEngineLocal.get();
if (aesEngine == null) {
aesEngine = new GCMBlockCipher(new AESFastEngine());
aesEngineLocal.set(aesEngine);
}
return aesEngine;
}
/**
* STEP 1: Channel is first created
*/
@Override
protected void initChannel(Channel channel) {
ChannelPipeline pipeline = channel.pipeline();
///////////////////////
// DECODE (or upstream)
///////////////////////
pipeline.addFirst(FRAME_AND_KRYO_DECODER, new KryoDecoder(serializationManager)); // cannot be shared because of possible fragmentation.
// this makes the proper event get raised in the registrationHandler to kill NEW idle connections. Once "connected" they last a lot longer.
// we ALWAYS have this initial IDLE handler, so we don't have to worry about a slow-loris attack against the server.
pipeline.addFirst(IDLE_HANDLER, new IdleStateHandler(4, 0, 0)); // timer must be shared.
/////////////////////////
// ENCODE (or downstream)
/////////////////////////
pipeline.addFirst(FRAME_AND_KRYO_ENCODER, registrationWrapper.getKryoTcpEncoder()); // this is shared
}
/**
* STEP 2: Channel is now active. (if debug is enabled...)
* Debug output, so we can tell what direction the connection is in the log
*/
@Override
public void channelActive(ChannelHandlerContext context) throws Exception {
// add the channel so we can access it later.
// do NOT want to add UDP channels, since they are tracked differently.
Channel channel = context.channel();
StringBuilder stringBuilder = new StringBuilder(76);
stringBuilder.append("Connected to remote ");
if (channel instanceof NioSocketChannel) {
stringBuilder.append("TCP");
} else if (channel instanceof NioDatagramChannel) {
stringBuilder.append("UDP");
} else if (channel instanceof NioUdtByteConnectorChannel) {
stringBuilder.append("UDT");
}
else {
stringBuilder.append("UNKNOWN");
}
stringBuilder.append(" connection. [");
stringBuilder.append(channel.localAddress());
boolean isSessionless = channel instanceof NioDatagramChannel;
if (isSessionless) {
if (channel.remoteAddress() != null) {
stringBuilder.append(" ==> ");
stringBuilder.append(channel.remoteAddress());
} else {
// this means we are LISTENING.
stringBuilder.append(" <== ");
stringBuilder.append("?????");
}
} else {
stringBuilder.append(getConnectionDirection());
stringBuilder.append(channel.remoteAddress());
}
stringBuilder.append("]");
logger.debug(stringBuilder.toString());
}
/**
* @return the direction that traffic is going to this handler (" <== " or " ==> ")
*/
protected abstract String getConnectionDirection();
/**
* Check to verify if two InetAdresses are equal, by comparing the underlying byte arrays.
*/
public static boolean checkEqual(InetAddress serverA, InetAddress serverB) {
if (serverA == null || serverB == null) {
return false;
}
return Arrays.equals(serverA.getAddress(), serverB.getAddress());
}
// have to setup AFTER establish connection, data, as we don't want to enable AES until we're ready.
protected final void setupConnectionCrypto(MetaChannel metaChannel) {
if (logger.isDebugEnabled()) {
String type = "TCP";
if (metaChannel.udpChannel != null) {
type += "/UDP";
}
if (metaChannel.udtChannel != null) {
type += "/UDT";
}
InetSocketAddress address = (InetSocketAddress)metaChannel.tcpChannel.remoteAddress();
logger.debug("Encrypting {} session with {}", type, address.getAddress());
}
ChannelPipeline pipeline = metaChannel.tcpChannel.pipeline();
int idleTimeout = registrationWrapper.getIdleTimeout();
// add the new handlers (FORCE encryption and longer IDLE handler)
pipeline.replace(FRAME_AND_KRYO_DECODER, FRAME_AND_KRYO_CRYPTO_DECODER, new KryoDecoderCrypto(serializationManager)); // cannot be shared because of possible fragmentation.
if (idleTimeout > 0) {
pipeline.replace(IDLE_HANDLER, IDLE_HANDLER_FULL, new IdleStateHandler(0, 0, registrationWrapper.getIdleTimeout(), TimeUnit.MILLISECONDS));
} else {
pipeline.remove(IDLE_HANDLER);
}
pipeline.replace(FRAME_AND_KRYO_ENCODER, FRAME_AND_KRYO_CRYPTO_ENCODER, registrationWrapper.getKryoTcpCryptoEncoder()); // this is shared
if (metaChannel.udpChannel != null && metaChannel.udpRemoteAddress == null) {
// CLIENT ONLY. The server handles this very differently.
pipeline = metaChannel.udpChannel.pipeline();
pipeline.replace(KRYO_DECODER, KRYO_CRYPTO_DECODER, new KryoDecoderUdpCrypto(serializationManager));
pipeline.replace(KRYO_ENCODER, KRYO_CRYPTO_ENCODER, new KryoEncoderUdpCrypto(serializationManager));
}
if (metaChannel.udtChannel != null) {
pipeline = metaChannel.udtChannel.pipeline();
pipeline.replace(FRAME_AND_KRYO_DECODER, FRAME_AND_KRYO_CRYPTO_DECODER, new KryoDecoderCrypto(serializationManager)); // cannot be shared because of possible fragmentation.
if (idleTimeout > 0) {
pipeline.replace(IDLE_HANDLER, IDLE_HANDLER_FULL, new IdleStateHandler(0, 0, idleTimeout, TimeUnit.MILLISECONDS));
} else {
pipeline.remove(IDLE_HANDLER);
}
pipeline.replace(FRAME_AND_KRYO_ENCODER, FRAME_AND_KRYO_CRYPTO_ENCODER, registrationWrapper.getKryoTcpCryptoEncoder());
}
}
/**
* Setup our meta-channel to migrate to the correct connection handler for all regular data.
*/
protected final void establishConnection(MetaChannel metaChannel) {
ChannelPipeline tcpPipe = metaChannel.tcpChannel.pipeline();
ChannelPipeline udpPipe;
ChannelPipeline udtPipe;
if (metaChannel.udpChannel != null && metaChannel.udpRemoteAddress == null) {
// don't want to muck with the SERVER udp pipeline, as it NEVER CHANGES.
// only the client will have the udp remote address
udpPipe = metaChannel.udpChannel.pipeline();
} else {
udpPipe = null;
}
if (metaChannel.udtChannel != null) {
udtPipe = metaChannel.udtChannel.pipeline();
} else {
udtPipe = null;
}
// add the "connected"/"normal" handler now that we have established a "new" connection.
// This will have state, etc. for this connection.
ConnectionImpl connection = registrationWrapper.connection0(metaChannel);
tcpPipe.addLast(CONNECTION_HANDLER, connection);
if (udpPipe != null) {
// remember, server is different than client!
udpPipe.addLast(CONNECTION_HANDLER, connection);
}
if (udtPipe != null) {
udtPipe.addLast(CONNECTION_HANDLER, connection);
}
}
// have to setup AFTER establish connection, data, as we don't want to enable AES until we're ready.
protected final void setupConnection(MetaChannel metaChannel) {
boolean registerServer = false;
// now that we are CONNECTED, we want to remove ourselves (and channel ID's) from the map.
// they will be ADDED in another map, in the followup handler!!
try {
IntMap<MetaChannel> channelMap = registrationWrapper.getAndLockChannelMap();
channelMap.remove(metaChannel.tcpChannel.hashCode());
channelMap.remove(metaChannel.connectionID);
ChannelPipeline pipeline = metaChannel.tcpChannel.pipeline();
// The TCP channel is what calls this method, so we can use "this" for TCP, and the others are handled during the registration process
pipeline.remove(this);
if (metaChannel.udpChannel != null) {
// the setup is different between CLIENT / SERVER
if (metaChannel.udpRemoteAddress == null) {
// CLIENT RUNS THIS
// don't want to muck with the SERVER udp pipeline, as it NEVER CHANGES.
// More specifically, the UDP SERVER doesn't use a channelMap, it uses the udpRemoteMap
// to keep track of UDP connections. This is very different than how the client works
// only the client will have the udp remote address
channelMap.remove(metaChannel.udpChannel.hashCode());
} else {
// SERVER RUNS THIS
// don't ALWAYS have UDP on SERVER...
registerServer = true;
}
}
} finally {
registrationWrapper.releaseChannelMap();
}
if (registerServer) {
// Only called if we have a UDP channel
setupServerUdpConnection(metaChannel);
}
if (logger.isInfoEnabled()) {
String type = "TCP";
if (metaChannel.udpChannel != null) {
type += "/UDP";
}
if (metaChannel.udtChannel != null) {
type += "/UDT";
}
InetSocketAddress address = (InetSocketAddress)metaChannel.tcpChannel.remoteAddress();
logger.info("Created a {} connection with {}", type, address.getAddress());
}
}
/**
* Registers the metachannel for the UDP server. Default is to do nothing.
*
* The server will override this.
* Only called if we have a UDP channel when we finalize the setup of the TCP connection
*/
protected void setupServerUdpConnection(MetaChannel metaChannel) {
}
/**
* Internal call by the pipeline to notify the "Connection" object that it has "connected", meaning that modifications
* to the pipeline are finished.
*/
protected final void notifyConnection(MetaChannel metaChannel) {
registrationWrapper.connectionConnected0(metaChannel.connection);
}
@Override
public final void channelInactive(ChannelHandlerContext context) throws Exception {
Channel channel = context.channel();
logger.info("Closed connection: {}", channel.remoteAddress());
long maxShutdownWaitTimeInMilliSeconds = EndPoint.maxShutdownWaitTimeInMilliSeconds;
// also, once we notify, we unregister this.
// SEARCH for our channel!
// on the server, we only get this for TCP events!
try {
IntMap<MetaChannel> channelMap = registrationWrapper.getAndLockChannelMap();
Entries<MetaChannel> entries = channelMap.entries();
while (entries.hasNext()) {
MetaChannel metaChannel = entries.next().value;
if (metaChannel.tcpChannel == channel || metaChannel.udpChannel == channel || metaChannel.udtChannel == channel) {
metaChannel.close(maxShutdownWaitTimeInMilliSeconds);
entries.remove();
break;
}
}
} finally {
registrationWrapper.releaseChannelMap();
}
super.channelInactive(context);
}
@Override
public void exceptionCaught(ChannelHandlerContext context, Throwable cause) throws Exception {
Channel channel = context.channel();
logger.error("Unexpected exception while trying to send/receive data on Client remote (network) channel. ({})" + System.getProperty("line.separator"), channel.remoteAddress(), cause);
if (channel.isOpen()) {
channel.close();
}
}
}

View File

@ -0,0 +1,19 @@
package dorkbox.network.connection.registration.remote;
import dorkbox.network.connection.RegistrationWrapper;
import dorkbox.network.util.SerializationManager;
public class RegistrationRemoteHandlerClient extends RegistrationRemoteHandler {
public RegistrationRemoteHandlerClient(String name, RegistrationWrapper registrationWrapper, SerializationManager serializationManager) {
super(name, registrationWrapper, serializationManager);
}
/**
* @return the direction that traffic is going to this handler (" <== " or " ==> ")
*/
@Override
protected String getConnectionDirection() {
return " ==> ";
}
}

View File

@ -0,0 +1,336 @@
package dorkbox.network.connection.registration.remote;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.ReferenceCountUtil;
import java.math.BigInteger;
import java.net.InetSocketAddress;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import org.bouncycastle.crypto.BasicAgreement;
import org.bouncycastle.crypto.agreement.ECDHCBasicAgreement;
import org.bouncycastle.crypto.digests.SHA384Digest;
import org.bouncycastle.crypto.engines.IESEngine;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.spec.ECParameterSpec;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import dorkbox.network.connection.RegistrationWrapper;
import dorkbox.network.connection.registration.MetaChannel;
import dorkbox.network.connection.registration.Registration;
import dorkbox.network.util.SecurityException;
import dorkbox.network.util.SerializationManager;
import dorkbox.network.util.primativeCollections.IntMap;
import dorkbox.util.bytes.OptimizeUtils;
import dorkbox.util.crypto.Crypto;
import dorkbox.util.crypto.serialization.EccPublicKeySerializer;
public class RegistrationRemoteHandlerClientTCP extends RegistrationRemoteHandlerClient {
private static final String DELETE_IP = "eleteIP"; // purposefully missing the "D", since that is a system parameter, which starts with "-D"
private ThreadLocal<IESEngine> eccEngineLocal = new ThreadLocal<IESEngine>();
private final static ECParameterSpec eccSpec = ECNamedCurveTable.getParameterSpec(Crypto.ECC.p521_curve);
public RegistrationRemoteHandlerClientTCP(String name, RegistrationWrapper registrationWrapper, SerializationManager serializationManager) {
super(name, registrationWrapper, serializationManager);
// check to see if we need to delete an IP address as commanded from the user prompt
String ipAsString = System.getProperty(DELETE_IP);
if (ipAsString != null) {
System.setProperty(DELETE_IP, "");
byte[] address = null;
try {
String[] split = ipAsString.split("\\.");
if (split.length == 4) {
address = new byte[4];
for (int i=0;i<split.length;i++) {
int asInt = Integer.parseInt(split[i]);
if (asInt >= 0 && asInt <= 255) {
address[i] = (byte) Integer.parseInt(split[i]);
} else {
address = null;
break;
}
}
}
} catch (Exception e) {
address = null;
}
if (address != null) {
try {
registrationWrapper.removeRegisteredServerKey(address);
} catch (SecurityException e) {
this.logger.error(e.getMessage(), e);
}
}
}
// end command
}
private final IESEngine getEccEngine() {
IESEngine iesEngine = this.eccEngineLocal.get();
if (iesEngine == null) {
iesEngine = Crypto.ECC.createEngine();
this.eccEngineLocal.set(iesEngine);
}
return iesEngine;
}
/**
* STEP 1: Channel is first created
*/
@Override
protected void initChannel(Channel channel) {
this.logger.trace("Channel registered: {}", channel.getClass().getSimpleName());
// TCP & UDT
// use the default.
super.initChannel(channel);
}
/**
* STEP 2: Channel is now active. Start the registration process
*/
@Override
public void channelActive(ChannelHandlerContext context) throws Exception {
if (this.logger.isDebugEnabled()) {
super.channelActive(context);
}
Channel channel = context.channel();
// look to see if we already have a connection (in progress) for the destined IP address.
// Note: our CHANNEL MAP can only have one item at a time, since we do NOT RELEASE the registration lock until it's complete!!
// The ORDER has to be TCP (always) -> UDP (optional) -> UDT (optional)
// TCP
MetaChannel metaChannel = new MetaChannel();
metaChannel.tcpChannel = channel;
try {
IntMap<MetaChannel> channelMap = this.registrationWrapper.getAndLockChannelMap();
channelMap.put(channel.hashCode(), metaChannel);
} finally {
this.registrationWrapper.releaseChannelMap();
}
this.logger.trace("Start new TCP Connection. Sending request to server");
Registration registration = new Registration();
registration.publicKey = this.registrationWrapper.getPublicKey();
// client start the handshake with a registration packet
channel.writeAndFlush(registration);
}
@Override
public void channelRead(ChannelHandlerContext context, Object message) throws Exception {
Channel channel = context.channel();
if (message instanceof Registration) {
// make sure this connection was properly registered in the map. (IT SHOULD BE)
MetaChannel metaChannel = null;
try {
IntMap<MetaChannel> channelMap = this.registrationWrapper.getAndLockChannelMap();
metaChannel = channelMap.get(channel.hashCode());
} finally {
this.registrationWrapper.releaseChannelMap();
}
if (metaChannel != null) {
metaChannel.updateTcpRoundTripTime();
Registration registration = (Registration) message;
if (metaChannel.connectionID == null) {
// want to validate the public key used! This is similar to how SSH works, in that once we use a public key, we want to validate
// against that ip-address::key pair, so we can better protect against MITM/spoof attacks.
InetSocketAddress tcpRemoteServer = (InetSocketAddress) channel.remoteAddress();
boolean valid = this.registrationWrapper.validateRemoteServerAddress(tcpRemoteServer, registration.publicKey);
if (!valid) {
//whoa! abort since something messed up! (log happens inside of validate method)
String hostAddress = tcpRemoteServer.getAddress().getHostAddress();
this.logger.error("Invalid ECC public key for server IP {} during handshake. WARNING. The server has changed!", hostAddress);
this.logger.error("Fix by adding the argument -D{} {} when starting the client.", DELETE_IP, hostAddress);
metaChannel.changedRemoteKey = true;
shutdown(this.registrationWrapper, channel);
ReferenceCountUtil.release(message);
return;
}
// setup crypto state
IESEngine decrypt = getEccEngine();
byte[] aesKeyBytes = Crypto.ECC.decrypt(decrypt, this.registrationWrapper.getPrivateKey(), registration.publicKey, registration.eccParameters,
registration.aesKey);
if (aesKeyBytes.length != 32) {
this.logger.error("Invalid decryption of aesKey. Aborting.");
shutdown(this.registrationWrapper, channel);
ReferenceCountUtil.release(message);
return;
}
// now decrypt payload using AES
byte[] payload = Crypto.AES.decrypt(getAesEngine(), aesKeyBytes, registration.aesIV, registration.payload);
if (payload.length == 0) {
this.logger.error("Invalid decryption of payload. Aborting.");
shutdown(this.registrationWrapper, channel);
ReferenceCountUtil.release(message);
return;
}
OptimizeUtils optimizeUtils = OptimizeUtils.get();
if (!optimizeUtils.canReadInt(payload)) {
this.logger.error("Invalid decryption of connection ID. Aborting.");
shutdown(this.registrationWrapper, channel);
ReferenceCountUtil.release(message);
return;
}
metaChannel.connectionID = optimizeUtils.readInt(payload, true);
int intLength = optimizeUtils.intLength(metaChannel.connectionID, true);
/*
* Diffie-Hellman-Merkle key exchange for the AES key
* see http://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange
*/
byte[] ecdhPubKeyBytes = Arrays.copyOfRange(payload, intLength, payload.length);
ECPublicKeyParameters ecdhPubKey = EccPublicKeySerializer.read(new Input(ecdhPubKeyBytes));
if (ecdhPubKey == null) {
this.logger.error("Invalid decode of ecdh public key. Aborting.");
shutdown(this.registrationWrapper, channel);
ReferenceCountUtil.release(message);
return;
}
// It is OK that we generate a new ECC keypair for ECDHE everytime that we connect. The server rotates keys every XXXX
// seconds, since this step is expensive.
metaChannel.ecdhKey = Crypto.ECC.generateKeyPair(eccSpec, new SecureRandom());
// register the channel!
try {
IntMap<MetaChannel> channelMap = this.registrationWrapper.getAndLockChannelMap();
channelMap.put(metaChannel.connectionID, metaChannel);
} finally {
this.registrationWrapper.releaseChannelMap();
}
metaChannel.publicKey = registration.publicKey;
// now save our shared AES keys
BasicAgreement agreement = new ECDHCBasicAgreement();
agreement.init(metaChannel.ecdhKey.getPrivate());
BigInteger shared = agreement.calculateAgreement(ecdhPubKey);
// now we setup our AES key based on our shared secret! (from ECDH)
// the shared secret is different each time a connection is made
byte[] keySeed = shared.toByteArray();
SHA384Digest sha384 = new SHA384Digest();
byte[] digest = new byte[sha384.getDigestSize()];
sha384.update(keySeed, 0, keySeed.length);
sha384.doFinal(digest, 0);
metaChannel.aesKey = Arrays.copyOfRange(digest, 0, 32); // 256bit keysize (32 bytes)
metaChannel.aesIV = Arrays.copyOfRange(digest, 32, 48); // 128bit blocksize (16 bytes)
// abort if something messed up!
if (metaChannel.aesKey.length != 32) {
this.logger.error("Fatal error trying to use AES key (wrong key length).");
shutdown(this.registrationWrapper, channel);
ReferenceCountUtil.release(message);
return;
}
Registration register = new Registration();
// encrypt the ECDH public key using our previous AES info
Output output = new Output(1024);
EccPublicKeySerializer.write(output, (ECPublicKeyParameters) metaChannel.ecdhKey.getPublic());
byte[] pubKeyAsBytes = output.toBytes();
register.payload = Crypto.AES.encrypt(getAesEngine(), aesKeyBytes, registration.aesIV, pubKeyAsBytes);
channel.write(register);
ReferenceCountUtil.release(message);
return;
}
// else, we are further along in our registration process
// REGISTRATION CONNECTED!
else {
if (metaChannel.connection == null) {
// STEP 1: do we have our aes keys?
if (metaChannel.ecdhKey != null) {
// wipe out our ECDH value.
metaChannel.ecdhKey = null;
// notify the client that we are ready to continue registering other session protocols (bootstraps)
boolean isDoneWithRegistration = this.registrationWrapper.continueRegistration0();
// tell the server we are done, and to setup crypto on it's side
if (isDoneWithRegistration) {
channel.write(registration);
// re-sync the TCP delta round trip time
metaChannel.updateTcpRoundTripTime();
}
// if we are NOT done, then we will continue registering other protocols, so do nothing else here.
}
// we only get this when we are 100% done with the registration of all connection types.
else {
setupConnectionCrypto(metaChannel);
// AES ENCRPYTION NOW USED
// this sets up the pipeline for the client, so all the necessary handlers are ready to go
establishConnection(metaChannel);
setupConnection(metaChannel);
final MetaChannel metaChannel2 = metaChannel;
// wait for a "round trip" amount of time, then notify the APP!
channel.eventLoop().schedule(new Runnable() {
@Override
public void run() {
RegistrationRemoteHandlerClientTCP.this.logger.trace("Notify Connection");
notifyConnection(metaChannel2);
}}, metaChannel.getNanoSecBetweenTCP() * 2, TimeUnit.NANOSECONDS);
}
}
}
} else {
// this means that UDP beat us to the "punch", and notified before we did. (notify removes all the entries from the map)
}
}
else {
this.logger.error("Error registering TCP with remote server!");
shutdown(this.registrationWrapper, channel);
}
ReferenceCountUtil.release(message);
}
}

View File

@ -0,0 +1,172 @@
package dorkbox.network.connection.registration.remote;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPipeline;
import io.netty.util.ReferenceCountUtil;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import dorkbox.network.connection.RegistrationWrapper;
import dorkbox.network.connection.registration.MetaChannel;
import dorkbox.network.connection.registration.Registration;
import dorkbox.network.pipeline.udp.KryoDecoderUdp;
import dorkbox.network.pipeline.udp.KryoEncoderUdp;
import dorkbox.network.util.SerializationManager;
import dorkbox.network.util.primativeCollections.IntMap;
import dorkbox.network.util.primativeCollections.IntMap.Entries;
import dorkbox.util.bytes.OptimizeUtils;
import dorkbox.util.crypto.Crypto;
public class RegistrationRemoteHandlerClientUDP extends RegistrationRemoteHandlerClient {
public RegistrationRemoteHandlerClientUDP(String name, RegistrationWrapper registrationWrapper, SerializationManager serializationManager) {
super(name, registrationWrapper, serializationManager);
}
/**
* STEP 1: Channel is first created
*/
@Override
protected void initChannel(Channel channel) {
logger.trace("Channel registered: " + channel.getClass().getSimpleName());
ChannelPipeline pipeline = channel.pipeline();
// UDP
// add first to "inject" these handlers in front of myself.
// this is only called ONCE for UDP for the CLIENT.
pipeline.addFirst(RegistrationRemoteHandler.KRYO_DECODER, new KryoDecoderUdp(serializationManager));
pipeline.addFirst(RegistrationRemoteHandler.KRYO_ENCODER, new KryoEncoderUdp(serializationManager));
}
/**
* STEP 2: Channel is now active. Start the registration process
*/
@Override
public void channelActive(ChannelHandlerContext context) throws Exception {
if (logger.isDebugEnabled()) {
super.channelActive(context);
}
Channel channel = context.channel();
// look to see if we already have a connection (in progress) for the destined IP address.
// Note: our CHANNEL MAP can only have one item at a time, since we do NOT RELEASE the registration lock until it's complete!!
// The ORDER has to be TCP (always) -> UDP (optional) -> UDT (optional)
// UDP
boolean success = false;
InetSocketAddress udpRemoteAddress = (InetSocketAddress) channel.remoteAddress();
if (udpRemoteAddress != null) {
InetAddress udpRemoteServer = udpRemoteAddress.getAddress();
try {
IntMap<MetaChannel> channelMap = registrationWrapper.getAndLockChannelMap();
Entries<MetaChannel> entries = channelMap.entries();
while (entries.hasNext()) {
MetaChannel metaChannel = entries.next().value;
// associate TCP and UDP!
InetAddress tcpRemoteServer = ((InetSocketAddress) metaChannel.tcpChannel.remoteAddress()).getAddress();
if (checkEqual(tcpRemoteServer, udpRemoteServer)) {
channelMap.put(channel.hashCode(), metaChannel);
metaChannel.udpChannel = channel;
success = true;
// only allow one server per registration!
break;
}
}
} finally {
registrationWrapper.releaseChannelMap();
}
if (!success) {
throw new RuntimeException("UDP cannot connect to a remote server before TCP is established!");
}
logger.trace("Start new UDP Connection. Sending request to server");
Registration registration = new Registration();
// client start the handshake with a registration packet
channel.writeAndFlush(registration);
} else {
throw new RuntimeException("UDP cannot connect to remote server! No remote address specified!");
}
}
@Override
public void channelRead(ChannelHandlerContext context, Object message) throws Exception {
Channel channel = context.channel();
// if we also have a UDP channel, we will receive the "connected" message on UDP (otherwise it will be on TCP)
MetaChannel metaChannel = null;
try {
IntMap<MetaChannel> channelMap = registrationWrapper.getAndLockChannelMap();
metaChannel = channelMap.get(channel.hashCode());
} finally {
registrationWrapper.releaseChannelMap();
}
if (metaChannel != null) {
if (message instanceof Registration) {
Registration registration = (Registration) message;
// now decrypt channelID using AES
byte[] payload = Crypto.AES.decrypt(getAesEngine(), metaChannel.aesKey, metaChannel.aesIV, registration.payload);
OptimizeUtils optimizeUtils = OptimizeUtils.get();
if (!optimizeUtils.canReadInt(payload)) {
logger.error("Invalid decryption of connection ID. Aborting.");
shutdown(registrationWrapper, channel);
ReferenceCountUtil.release(message);
return;
}
Integer connectionID = optimizeUtils.readInt(payload, true);
MetaChannel metaChannel2 = null;
try {
IntMap<MetaChannel> channelMap = registrationWrapper.getAndLockChannelMap();
metaChannel2 = channelMap.get(connectionID);
} finally {
registrationWrapper.releaseChannelMap();
}
if (metaChannel2 != null) {
// hooray! we are successful
// notify the client that we are ready to continue registering other session protocols (bootstraps)
boolean isDoneWithRegistration = registrationWrapper.continueRegistration0();
// tell the server we are done, and to setup crypto on it's side
if (isDoneWithRegistration) {
// bounce it back over TCP, so we can receive a "final" connected message over TCP.
metaChannel.tcpChannel.writeAndFlush(registration);
// re-sync the TCP delta round trip time
metaChannel.updateTcpRoundTripTime();
}
// since we are done here, we need to REMOVE this handler
channel.pipeline().remove(this);
// if we are NOT done, then we will continue registering other protocols, so do nothing else here.
ReferenceCountUtil.release(message);
return;
}
}
}
// if we get here, there was an error!
logger.error("Error registering UDP with remote server!");
shutdown(registrationWrapper, channel);
ReferenceCountUtil.release(message);
}
}

View File

@ -0,0 +1,166 @@
package dorkbox.network.connection.registration.remote;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.ReferenceCountUtil;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import dorkbox.network.connection.RegistrationWrapper;
import dorkbox.network.connection.registration.MetaChannel;
import dorkbox.network.connection.registration.Registration;
import dorkbox.network.util.SerializationManager;
import dorkbox.network.util.primativeCollections.IntMap;
import dorkbox.network.util.primativeCollections.IntMap.Entries;
import dorkbox.util.bytes.OptimizeUtils;
import dorkbox.util.crypto.Crypto;
public class RegistrationRemoteHandlerClientUDT extends RegistrationRemoteHandlerClient {
public RegistrationRemoteHandlerClientUDT(String name, RegistrationWrapper registrationWrapper, SerializationManager serializationManager) {
super(name, registrationWrapper, serializationManager);
}
/**
* STEP 1: Channel is first created
*/
@Override
protected void initChannel(Channel channel) {
logger.trace("Channel registered: {}", channel.getClass().getSimpleName());
// TCP & UDT
// use the default.
super.initChannel(channel);
}
/**
* STEP 2: Channel is now active. Start the registration process
*/
@Override
public void channelActive(ChannelHandlerContext context) throws Exception {
if (logger.isDebugEnabled()) {
super.channelActive(context);
}
Channel channel = context.channel();
// look to see if we already have a connection (in progress) for the destined IP address.
// Note: our CHANNEL MAP can only have one item at a time, since we do NOT RELEASE the registration lock until it's complete!!
// The ORDER has to be TCP (always) -> UDP (optional) -> UDT (optional)
// UDT
boolean success = false;
InetSocketAddress udtRemoteAddress = (InetSocketAddress) channel.remoteAddress();
if (udtRemoteAddress != null) {
InetAddress udtRemoteServer = udtRemoteAddress.getAddress();
try {
IntMap<MetaChannel> channelMap = registrationWrapper.getAndLockChannelMap();
Entries<MetaChannel> entries = channelMap.entries();
while (entries.hasNext()) {
MetaChannel metaChannel = entries.next().value;
// associate TCP and UDP!
InetAddress tcpRemoteServer = ((InetSocketAddress) metaChannel.tcpChannel.remoteAddress()).getAddress();
if (checkEqual(tcpRemoteServer, udtRemoteServer)) {
channelMap.put(channel.hashCode(), metaChannel);
metaChannel.udtChannel = channel;
success = true;
// only allow one server per registration!
break;
}
}
} finally {
registrationWrapper.releaseChannelMap();
}
if (!success) {
throw new RuntimeException("UDT cannot connect to a remote server before TCP is established!");
}
logger.trace("Start new UDT Connection. Sending request to server");
Registration registration = new Registration();
// client start the handshake with a registration packet
channel.writeAndFlush(registration);
} else {
throw new RuntimeException("UDT cannot connect to remote server! No remote address specified!");
}
}
@Override
public void channelRead(ChannelHandlerContext context, Object message) throws Exception {
Channel channel = context.channel();
// if we also have a UDP channel, we will receive the "connected" message on UDP (otherwise it will be on TCP)
MetaChannel metaChannel = null;
try {
IntMap<MetaChannel> channelMap = registrationWrapper.getAndLockChannelMap();
metaChannel = channelMap.get(channel.hashCode());
} finally {
registrationWrapper.releaseChannelMap();
}
if (metaChannel != null) {
if (message instanceof Registration) {
Registration registration = (Registration) message;
// now decrypt channelID using AES
byte[] payload = Crypto.AES.decrypt(getAesEngine(), metaChannel.aesKey, metaChannel.aesIV, registration.payload);
OptimizeUtils optimizeUtils = OptimizeUtils.get();
if (!optimizeUtils.canReadInt(payload)) {
logger.error("Invalid decryption of connection ID. Aborting.");
shutdown(registrationWrapper, channel);
ReferenceCountUtil.release(message);
return;
}
Integer connectionID = optimizeUtils.readInt(payload, true);
MetaChannel metaChannel2 = null;
try {
IntMap<MetaChannel> channelMap = registrationWrapper.getAndLockChannelMap();
metaChannel2 = channelMap.get(connectionID);
} finally {
registrationWrapper.releaseChannelMap();
}
if (metaChannel2 != null) {
// hooray! we are successful
// notify the client that we are ready to continue registering other session protocols (bootstraps)
boolean isDoneWithRegistration = registrationWrapper.continueRegistration0();
// tell the server we are done, and to setup crypto on it's side
if (isDoneWithRegistration) {
// bounce it back over TCP, so we can receive a "final" connected message over TCP.
metaChannel.tcpChannel.writeAndFlush(registration);
// re-sync the TCP delta round trip time
metaChannel.updateTcpRoundTripTime();
}
// since we are done here, we need to REMOVE this handler
channel.pipeline().remove(this);
// if we are NOT done, then we will continue registering other protocols, so do nothing else here.
ReferenceCountUtil.release(message);
return;
}
}
}
// if we get here, there was an error!
logger.error("Error registering UDT with remote server!");
shutdown(registrationWrapper, channel);
ReferenceCountUtil.release(message);
}
}

View File

@ -0,0 +1,29 @@
package dorkbox.network.connection.registration.remote;
import dorkbox.network.connection.RegistrationWrapper;
import dorkbox.network.connection.registration.MetaChannel;
import dorkbox.network.util.SerializationManager;
public class RegistrationRemoteHandlerServer extends RegistrationRemoteHandler {
public RegistrationRemoteHandlerServer(String name, RegistrationWrapper registrationWrapper, SerializationManager serializationManager) {
super(name, registrationWrapper, serializationManager);
}
/**
* Registers the metachannel for the UDP server (For the TCP/UDT streams)
*/
@Override
protected void setupServerUdpConnection(MetaChannel metaChannel) {
registrationWrapper.registerServerUDP(metaChannel);
}
/**
* @return the direction that traffic is going to this handler (" <== " or " ==> ")
*/
@Override
protected String getConnectionDirection() {
return " <== ";
}
}

View File

@ -0,0 +1,322 @@
package dorkbox.network.connection.registration.remote;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.ReferenceCountUtil;
import java.math.BigInteger;
import java.net.InetSocketAddress;
import java.security.SecureRandom;
import java.util.concurrent.TimeUnit;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.BasicAgreement;
import org.bouncycastle.crypto.agreement.ECDHCBasicAgreement;
import org.bouncycastle.crypto.digests.SHA384Digest;
import org.bouncycastle.crypto.engines.IESEngine;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.spec.ECParameterSpec;
import org.bouncycastle.util.Arrays;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import dorkbox.network.connection.RegistrationWrapper;
import dorkbox.network.connection.registration.MetaChannel;
import dorkbox.network.connection.registration.Registration;
import dorkbox.network.util.RandomConnectionIdGenerator;
import dorkbox.network.util.SerializationManager;
import dorkbox.network.util.primativeCollections.IntMap;
import dorkbox.util.bytes.OptimizeUtils;
import dorkbox.util.crypto.Crypto;
import dorkbox.util.crypto.serialization.EccPublicKeySerializer;
public class RegistrationRemoteHandlerServerTCP extends RegistrationRemoteHandlerServer {
private static final long ECDH_TIMEOUT = 10*60*60*1000*1000*1000; // 10 minutes in nanoseconds
private final static ECParameterSpec eccSpec = ECNamedCurveTable.getParameterSpec(Crypto.ECC.p521_curve);
private ThreadLocal<IESEngine> eccEngineLocal = new ThreadLocal<IESEngine>();
private final Object ecdhKeyLock = new Object();
private AsymmetricCipherKeyPair ecdhKeyPair = Crypto.ECC.generateKeyPair(eccSpec, new SecureRandom());
private volatile long ecdhTimeout = System.nanoTime();
public RegistrationRemoteHandlerServerTCP(String name, RegistrationWrapper registrationWrapper, SerializationManager serializationManager) {
super(name, registrationWrapper, serializationManager);
}
private final IESEngine getEccEngine() {
IESEngine iesEngine = this.eccEngineLocal.get();
if (iesEngine == null) {
iesEngine = Crypto.ECC.createEngine();
this.eccEngineLocal.set(iesEngine);
}
return iesEngine;
}
/**
* Rotates the ECDH key every 10 minutes, as this is a VERY expensive calculation to keep on doing for every connection.
*/
private AsymmetricCipherKeyPair getEchdKeyOnRotate(SecureRandom secureRandom) {
if (System.nanoTime() - this.ecdhTimeout > ECDH_TIMEOUT) {
synchronized (this.ecdhKeyLock) {
this.ecdhTimeout = System.nanoTime();
this.ecdhKeyPair = Crypto.ECC.generateKeyPair(eccSpec, secureRandom);
}
}
return this.ecdhKeyPair;
}
/**
* STEP 1: Channel is first created (This is TCP/UDT only, as such it differs from the client which is TCP/UDP)
*/
@Override
protected void initChannel(Channel channel) {
super.initChannel(channel);
}
/**
* STEP 2: Channel is now active. Prepare the meta channel to listen for the registration process
*/
@Override
public void channelActive(ChannelHandlerContext context) throws Exception {
if (this.logger.isDebugEnabled()) {
super.channelActive(context);
}
Channel channel = context.channel();
// The ORDER has to be TCP (always) -> UDP (optional, in UDP listener) -> UDT (optional)
// TCP
// save this new connection in our associated map. We will get a new one for each new connection from a client.
MetaChannel metaChannel = new MetaChannel();
metaChannel.tcpChannel = channel;
try {
IntMap<MetaChannel> channelMap = this.registrationWrapper.getAndLockChannelMap();
channelMap.put(channel.hashCode(), metaChannel);
} finally {
this.registrationWrapper.releaseChannelMap();
}
this.logger.trace(this.name, "New TCP connection. Saving TCP channel info.");
}
/**
* STEP 3-XXXXX: We pass registration messages around until we the registration handshake is complete!
*/
@Override
public void channelRead(ChannelHandlerContext context, Object message) throws Exception {
Channel channel = context.channel();
// only TCP will come across here for the server. (UDP here is called by the UDP handler/wrapper)
if (message instanceof Registration) {
Registration registration = (Registration) message;
MetaChannel metaChannel = null;
try {
IntMap<MetaChannel> channelMap = this.registrationWrapper.getAndLockChannelMap();
metaChannel = channelMap.get(channel.hashCode());
} finally {
this.registrationWrapper.releaseChannelMap();
}
// make sure this connection was properly registered in the map. (IT SHOULD BE)
if (metaChannel != null) {
metaChannel.updateTcpRoundTripTime();
SecureRandom secureRandom = this.registrationWrapper.getSecureRandom();
// first time we've seen data from this new TCP connection
if (metaChannel.connectionID == null) {
// whoa! Didn't send valid public key info!
if (registration.publicKey == null) {
this.logger.error("Null ECC public key during client handshake. This shouldn't happen!");
shutdown(this.registrationWrapper, channel);
ReferenceCountUtil.release(message);
return;
}
// want to validate the public key used! This is similar to how SSH works, in that once we use a public key, we want to validate
// against that ip-address::key pair, so we can better protect against MITM/spoof attacks.
InetSocketAddress tcpRemoteClient = (InetSocketAddress) channel.remoteAddress();
boolean valid = this.registrationWrapper.validateRemoteServerAddress(tcpRemoteClient, registration.publicKey);
if (!valid) {
//whoa! abort since something messed up! (log happens inside of validate method)
this.logger.info("Invalid ECC public key for IP {} during handshake with client. Toggling extra flag in channel to indicate this.", tcpRemoteClient.getAddress().getHostAddress());
metaChannel.changedRemoteKey = true;
}
Integer connectionID = RandomConnectionIdGenerator.getRandom();
// if I'm unlucky, keep from confusing connections!
try {
IntMap<MetaChannel> channelMap = this.registrationWrapper.getAndLockChannelMap();
while (channelMap.containsKey(connectionID)) {
connectionID = RandomConnectionIdGenerator.getRandom();
}
metaChannel.connectionID = connectionID;
channelMap.put(connectionID, metaChannel);
} finally {
this.registrationWrapper.releaseChannelMap();
}
Registration register = new Registration();
// save off encryption handshake info
metaChannel.publicKey = registration.publicKey;
OptimizeUtils optimizeUtils = OptimizeUtils.get();
// use ECC to create an AES key, which is used to encrypt the ECDH public key and the connectionID
/*
* Diffie-Hellman-Merkle key
* see http://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange
*/
// the ecdh key will ROTATE every 10 minutes, since generating it for EVERY connection is expensive
// and since we are combining ECDHE+ECC public/private keys for each connection, other
// connections cannot break someone else's connection, since they are still protected by their own private keys.
metaChannel.ecdhKey = getEchdKeyOnRotate(secureRandom);
Output output = new Output(1024);
EccPublicKeySerializer.write(output, (ECPublicKeyParameters) metaChannel.ecdhKey.getPublic());
byte[] pubKeyAsBytes = output.toBytes();
// save off the connectionID as a byte array
int intLength = optimizeUtils.intLength(connectionID, true);
byte[] idAsBytes = new byte[intLength];
optimizeUtils.writeInt(idAsBytes, connectionID, true);
byte[] combinedBytes = Arrays.concatenate(idAsBytes, pubKeyAsBytes);
// now we have to setup the TEMP AES key!
metaChannel.aesKey = new byte[32]; // 256bit keysize (32 bytes)
metaChannel.aesIV = new byte[16]; // 128bit blocksize (16 bytes)
secureRandom.nextBytes(metaChannel.aesKey);
secureRandom.nextBytes(metaChannel.aesIV);
IESEngine encrypt = getEccEngine();
register.publicKey = this.registrationWrapper.getPublicKey();
register.eccParameters = Crypto.ECC.generateSharedParameters(secureRandom);
// now we have to ENCRYPT the AES key!
register.eccParameters = Crypto.ECC.generateSharedParameters(secureRandom);
register.aesIV = metaChannel.aesIV;
register.aesKey = Crypto.ECC.encrypt(encrypt, this.registrationWrapper.getPrivateKey(), metaChannel.publicKey, register.eccParameters, metaChannel.aesKey);
// now encrypt payload via AES
register.payload = Crypto.AES.encrypt(getAesEngine(), metaChannel.aesKey, register.aesIV, combinedBytes);
channel.write(register);
this.logger.trace("Assigning new random connection ID for TCP and performing ECDH.");
// re-sync the TCP delta round trip time
metaChannel.updateTcpRoundTripTime();
ReferenceCountUtil.release(message);
return;
}
// else continue the registration process
else {
// do we have a connection setup yet?
if (metaChannel.connection == null) {
// check if we have ECDH specified (if we do, then we are at STEP 1).
if (metaChannel.ecdhKey != null) {
// now we have to decrypt the ECDH key using our TEMP AES keys
byte[] payload = Crypto.AES.decrypt(getAesEngine(), metaChannel.aesKey, metaChannel.aesIV, registration.payload);
if (payload.length == 0) {
this.logger.error("Invalid decryption of payload. Aborting.");
shutdown(this.registrationWrapper, channel);
ReferenceCountUtil.release(message);
return;
}
ECPublicKeyParameters ecdhPubKey = EccPublicKeySerializer.read(new Input(payload));
if (ecdhPubKey == null) {
this.logger.error("Invalid decode of ecdh public key. Aborting.");
shutdown(this.registrationWrapper, channel);
ReferenceCountUtil.release(message);
return;
}
BasicAgreement agreement = new ECDHCBasicAgreement();
agreement.init(metaChannel.ecdhKey.getPrivate());
BigInteger shared = agreement.calculateAgreement(ecdhPubKey);
// wipe out our saved values.
metaChannel.aesKey = null;
metaChannel.aesIV = null;
metaChannel.ecdhKey = null;
// now we setup our AES key based on our shared secret! (from ECDH)
// the shared secret is different each time a connection is made
byte[] keySeed = shared.toByteArray();
SHA384Digest sha384 = new SHA384Digest();
byte[] digest = new byte[sha384.getDigestSize()];
sha384.update(keySeed, 0, keySeed.length);
sha384.doFinal(digest, 0);
metaChannel.aesKey = Arrays.copyOfRange(digest, 0, 32); // 256bit keysize (32 bytes)
metaChannel.aesIV = Arrays.copyOfRange(digest, 32, 48); // 128bit blocksize (16 bytes)
// tell the client to continue it's registration process.
channel.write(new Registration());
}
// we only get this when we are 100% done with the registration of all connection types.
else {
channel.write(registration); // causes client to setup network connection & AES
setupConnectionCrypto(metaChannel);
// AES ENCRPYTION NOW USED
// this sets up the pipeline for the server, so all the necessary handlers are ready to go
establishConnection(metaChannel);
setupConnection(metaChannel);
final MetaChannel chan2 = metaChannel;
// wait for a "round trip" amount of time, then notify the APP!
channel.eventLoop().schedule(new Runnable() {
@Override
public void run() {
RegistrationRemoteHandlerServerTCP.this.logger.trace("Notify Connection");
notifyConnection(chan2);
}}, metaChannel.getNanoSecBetweenTCP() * 2, TimeUnit.NANOSECONDS);
}
}
ReferenceCountUtil.release(message);
return;
}
}
// this should NEVER happen!
this.logger.error("Error registering TCP channel! MetaChannel is null!");
}
shutdown(this.registrationWrapper, channel);
ReferenceCountUtil.release(message);
}
}

View File

@ -0,0 +1,270 @@
package dorkbox.network.connection.registration.remote;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.socket.DatagramPacket;
import io.netty.handler.codec.MessageToMessageCodec;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.List;
import dorkbox.network.Broadcast;
import dorkbox.network.connection.Connection;
import dorkbox.network.connection.ConnectionImpl;
import dorkbox.network.connection.EndPoint;
import dorkbox.network.connection.RegistrationWrapper;
import dorkbox.network.connection.registration.MetaChannel;
import dorkbox.network.connection.registration.Registration;
import dorkbox.network.connection.wrapper.UdpWrapper;
import dorkbox.network.util.NetException;
import dorkbox.network.util.SerializationManager;
import dorkbox.network.util.primativeCollections.IntMap;
import dorkbox.network.util.primativeCollections.IntMap.Entries;
import dorkbox.util.bytes.OptimizeUtils;
import dorkbox.util.crypto.Crypto;
@Sharable
public class RegistrationRemoteHandlerServerUDP extends MessageToMessageCodec<DatagramPacket, UdpWrapper> {
// this is for the SERVER only. UDP channel is ALWAYS the SAME channel (it's the server's listening channel).
private final org.slf4j.Logger logger;
private final String name;
private final ByteBuf discoverResponseBuffer;
private final RegistrationWrapper registrationWrapper;
private final SerializationManager serializationManager;
public RegistrationRemoteHandlerServerUDP(String name, RegistrationWrapper registrationWrapper, SerializationManager serializationManager) {
this.name = name + " Registration-UDP-Server";
logger = org.slf4j.LoggerFactory.getLogger(this.name);
this.registrationWrapper = registrationWrapper;
this.serializationManager = serializationManager;
// absolutely MUST send packet > 0 across, otherwise netty will think it failed to write to the socket, and keep trying. (bug was fixed by netty. Keeping this code)
discoverResponseBuffer = Unpooled.buffer(1);
discoverResponseBuffer.writeByte(Broadcast.broadcastResponseID);
}
/**
* STEP 2: Channel is now active. We are now LISTENING to UDP messages!
*/
@Override
public void channelActive(ChannelHandlerContext context) throws Exception {
// do NOT want to add UDP channels, since they are tracked differently for the server.
}
@Override
public void exceptionCaught(ChannelHandlerContext context, Throwable cause) throws Exception {
// log UDP errors.
logger.error("Exception caught in UDP stream.", cause);
super.exceptionCaught(context, cause);
}
@Override
protected void encode(ChannelHandlerContext context, UdpWrapper msg, List<Object> out) throws Exception {
Object object = msg.object();
InetSocketAddress remoteAddress = msg.remoteAddress();
if (object instanceof ByteBuf) {
// this is the response from a discoverHost query
out.add(new DatagramPacket((ByteBuf) object, remoteAddress));
} else {
ByteBuf buffer = Unpooled.buffer(EndPoint.udpMaxSize);
sendUDP(context, object, buffer, remoteAddress);
if (buffer != null) {
out.add(new DatagramPacket(buffer, remoteAddress));
}
}
}
@Override
protected void decode(ChannelHandlerContext context, DatagramPacket msg, List<Object> out) throws Exception {
Channel channel = context.channel();
ByteBuf data = msg.content();
InetSocketAddress remoteAddress = msg.sender();
// must have a remote address in the packet. (ie, ignore broadcast)
if (remoteAddress == null) {
logger.debug("Ignoring packet with null UDP remote address. (Is it broadcast?)");
return;
}
if (data.readableBytes() == 1) {
if (data.readByte() == Broadcast.broadcastID) {
// CANNOT use channel.getRemoteAddress()
channel.writeAndFlush(new UdpWrapper(discoverResponseBuffer, remoteAddress));
logger.debug("Responded to host discovery from: {}", remoteAddress);
} else {
logger.error("Invalid signature for 'Discover Host' from remote address: {}", remoteAddress);
}
} else {
// we cannot use the REGULAR pipeline, since we can't pass along the remote address for
// when we establish the "network connection"
// send on the message, now that we have the WRITE channel figured out and the data.
receivedUDP(context, channel, data, remoteAddress);
}
}
public final void sendUDP(ChannelHandlerContext context, Object object, ByteBuf buffer, InetSocketAddress udpRemoteAddress) {
Connection networkConnection = registrationWrapper.getServerUDP(udpRemoteAddress);
if (networkConnection != null) {
// try to write data! (IT SHOULD ALWAYS BE ENCRYPTED HERE!)
serializationManager.writeWithCryptoUdp(networkConnection, buffer, object);
} else {
// this means we are still in the REGISTRATION phase.
serializationManager.write(buffer, object);
}
}
// this will be invoked by the UdpRegistrationHandlerServer. Remember, TCP will be established first.
public final void receivedUDP(ChannelHandlerContext context, Channel channel, ByteBuf data, InetSocketAddress udpRemoteAddress) throws Exception {
// registration is the ONLY thing NOT encrypted
if (serializationManager.isEncrypted(data)) {
// we need to FORWARD this message "down the pipeline".
ConnectionImpl connection = registrationWrapper.getServerUDP(udpRemoteAddress);
if (connection != null) {
// try to read data! (IT SHOULD ALWAYS BE ENCRYPTED HERE!)
Object object;
try {
object = serializationManager.readWithCryptoUdp(connection, data, data.writerIndex());
} catch (NetException e) {
logger.error("UDP unable to deserialize buffer", e);
shutdown(registrationWrapper, channel);
return;
}
connection.channelRead(object);
}
// if we don't have this "from" IP address ALREADY registered, drop the packet.
// OR the channel was shutdown while it was still receiving data.
else {
// we DON'T CARE about this, so we will just ignore the incoming message.
}
}
// manage the registration packets!
else {
Object object;
try {
object = serializationManager.read(data, data.writerIndex());
} catch (NetException e) {
logger.error("UDP unable to deserialize buffer", e);
shutdown(registrationWrapper, channel);
return;
}
if (object instanceof Registration) {
boolean matches = false;
MetaChannel metaChannel = null;
try {
// find out and make sure that UDP and TCP are talking to the same server
InetAddress udpRemoteServer = udpRemoteAddress.getAddress();
IntMap<MetaChannel> channelMap = registrationWrapper.getAndLockChannelMap();
Entries<MetaChannel> entries = channelMap.entries();
while (entries.hasNext()) {
metaChannel = entries.next().value;
// only look at connections that do not have UDP already setup.
if (metaChannel.udpChannel == null) {
InetSocketAddress tcpRemote = (InetSocketAddress) metaChannel.tcpChannel.remoteAddress();
InetAddress tcpRemoteAddress = tcpRemote.getAddress();
if (RegistrationRemoteHandler.checkEqual(tcpRemoteAddress, udpRemoteServer)) {
matches = true;
break;
} else {
logger.error("Mismatch UDP and TCP client addresses! UDP: {} TCP: {}", udpRemoteServer, tcpRemoteAddress);
shutdown(registrationWrapper, channel);
return;
}
}
}
} finally {
registrationWrapper.releaseChannelMap();
}
if (matches && metaChannel != null) {
// associate TCP and UDP!
metaChannel.udpChannel = channel;
metaChannel.udpRemoteAddress = udpRemoteAddress;
Registration register = new Registration();
// save off the connectionID as a byte array, then encrypt it
OptimizeUtils optimizeUtils = OptimizeUtils.get();
int intLength = optimizeUtils.intLength(metaChannel.connectionID, true);
byte[] idAsBytes = new byte[intLength];
optimizeUtils.writeInt(idAsBytes, metaChannel.connectionID, true);
// now encrypt payload via AES
register.payload = Crypto.AES.encrypt(RegistrationRemoteHandler.getAesEngine(), metaChannel.aesKey, metaChannel.aesIV, idAsBytes);
channel.writeAndFlush(new UdpWrapper(register, udpRemoteAddress));
logger.trace("Register UDP connection from {}", udpRemoteAddress);
return;
}
// if we get here, there was a failure!
logger.error("Error trying to register UDP without udp specified! UDP: {}", udpRemoteAddress);
shutdown(registrationWrapper, channel);
return;
}
else {
logger.error("UDP attempting to spoof client! Unencrypted packet other than registration received.");
shutdown(null, channel);
return;
}
}
}
/**
* Copied from RegistrationHandler. There were issues accessing it as static with generics.
*/
public MetaChannel shutdown(RegistrationWrapper registrationWrapper, Channel channel) {
logger.error("SHUTDOWN HANDLER REACHED! SOMETHING MESSED UP! TRYING TO ABORT");
// shutdown. Something messed up. Only reach this is something messed up.
// properly shutdown the TCP/UDP channels.
if (channel.isOpen()) {
channel.close();
}
// also, once we notify, we unregister this.
if (registrationWrapper != null) {
try {
IntMap<MetaChannel> channelMap = registrationWrapper.getAndLockChannelMap();
Entries<MetaChannel> entries = channelMap.entries();
while (entries.hasNext()) {
MetaChannel metaChannel = entries.next().value;
if (metaChannel.localChannel == channel || metaChannel.tcpChannel == channel || metaChannel.udpChannel == channel) {
entries.remove();
metaChannel.close();
return metaChannel;
}
}
} finally {
registrationWrapper.releaseChannelMap();
}
}
return null;
}
}

View File

@ -0,0 +1,125 @@
package dorkbox.network.connection.registration.remote;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.ReferenceCountUtil;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import dorkbox.network.connection.RegistrationWrapper;
import dorkbox.network.connection.registration.MetaChannel;
import dorkbox.network.connection.registration.Registration;
import dorkbox.network.util.SerializationManager;
import dorkbox.network.util.primativeCollections.IntMap;
import dorkbox.network.util.primativeCollections.IntMap.Entries;
import dorkbox.util.bytes.OptimizeUtils;
import dorkbox.util.crypto.Crypto;
public class RegistrationRemoteHandlerServerUDT extends RegistrationRemoteHandlerServer {
public RegistrationRemoteHandlerServerUDT(String name, RegistrationWrapper registrationWrapper, SerializationManager serializationManager) {
super(name, registrationWrapper, serializationManager);
}
/**
* STEP 1: Channel is first created (This is TCP/UDT only, as such it differs from the client which is TCP/UDP)
*/
@Override
protected void initChannel(Channel channel) {
super.initChannel(channel);
}
/**
* STEP 2: Channel is now active. Prepare the meta channel to listen for the registration process
*/
@Override
public void channelActive(ChannelHandlerContext context) throws Exception {
if (logger.isDebugEnabled()) {
super.channelActive(context);
}
// UDT channels are added when the registration request arrives on a UDT channel.
}
/**
* STEP 3-XXXXX: We pass registration messages around until we the registration handshake is complete!
*/
@Override
public void channelRead(ChannelHandlerContext context, Object message) throws Exception {
Channel channel = context.channel();
// only TCP will come across here for the server. (UDP here is called by the UDP handler/wrapper)
if (message instanceof Registration) {
// find out and make sure that UDP and TCP are talking to the same server
InetAddress udtRemoteAddress = ((InetSocketAddress) channel.remoteAddress()).getAddress();
boolean matches = false;
MetaChannel metaChannel = null;
try {
IntMap<MetaChannel> channelMap = registrationWrapper.getAndLockChannelMap();
Entries<MetaChannel> entries = channelMap.entries();
while (entries.hasNext()) {
metaChannel = entries.next().value;
// only look at connections that do not have UDT already setup.
if (metaChannel.udtChannel == null) {
InetSocketAddress tcpRemote = (InetSocketAddress) metaChannel.tcpChannel.remoteAddress();
InetAddress tcpRemoteAddress = tcpRemote.getAddress();
if (checkEqual(tcpRemoteAddress, udtRemoteAddress)) {
matches = true;
} else {
logger.error(name, "Mismatch UDT and TCP client addresses! UDP: {} TCP: {}", udtRemoteAddress, tcpRemoteAddress);
shutdown(registrationWrapper, channel);
ReferenceCountUtil.release(message);
return;
}
}
}
} finally {
registrationWrapper.releaseChannelMap();
}
if (matches && metaChannel != null) {
// associate TCP and UDT!
metaChannel.udtChannel = channel;
Registration register = new Registration();
// save off the connectionID as a byte array, then encrypt it
OptimizeUtils optimizeUtils = OptimizeUtils.get();
int intLength = optimizeUtils.intLength(metaChannel.connectionID, true);
byte[] idAsBytes = new byte[intLength];
optimizeUtils.writeInt(idAsBytes, metaChannel.connectionID, true);
// now encrypt payload via AES
register.payload = Crypto.AES.encrypt(RegistrationRemoteHandler.getAesEngine(), metaChannel.aesKey, metaChannel.aesIV, idAsBytes);
// send back, so the client knows that UDP was ok. We include the encrypted connection ID, so the client knows it's a legit server
channel.writeAndFlush(register);
// since we are done here, we need to REMOVE this handler
channel.pipeline().remove(this);
logger.trace("Register UDT connection from {}", udtRemoteAddress);
ReferenceCountUtil.release(message);
return;
}
// if we get here, there was a failure!
logger.error("Error trying to register UDT without udt specified! UDT: {}", udtRemoteAddress);
shutdown(registrationWrapper, channel);
ReferenceCountUtil.release(message);
return;
}
else {
logger.error("UDT attempting to spoof client! Unencrypted packet other than registration received.");
shutdown(registrationWrapper, channel);
ReferenceCountUtil.release(message);
return;
}
}
}

View File

@ -0,0 +1,122 @@
package dorkbox.network.connection.wrapper;
import io.netty.channel.Channel;
import io.netty.channel.local.LocalAddress;
import java.util.concurrent.atomic.AtomicBoolean;
import org.bouncycastle.crypto.params.ParametersWithIV;
import dorkbox.network.connection.Connection;
import dorkbox.network.connection.ConnectionPoint;
import dorkbox.network.connection.EndPoint;
import dorkbox.network.connection.ISessionManager;
import dorkbox.network.connection.ping.PingFuture;
import dorkbox.network.connection.ping.PingFutureLocal;
import dorkbox.network.connection.registration.MetaChannel;
public class ChannelLocalWrapper implements ChannelWrapper, ConnectionPoint {
private final Channel channel;
private String remoteAddress;
private AtomicBoolean shouldFlush = new AtomicBoolean(false);
public ChannelLocalWrapper(MetaChannel metaChannel) {
this.channel = metaChannel.localChannel;
}
/**
* Initialize the connection with any extra info that is needed but was unavailable at the channel construction.
*/
@Override
public final void init() {
remoteAddress = ((LocalAddress) this.channel.remoteAddress()).id();
}
@Override
public void write(Object object) {
channel.write(object);
shouldFlush.set(true);
}
/**
* Flushes the contents of the LOCAL pipes to the actual transport.
*/
@Override
public void flush() {
if (shouldFlush.compareAndSet(true, false)) {
channel.flush();
}
}
@Override
public void close(Connection connection, ISessionManager sessionManager) {
long maxShutdownWaitTimeInMilliSeconds = EndPoint.maxShutdownWaitTimeInMilliSeconds;
// Wait until the connection is closed or the connection attempt fails.
channel.close().awaitUninterruptibly(maxShutdownWaitTimeInMilliSeconds);
}
@Override
public ConnectionPoint tcp() {
return this;
}
@Override
public ConnectionPoint udp() {
return this;
}
@Override
public ConnectionPoint udt() {
return this;
}
@Override
public PingFuture pingFuture() {
return new PingFutureLocal();
}
@Override
public ParametersWithIV cryptoParameters() {
return null;
}
@Override
public final String getRemoteHost() {
return remoteAddress;
}
@Override
public int id() {
return channel.hashCode();
}
@Override
public int hashCode() {
return channel.hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
ChannelLocalWrapper other = (ChannelLocalWrapper) obj;
if (this.remoteAddress == null) {
if (other.remoteAddress != null) {
return false;
}
} else if (!this.remoteAddress.equals(other.remoteAddress)) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,47 @@
package dorkbox.network.connection.wrapper;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import dorkbox.network.connection.ConnectionPoint;
public class ChannelNetwork implements ConnectionPoint {
private volatile ChannelFuture lastWriteFuture;
private final Channel channel;
private volatile boolean shouldFlush = false;
public ChannelNetwork(Channel channel) {
this.channel = channel;
}
@Override
public void write(Object object) {
shouldFlush = true;
lastWriteFuture = channel.write(object);
}
@Override
public void flush() {
if (shouldFlush) {
shouldFlush = false;
channel.flush();
}
}
public void close(long maxShutdownWaitTimeInMilliSeconds) {
// Wait until all messages are flushed before closing the channel.
if (lastWriteFuture != null) {
lastWriteFuture.awaitUninterruptibly(maxShutdownWaitTimeInMilliSeconds);
lastWriteFuture = null;
}
shouldFlush = false;
channel.close().awaitUninterruptibly(maxShutdownWaitTimeInMilliSeconds);
}
public int id() {
return channel.hashCode();
}
}

View File

@ -0,0 +1,44 @@
package dorkbox.network.connection.wrapper;
import io.netty.channel.Channel;
import java.net.InetSocketAddress;
import dorkbox.network.connection.UdpServer;
import dorkbox.network.util.NetException;
public class ChannelNetworkUdp extends ChannelNetwork {
private final InetSocketAddress udpRemoteAddress;
private final UdpServer udpServer;
public ChannelNetworkUdp(Channel channel, InetSocketAddress udpRemoteAddress, UdpServer udpServer) {
super(channel);
if (udpRemoteAddress == null) {
throw new NetException("Cannot create a server UDP channel wihtout a remote udp address!");
}
this.udpRemoteAddress = udpRemoteAddress;
this.udpServer = udpServer; // ONLY valid in the server!
}
@Override
public void write(Object object) {
// this shoots out the SERVER pipeline, which is SLIGHTLY different!
super.write(new UdpWrapper(object, udpRemoteAddress));
}
@Override
public void close(long maxShutdownWaitTimeInMilliSeconds) {
// we ONLY want to close the UDP channel when we are STOPPING the server, otherwise we close the UDP channel
// that listens for new connections! SEE Server.close().
// super.close(maxShutdownWaitTimeInMilliSeconds);
// need to UNREGISTER the address from my ChannelManager.
if (udpServer != null) {
// only the server does this.
udpServer.unRegisterServerUDP(udpRemoteAddress);
}
}
}

View File

@ -0,0 +1,185 @@
package dorkbox.network.connection.wrapper;
import io.netty.channel.Channel;
import io.netty.channel.EventLoop;
import io.netty.util.concurrent.Promise;
import java.net.InetSocketAddress;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;
import dorkbox.network.connection.Connection;
import dorkbox.network.connection.ConnectionPoint;
import dorkbox.network.connection.EndPoint;
import dorkbox.network.connection.ISessionManager;
import dorkbox.network.connection.UdpServer;
import dorkbox.network.connection.ping.PingFuture;
import dorkbox.network.connection.registration.MetaChannel;
public class ChannelNetworkWrapper implements ChannelWrapper {
private final ChannelNetwork tcp;
private final ChannelNetwork udp;
private final ChannelNetwork udt;
private final ParametersWithIV cryptoAesKeyAndIV;
// did the remote connection public ECC key change?
private final boolean remotePublicKeyChanged;
private final String remoteAddress;
private final EventLoop eventLoop;
/**
* @param udpServer is null when created by the client, non-null when created by the server
*/
public ChannelNetworkWrapper(MetaChannel metaChannel, UdpServer udpServer) {
Channel tcpChannel = metaChannel.tcpChannel;
eventLoop = tcpChannel.eventLoop();
tcp = new ChannelNetwork(tcpChannel);
if (metaChannel.udpChannel != null) {
if (metaChannel.udpRemoteAddress != null) {
udp = new ChannelNetworkUdp(metaChannel.udpChannel, metaChannel.udpRemoteAddress, udpServer);
} else {
udp = new ChannelNetwork(metaChannel.udpChannel);
}
} else {
udp = null;
}
if (metaChannel.udtChannel != null) {
udt = new ChannelNetwork(metaChannel.udtChannel);
} else {
udt = null;
}
remoteAddress = ((InetSocketAddress)tcpChannel.remoteAddress()).getAddress().getHostAddress();
remotePublicKeyChanged = metaChannel.changedRemoteKey;
// AES key & IV (only for networked connections)
cryptoAesKeyAndIV = new ParametersWithIV(new KeyParameter(metaChannel.aesKey), metaChannel.aesIV);
// TODO: have to be able to get a NEW IV, so we can rotate keys!
}
/**
* Initialize the connection with any extra info that is needed but was unavailable at the channel construction.
*/
@Override
public final void init() {
// nothing to do.
}
public final boolean remoteKeyChanged() {
return remotePublicKeyChanged;
}
/**
* Flushes the contents of the TCP/UDP/UDT/etc pipes to the actual transport.
*/
@Override
public void flush() {
tcp.flush();
if (udp != null) {
udp.flush();
}
if (udt != null) {
udt.flush();
}
}
@Override
public ParametersWithIV cryptoParameters() {
return cryptoAesKeyAndIV;
}
@Override
public ConnectionPoint tcp() {
return tcp;
}
@Override
public ConnectionPoint udp() {
return udp;
}
@Override
public ConnectionPoint udt() {
return udt;
}
@Override
public PingFuture pingFuture() {
Promise<Integer> newPromise = eventLoop.newPromise();
return new PingFuture(newPromise);
}
@Override
public String getRemoteHost() {
return remoteAddress;
}
@Override
public void close(final Connection connection, final ISessionManager sessionManager) {
long maxShutdownWaitTimeInMilliSeconds = EndPoint.maxShutdownWaitTimeInMilliSeconds;
tcp.close(maxShutdownWaitTimeInMilliSeconds);
if (udp != null) {
udp.close(maxShutdownWaitTimeInMilliSeconds);
}
if (udt != null) {
udt.close(maxShutdownWaitTimeInMilliSeconds);
// we need to yield the thread here, so that the socket has a chance to close
Thread.yield();
}
}
@Override
public String toString() {
return "NetworkConnection [" + getRemoteHost() + "]";
}
@Override
public int id() {
return tcp.id();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!super.equals(obj)) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
ChannelNetworkWrapper other = (ChannelNetworkWrapper) obj;
if (remoteAddress == null) {
if (other.remoteAddress != null) {
return false;
}
} else if (!remoteAddress.equals(other.remoteAddress)) {
return false;
}
return true;
}
@Override
public int hashCode() {
return remoteAddress.hashCode();
}
}

View File

@ -0,0 +1,45 @@
package dorkbox.network.connection.wrapper;
import org.bouncycastle.crypto.params.ParametersWithIV;
import dorkbox.network.connection.Connection;
import dorkbox.network.connection.ConnectionPoint;
import dorkbox.network.connection.ISessionManager;
import dorkbox.network.connection.ping.PingFuture;
public interface ChannelWrapper {
public ConnectionPoint tcp();
public ConnectionPoint udp();
public ConnectionPoint udt();
/**
* Initialize the connection with any extra info that is needed but was unavailable at the channel construction.
*/
public void init();
/**
* Flushes the contents of the TCP/UDP/UDT/etc pipes to the actual transport.
*/
public void flush();
public PingFuture pingFuture();
public ParametersWithIV cryptoParameters();
/**
* @return the remote host (can be local, tcp, udp, udt)
*/
public String getRemoteHost();
public void close(final Connection connection, final ISessionManager sessionManager);
@Override
public String toString();
public int id();
@Override
public boolean equals(Object obj);
}

View File

@ -0,0 +1,22 @@
package dorkbox.network.connection.wrapper;
import java.net.InetSocketAddress;
public class UdpWrapper {
private final Object object;
private final InetSocketAddress remoteAddress;
public UdpWrapper(Object object, InetSocketAddress remoteAddress2) {
this.object = object;
this.remoteAddress = remoteAddress2;
}
public Object object() {
return this.object;
}
public InetSocketAddress remoteAddress() {
return this.remoteAddress;
}
}

View File

@ -0,0 +1,639 @@
package dorkbox.network.pipeline;
import io.netty.buffer.ByteBuf;
import java.io.DataInput;
import java.io.InputStream;
import com.esotericsoftware.kryo.KryoException;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
/**
* An {@link InputStream} which reads data from a {@link ChannelBuffer}.
* <p>
* A read operation against this stream will occur at the {@code readerIndex}
* of its underlying buffer and the {@code readerIndex} will increase during
* the read operation.
* <p>
* This stream implements {@link DataInput} for your convenience.
* The endianness of the stream is not always big endian but depends on
* the endianness of the underlying buffer.
* <p>
* Utility methods are provided for efficiently reading primitive types and strings.
*
* Modified from KRYO to use ByteBuf.
*/
public class ByteBufInput extends Input {
private char[] inputChars = new char[32];
private ByteBuf byteBuf;
private int startIndex;
/** Creates an uninitialized Input. {@link #setBuffer(ByteBuf)} must be called before the Input is used. */
public ByteBufInput () {
}
public ByteBufInput(ByteBuf buffer) {
setBuffer(buffer);
}
public final void setBuffer(ByteBuf byteBuf) {
this.byteBuf = byteBuf;
if (byteBuf != null) {
startIndex = byteBuf.readerIndex(); // where the object starts...
} else {
startIndex = 0;
}
}
public ByteBuf getByteBuf() {
return byteBuf;
}
/** Sets a new buffer. The position and total are reset, discarding any buffered bytes. */
@Override
@Deprecated
public void setBuffer (byte[] bytes) {
throw new RuntimeException("Cannot access this method!");
}
/** Sets a new buffer. The position and total are reset, discarding any buffered bytes. */
@Override
@Deprecated
public void setBuffer (byte[] bytes, int offset, int count) {
throw new RuntimeException("Cannot access this method!");
}
@Override
@Deprecated
public byte[] getBuffer () {
throw new RuntimeException("Cannot access this method!");
}
@Override
@Deprecated
public InputStream getInputStream () {
throw new RuntimeException("Cannot access this method!");
}
/** Sets a new InputStream. The position and total are reset, discarding any buffered bytes.
* @param inputStream May be null. */
@Override
@Deprecated
public void setInputStream (InputStream inputStream) {
throw new RuntimeException("Cannot access this method!");
}
/** Returns the number of bytes read. */
@Override
public long total () {
return byteBuf.readerIndex() - startIndex;
}
/** Returns the current position in the buffer. */
@Override
public int position () {
return byteBuf.readerIndex();
}
/** Sets the current position in the buffer. */
@Override
@Deprecated
public void setPosition (int position) {
throw new RuntimeException("Cannot access this method!");
}
/** Returns the limit for the buffer. */
@Override
public int limit () {
return byteBuf.writerIndex();
}
/** Sets the limit in the buffer. */
@Override
@Deprecated
public void setLimit (int limit) {
throw new RuntimeException("Cannot access this method!");
}
/** Sets the position and total to zero. */
@Override
public void rewind () {
byteBuf.readerIndex(startIndex);
}
/** Discards the specified number of bytes. */
@Override
public void skip (int count) throws KryoException {
byteBuf.skipBytes(count);
}
/** Fills the buffer with more bytes. Can be overridden to fill the bytes from a source other than the InputStream. */
@Override
@Deprecated
protected int fill (byte[] buffer, int offset, int count) throws KryoException {
throw new RuntimeException("Cannot access this method!");
}
// InputStream
/** Reads a single byte as an int from 0 to 255, or -1 if there are no more bytes are available. */
@Override
public int read () throws KryoException {
return byteBuf.readByte() & 0xFF;
}
/** Reads bytes.length bytes or less and writes them to the specified byte[], starting at 0, and returns the number of bytes
* read. */
@Override
public int read (byte[] bytes) throws KryoException {
int start = byteBuf.readerIndex();
byteBuf.readBytes(bytes);
int end = byteBuf.readerIndex();
return end - start; // return how many bytes were actually read.
}
/** Reads count bytes or less and writes them to the specified byte[], starting at offset, and returns the number of bytes read
* or -1 if no more bytes are available. */
@Override
public int read (byte[] bytes, int offset, int count) throws KryoException {
if (bytes == null) {
throw new IllegalArgumentException("bytes cannot be null.");
}
int start = byteBuf.readerIndex();
byteBuf.readBytes(bytes, offset, count);
int end = byteBuf.readerIndex();
return end - start; // return how many bytes were actually read.
}
/** Discards the specified number of bytes. */
@Override
public long skip (long count) throws KryoException {
long remaining = count;
while (remaining > 0) {
int skip = Math.max(Integer.MAX_VALUE, (int)remaining);
skip(skip);
remaining -= skip;
}
return count;
}
/** Closes the underlying InputStream, if any. */
@Override
@Deprecated
public void close () throws KryoException {
// does nothing.
}
// byte
/** Reads a single byte. */
@Override
public byte readByte () throws KryoException {
return byteBuf.readByte();
}
/** Reads a byte as an int from 0 to 255. */
@Override
public int readByteUnsigned () throws KryoException {
return byteBuf.readUnsignedByte();
}
/** Reads the specified number of bytes into a new byte[]. */
@Override
public byte[] readBytes (int length) throws KryoException {
byte[] bytes = new byte[length];
readBytes(bytes, 0, length);
return bytes;
}
/** Reads bytes.length bytes and writes them to the specified byte[], starting at index 0. */
@Override
public void readBytes (byte[] bytes) throws KryoException {
readBytes(bytes, 0, bytes.length);
}
/** Reads count bytes and writes them to the specified byte[], starting at offset. */
@Override
public void readBytes (byte[] bytes, int offset, int count) throws KryoException {
if (bytes == null) {
throw new IllegalArgumentException("bytes cannot be null.");
}
byteBuf.readBytes(bytes, offset, count);
}
// int
/** Reads a 4 byte int. */
@Override
public int readInt () throws KryoException {
return byteBuf.readInt();
}
/** Reads a 1-5 byte int. This stream may consider such a variable length encoding request as a hint. It is not guaranteed that
* a variable length encoding will be really used. The stream may decide to use native-sized integer representation for
* efficiency reasons. **/
@Override
public int readInt (boolean optimizePositive) throws KryoException {
return readVarInt(optimizePositive);
}
/** Reads a 1-5 byte int. It is guaranteed that a varible length encoding will be used. */
@Override
public int readVarInt (boolean optimizePositive) throws KryoException {
ByteBuf buffer = byteBuf;
int b = buffer.readByte();
int result = b & 0x7F;
if ((b & 0x80) != 0) {
b = buffer.readByte();
result |= (b & 0x7F) << 7;
if ((b & 0x80) != 0) {
b = buffer.readByte();
result |= (b & 0x7F) << 14;
if ((b & 0x80) != 0) {
b = buffer.readByte();
result |= (b & 0x7F) << 21;
if ((b & 0x80) != 0) {
b = buffer.readByte();
result |= (b & 0x7F) << 28;
}
}
}
}
return optimizePositive ? result : result >>> 1 ^ -(result & 1);
}
/** Returns true if enough bytes are available to read an int with {@link #readInt(boolean)}. */
@Override
public boolean canReadInt () throws KryoException {
ByteBuf buffer = byteBuf;
int limit = buffer.writerIndex();
if (limit - buffer.readerIndex() >= 5) {
return true;
}
if ((buffer.readByte() & 0x80) == 0) {
return true;
}
if (buffer.readerIndex() == limit) {
return false;
}
if ((buffer.readByte() & 0x80) == 0) {
return true;
}
if (buffer.readerIndex() == limit) {
return false;
}
if ((buffer.readByte() & 0x80) == 0) {
return true;
}
if (buffer.readerIndex() == limit) {
return false;
}
if ((buffer.readByte() & 0x80) == 0) {
return true;
}
if (buffer.readerIndex() == limit) {
return false;
}
return true;
}
/** Returns true if enough bytes are available to read a long with {@link #readLong(boolean)}. */
@Override
public boolean canReadLong () throws KryoException {
ByteBuf buffer = byteBuf;
int limit = buffer.writerIndex();
if (limit - buffer.readerIndex() >= 9) {
return true;
}
if ((buffer.readByte() & 0x80) == 0) {
return true;
}
if (buffer.readerIndex() == limit) {
return false;
}
if ((buffer.readByte() & 0x80) == 0) {
return true;
}
if (buffer.readerIndex() == limit) {
return false;
}
if ((buffer.readByte() & 0x80) == 0) {
return true;
}
if (buffer.readerIndex() == limit) {
return false;
}
if ((buffer.readByte() & 0x80) == 0) {
return true;
}
if (buffer.readerIndex() == limit) {
return false;
}
if ((buffer.readByte() & 0x80) == 0) {
return true;
}
if (buffer.readerIndex() == limit) {
return false;
}
if ((buffer.readByte() & 0x80) == 0) {
return true;
}
if (buffer.readerIndex() == limit) {
return false;
}
if ((buffer.readByte() & 0x80) == 0) {
return true;
}
if (buffer.readerIndex() == limit) {
return false;
}
if ((buffer.readByte() & 0x80) == 0) {
return true;
}
if (buffer.readerIndex() == limit) {
return false;
}
return true;
}
// string
/** Reads the length and string of UTF8 characters, or null. This can read strings written by {@link Output#writeString(String)}
* , {@link Output#writeString(CharSequence)}, and {@link Output#writeAscii(String)}.
* @return May be null. */
@Override
public String readString () {
ByteBuf buffer = byteBuf;
int b = buffer.readByte();
if ((b & 0x80) == 0)
{
return readAscii(); // ASCII.
}
// Null, empty, or UTF8.
int charCount = readUtf8Length(b);
switch (charCount) {
case 0:
return null;
case 1:
return "";
}
charCount--;
if (inputChars.length < charCount) {
inputChars = new char[charCount];
}
readUtf8(charCount);
return new String(inputChars, 0, charCount);
}
private int readUtf8Length (int b) {
int result = b & 0x3F; // Mask all but first 6 bits.
if ((b & 0x40) != 0) { // Bit 7 means another byte, bit 8 means UTF8.
ByteBuf buffer = byteBuf;
b = buffer.readByte();
result |= (b & 0x7F) << 6;
if ((b & 0x80) != 0) {
b = buffer.readByte();
result |= (b & 0x7F) << 13;
if ((b & 0x80) != 0) {
b = buffer.readByte();
result |= (b & 0x7F) << 20;
if ((b & 0x80) != 0) {
b = buffer.readByte();
result |= (b & 0x7F) << 27;
}
}
}
}
return result;
}
private void readUtf8 (int charCount) {
ByteBuf buffer = byteBuf;
char[] chars = this.inputChars; // will be the correct size.
// Try to read 7 bit ASCII chars.
int charIndex = 0;
int count = charCount;
int b;
while (charIndex < count) {
b = buffer.readByte();
if (b < 0) {
buffer.readerIndex(buffer.readerIndex()-1);
break;
}
chars[charIndex++] = (char)b;
}
// If buffer didn't hold all chars or any were not ASCII, use slow path for remainder.
if (charIndex < charCount) {
readUtf8_slow(charCount, charIndex);
}
}
private void readUtf8_slow (int charCount, int charIndex) {
ByteBuf buffer = byteBuf;
char[] chars = this.inputChars;
while (charIndex < charCount) {
int b = buffer.readByte() & 0xFF;
switch (b >> 4) {
case 0:
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
case 7:
chars[charIndex] = (char)b;
break;
case 12:
case 13:
chars[charIndex] = (char)((b & 0x1F) << 6 | buffer.readByte() & 0x3F);
break;
case 14:
chars[charIndex] = (char)((b & 0x0F) << 12 | (buffer.readByte() & 0x3F) << 6 | buffer.readByte() & 0x3F);
break;
}
charIndex++;
}
}
private String readAscii () {
ByteBuf buffer = byteBuf;
int start = buffer.readerIndex() - 1;
int b;
do {
b = buffer.readByte();
} while ((b & 0x80) == 0);
int i = buffer.readerIndex()-1;
buffer.setByte(i, buffer.getByte(i) & 0x7F); // Mask end of ascii bit.
int capp = buffer.readerIndex() - start;
byte[] ba = new byte[capp];
buffer.getBytes(start, ba);
@SuppressWarnings("deprecation")
String value = new String(ba, 0, 0, capp);
buffer.setByte(i, buffer.getByte(i) | 0x80);
return value;
}
/** Reads the length and string of UTF8 characters, or null. This can read strings written by {@link Output#writeString(String)}
* , {@link Output#writeString(CharSequence)}, and {@link Output#writeAscii(String)}.
* @return May be null. */
@Override
public StringBuilder readStringBuilder () {
ByteBuf buffer = byteBuf;
int b = buffer.readByte();
if ((b & 0x80) == 0)
{
return new StringBuilder(readAscii()); // ASCII.
}
// Null, empty, or UTF8.
int charCount = readUtf8Length(b);
switch (charCount) {
case 0:
return null;
case 1:
return new StringBuilder("");
}
charCount--;
if (inputChars.length < charCount) {
inputChars = new char[charCount];
}
readUtf8(charCount);
StringBuilder builder = new StringBuilder(charCount);
builder.append(inputChars, 0, charCount);
return builder;
}
// float
/** Reads a 4 byte float. */
@Override
public float readFloat () throws KryoException {
return Float.intBitsToFloat(readInt());
}
/** Reads a 1-5 byte float with reduced precision. */
@Override
public float readFloat (float precision, boolean optimizePositive) throws KryoException {
return readInt(optimizePositive) / precision;
}
// short
/** Reads a 2 byte short. */
@Override
public short readShort () throws KryoException {
return byteBuf.readShort();
}
/** Reads a 2 byte short as an int from 0 to 65535. */
@Override
public int readShortUnsigned () throws KryoException {
return byteBuf.readUnsignedShort();
}
// long
/** Reads an 8 byte long. */
@Override
public long readLong () throws KryoException {
return byteBuf.readLong();
}
/** Reads a 1-9 byte long. */
@Override
public long readLong (boolean optimizePositive) throws KryoException {
ByteBuf buffer = byteBuf;
int b = buffer.readByte();
long result = b & 0x7F;
if ((b & 0x80) != 0) {
b = buffer.readByte();
result |= (b & 0x7F) << 7;
if ((b & 0x80) != 0) {
b = buffer.readByte();
result |= (b & 0x7F) << 14;
if ((b & 0x80) != 0) {
b = buffer.readByte();
result |= (b & 0x7F) << 21;
if ((b & 0x80) != 0) {
b = buffer.readByte();
result |= (long)(b & 0x7F) << 28;
if ((b & 0x80) != 0) {
b = buffer.readByte();
result |= (long)(b & 0x7F) << 35;
if ((b & 0x80) != 0) {
b = buffer.readByte();
result |= (long)(b & 0x7F) << 42;
if ((b & 0x80) != 0) {
b = buffer.readByte();
result |= (long)(b & 0x7F) << 49;
if ((b & 0x80) != 0) {
b = buffer.readByte();
result |= (long)b << 56;
}
}
}
}
}
}
}
}
if (!optimizePositive) {
result = result >>> 1 ^ -(result & 1);
}
return result;
}
// boolean
/** Reads a 1 byte boolean. */
@Override
public boolean readBoolean () throws KryoException {
return byteBuf.readBoolean();
}
// char
/** Reads a 2 byte char. */
@Override
public char readChar () throws KryoException {
return byteBuf.readChar();
}
// double
/** Reads an 8 bytes double. */
@Override
public double readDouble () throws KryoException {
return Double.longBitsToDouble(readLong());
}
/** Reads a 1-9 byte double with reduced precision. */
@Override
public double readDouble (double precision, boolean optimizePositive) throws KryoException {
return readLong(optimizePositive) / precision;
}
}

View File

@ -0,0 +1,646 @@
package dorkbox.network.pipeline;
import io.netty.buffer.ByteBuf;
import java.io.DataOutput;
import java.io.OutputStream;
import com.esotericsoftware.kryo.KryoException;
import com.esotericsoftware.kryo.io.Output;
/**
* An {@link OutputStream} which writes data to a {@link ChannelBuffer}.
* <p>
* A write operation against this stream will occur at the {@code writerIndex}
* of its underlying buffer and the {@code writerIndex} will increase during
* the write operation.
* <p>
* This stream implements {@link DataOutput} for your convenience.
* The endianness of the stream is not always big endian but depends on
* the endianness of the underlying buffer.
*
* <p>
* Utility methods are provided for efficiently reading primitive types and strings.
*
* Modified from KRYO to use ByteBuf.
*/
public class ByteBufOutput extends Output {
private ByteBuf byteBuf;
private int startIndex;
/** Creates an uninitialized Output. {@link #setBuffer(ByteBuf)} must be called before the Output is used. */
public ByteBufOutput () {
}
public ByteBufOutput(ByteBuf buffer) {
setBuffer(buffer);
}
public final void setBuffer(ByteBuf byteBuf) {
this.byteBuf = byteBuf;
if (byteBuf != null) {
this.byteBuf.readerIndex(0);
startIndex = byteBuf.writerIndex();
} else {
startIndex = 0;
}
}
public ByteBuf getByteBuf() {
return byteBuf;
}
@Override
@Deprecated
public OutputStream getOutputStream () {
throw new RuntimeException("Cannot access this method!");
}
/** Sets a new OutputStream. The position and total are reset, discarding any buffered bytes.
* @param outputStream May be null. */
@Override
@Deprecated
public void setOutputStream (OutputStream outputStream) {
throw new RuntimeException("Cannot access this method!");
}
/** Sets the buffer that will be written to. {@link #setBuffer(byte[], int)} is called with the specified buffer's length as the
* maxBufferSize. */
@Override
@Deprecated
public void setBuffer (byte[] buffer) {
throw new RuntimeException("Cannot access this method!");
}
/** Sets the buffer that will be written to. The position and total are reset, discarding any buffered bytes. The
* {@link #setOutputStream(OutputStream) OutputStream} is set to null.
* @param maxBufferSize The buffer is doubled as needed until it exceeds maxBufferSize and an exception is thrown. */
@Override
@Deprecated
public void setBuffer (byte[] buffer, int maxBufferSize) {
throw new RuntimeException("Cannot access this method!");
}
/** Returns the buffer. The bytes between zero and {@link #position()} are the data that has been written. */
@Override
@Deprecated
public byte[] getBuffer () {
throw new RuntimeException("Cannot access this method!");
}
/** Returns a new byte array containing the bytes currently in the buffer between zero and {@link #position()}. */
@Override
@Deprecated
public byte[] toBytes () {
throw new RuntimeException("Cannot access this method!");
}
/** Returns the current position in the buffer. This is the number of bytes that have not been flushed. */
@Override
public int position () {
return byteBuf.writerIndex();
}
/** Sets the current position in the buffer. */
@Override
@Deprecated
public void setPosition (int position) {
throw new RuntimeException("Cannot access this method!");
}
/** Returns the total number of bytes written. This may include bytes that have not been flushed. */
@Override
public long total () {
return byteBuf.writerIndex() - startIndex;
}
/** Sets the position and total to zero. */
@Override
public void clear () {
byteBuf.readerIndex(0);
byteBuf.writerIndex(startIndex);
}
// OutputStream
/** Writes the buffered bytes to the underlying OutputStream, if any. */
@Override
@Deprecated
public void flush () throws KryoException {
// do nothing...
}
/** Flushes any buffered bytes and closes the underlying OutputStream, if any. */
@Override
@Deprecated
public void close () throws KryoException {
// do nothing...
}
/** Writes a byte. */
@Override
public void write (int value) throws KryoException {
byteBuf.writeByte(value);
}
/** Writes the bytes. Note the byte[] length is not written. */
@Override
public void write (byte[] bytes) throws KryoException {
if (bytes == null) {
throw new IllegalArgumentException("bytes cannot be null.");
}
writeBytes(bytes, 0, bytes.length);
}
/** Writes the bytes. Note the byte[] length is not written. */
@Override
public void write (byte[] bytes, int offset, int length) throws KryoException {
writeBytes(bytes, offset, length);
}
// byte
@Override
public void writeByte (byte value) throws KryoException {
byteBuf.writeByte(value);
}
@Override
public void writeByte (int value) throws KryoException {
byteBuf.writeByte((byte)value);
}
/** Writes the bytes. Note the byte[] length is not written. */
@Override
public void writeBytes (byte[] bytes) throws KryoException {
if (bytes == null) {
throw new IllegalArgumentException("bytes cannot be null.");
}
writeBytes(bytes, 0, bytes.length);
}
/** Writes the bytes. Note the byte[] length is not written. */
@Override
public void writeBytes (byte[] bytes, int offset, int count) throws KryoException {
if (bytes == null) {
throw new IllegalArgumentException("bytes cannot be null.");
}
byteBuf.writeBytes(bytes, offset, count);
}
// int
/** Writes a 4 byte int. */
@Override
public void writeInt (int value) throws KryoException {
byteBuf.writeInt(value);
}
/** Writes a 1-5 byte int. This stream may consider such a variable length encoding request as a hint. It is not guaranteed that
* a variable length encoding will be really used. The stream may decide to use native-sized integer representation for
* efficiency reasons.
*
* @param optimizePositive If true, small positive numbers will be more efficient (1 byte) and small negative numbers will be
* inefficient (5 bytes). */
@Override
public int writeInt (int value, boolean optimizePositive) throws KryoException {
return writeVarInt(value, optimizePositive);
}
/** Writes a 1-5 byte int. It is guaranteed that a varible length encoding will be used.
*
* @param optimizePositive If true, small positive numbers will be more efficient (1 byte) and small negative numbers will be
* inefficient (5 bytes). */
@Override
public int writeVarInt (int value, boolean optimizePositive) throws KryoException {
ByteBuf buffer = byteBuf;
if (!optimizePositive) {
value = value << 1 ^ value >> 31;
}
if (value >>> 7 == 0) {
buffer.writeByte((byte)value);
return 1;
}
if (value >>> 14 == 0) {
buffer.writeByte((byte)(value & 0x7F | 0x80));
buffer.writeByte((byte)(value >>> 7));
return 2;
}
if (value >>> 21 == 0) {
buffer.writeByte((byte)(value & 0x7F | 0x80));
buffer.writeByte((byte)(value >>> 7 | 0x80));
buffer.writeByte((byte)(value >>> 14));
return 3;
}
if (value >>> 28 == 0) {
buffer.writeByte((byte)(value & 0x7F | 0x80));
buffer.writeByte((byte)(value >>> 7 | 0x80));
buffer.writeByte((byte)(value >>> 14 | 0x80));
buffer.writeByte((byte)(value >>> 21));
return 4;
}
buffer.writeByte((byte)(value & 0x7F | 0x80));
buffer.writeByte((byte)(value >>> 7 | 0x80));
buffer.writeByte((byte)(value >>> 14 | 0x80));
buffer.writeByte((byte)(value >>> 21 | 0x80));
buffer.writeByte((byte)(value >>> 28));
return 5;
}
// string
/** Writes the length and string, or null. Short strings are checked and if ASCII they are written more efficiently, else they
* are written as UTF8. If a string is known to be ASCII, {@link #writeAscii(String)} may be used. The string can be read using
* {@link Input#readString()} or {@link Input#readStringBuilder()}.
* @param value May be null. */
@Override
public void writeString (String value) throws KryoException {
if (value == null) {
writeByte(0x80); // 0 means null, bit 8 means UTF8.
return;
}
int charCount = value.length();
if (charCount == 0) {
writeByte(1 | 0x80); // 1 means empty string, bit 8 means UTF8.
return;
}
// Detect ASCII.
boolean ascii = false;
if (charCount > 1 && charCount < 64) { // only snoop 64 chars in
ascii = true;
for (int i = 0; i < charCount; i++) {
int c = value.charAt(i);
if (c > 127) {
ascii = false;
break;
}
}
}
ByteBuf buffer = byteBuf;
if (buffer.writableBytes() < charCount) {
buffer.capacity(buffer.capacity() + charCount + 1);
}
if (!ascii) {
writeUtf8Length(charCount + 1);
}
int charIndex = 0;
// Try to write 8 bit chars.
for (; charIndex < charCount; charIndex++) {
int c = value.charAt(charIndex);
if (c > 127)
{
break; // whoops! detect ascii. have to continue with a slower method!
}
buffer.writeByte((byte)c);
}
if (charIndex < charCount) {
writeString_slow(value, charCount, charIndex);
}
else if (ascii) {
// specify it's ASCII
int i = buffer.writerIndex() - 1;
buffer.setByte(i, buffer.getByte(i) | 0x80); // Bit 8 means end of ASCII.
}
}
/** Writes the length and CharSequence as UTF8, or null. The string can be read using {@link Input#readString()} or
* {@link Input#readStringBuilder()}.
* @param value May be null. */
@Override
public void writeString (CharSequence value) throws KryoException {
if (value == null) {
writeByte(0x80); // 0 means null, bit 8 means UTF8.
return;
}
int charCount = value.length();
if (charCount == 0) {
writeByte(1 | 0x80); // 1 means empty string, bit 8 means UTF8.
return;
}
writeUtf8Length(charCount + 1);
ByteBuf buffer = byteBuf;
if (buffer.writableBytes() < charCount) {
buffer.capacity(buffer.capacity() + charCount + 1);
}
int charIndex = 0;
// Try to write 8 bit chars.
for (; charIndex < charCount; charIndex++) {
int c = value.charAt(charIndex);
if (c > 127)
{
break; // whoops! have to continue with a slower method!
}
buffer.writeByte((byte)c);
}
if (charIndex < charCount) {
writeString_slow(value, charCount, charIndex);
}
}
/** Writes a string that is known to contain only ASCII characters. Non-ASCII strings passed to this method will be corrupted.
* Each byte is a 7 bit character with the remaining byte denoting if another character is available. This is slightly more
* efficient than {@link #writeString(String)}. The string can be read using {@link Input#readString()} or
* {@link Input#readStringBuilder()}.
* @param value May be null. */
@Override
public void writeAscii (String value) throws KryoException {
if (value == null) {
writeByte(0x80); // 0 means null, bit 8 means UTF8.
return;
}
int charCount = value.length();
if (charCount == 0) {
writeByte(1 | 0x80); // 1 means empty string, bit 8 means UTF8.
return;
}
ByteBuf buffer = byteBuf;
if (buffer.writableBytes() < charCount) {
buffer.capacity(buffer.capacity() + charCount + 1);
}
int charIndex = 0;
// Try to write 8 bit chars.
for (; charIndex < charCount; charIndex++) {
int c = value.charAt(charIndex);
buffer.writeByte((byte)c);
}
// specify it's ASCII
int i = buffer.writerIndex() - 1;
buffer.setByte(i, buffer.getByte(i) | 0x80); // Bit 8 means end of ASCII.
}
/** Writes the length of a string, which is a variable length encoded int except the first byte uses bit 8 to denote UTF8 and
* bit 7 to denote if another byte is present. */
private void writeUtf8Length (int value) {
if (value >>> 6 == 0) {
byteBuf.writeByte((byte)(value | 0x80)); // Set bit 8.
} else if (value >>> 13 == 0) {
ByteBuf buffer = byteBuf;
buffer.writeByte((byte)(value | 0x40 | 0x80)); // Set bit 7 and 8.
buffer.writeByte((byte)(value >>> 6));
} else if (value >>> 20 == 0) {
ByteBuf buffer = byteBuf;
buffer.writeByte((byte)(value | 0x40 | 0x80)); // Set bit 7 and 8.
buffer.writeByte((byte)(value >>> 6 | 0x80)); // Set bit 8.
buffer.writeByte((byte)(value >>> 13));
} else if (value >>> 27 == 0) {
ByteBuf buffer = byteBuf;
buffer.writeByte((byte)(value | 0x40 | 0x80)); // Set bit 7 and 8.
buffer.writeByte((byte)(value >>> 6 | 0x80)); // Set bit 8.
buffer.writeByte((byte)(value >>> 13 | 0x80)); // Set bit 8.
buffer.writeByte((byte)(value >>> 20));
} else {
ByteBuf buffer = byteBuf;
buffer.writeByte((byte)(value | 0x40 | 0x80)); // Set bit 7 and 8.
buffer.writeByte((byte)(value >>> 6 | 0x80)); // Set bit 8.
buffer.writeByte((byte)(value >>> 13 | 0x80)); // Set bit 8.
buffer.writeByte((byte)(value >>> 20 | 0x80)); // Set bit 8.
buffer.writeByte((byte)(value >>> 27));
}
}
private void writeString_slow (CharSequence value, int charCount, int charIndex) {
ByteBuf buffer = byteBuf;
for (; charIndex < charCount; charIndex++) {
int c = value.charAt(charIndex);
if (c <= 0x007F) {
buffer.writeByte((byte)c);
} else if (c > 0x07FF) {
buffer.writeByte((byte)(0xE0 | c >> 12 & 0x0F));
buffer.writeByte((byte)(0x80 | c >> 6 & 0x3F));
buffer.writeByte((byte)(0x80 | c & 0x3F));
} else {
buffer.writeByte((byte)(0xC0 | c >> 6 & 0x1F));
buffer.writeByte((byte)(0x80 | c & 0x3F));
}
}
}
// float
/** Writes a 4 byte float. */
@Override
public void writeFloat (float value) throws KryoException {
writeInt(Float.floatToIntBits(value));
}
/** Writes a 1-5 byte float with reduced precision.
* @param optimizePositive If true, small positive numbers will be more efficient (1 byte) and small negative numbers will be
* inefficient (5 bytes). */
@Override
public int writeFloat (float value, float precision, boolean optimizePositive) throws KryoException {
return writeInt((int)(value * precision), optimizePositive);
}
// short
/** Writes a 2 byte short. */
@Override
public void writeShort (int value) throws KryoException {
byteBuf.writeShort(value);
}
// long
/** Writes an 8 byte long. */
@Override
public void writeLong (long value) throws KryoException {
byteBuf.writeLong(value);
}
/** Writes a 1-9 byte long. This stream may consider such a variable length encoding request as a hint. It is not guaranteed
* that a variable length encoding will be really used. The stream may decide to use native-sized integer representation for
* efficiency reasons.
*
* @param optimizePositive If true, small positive numbers will be more efficient (1 byte) and small negative numbers will be
* inefficient (9 bytes). */
@Override
public int writeLong (long value, boolean optimizePositive) throws KryoException {
return writeVarLong(value, optimizePositive);
}
/** Writes a 1-9 byte long. It is guaranteed that a varible length encoding will be used.
* @param optimizePositive If true, small positive numbers will be more efficient (1 byte) and small negative numbers will be
* inefficient (9 bytes). */
@Override
public int writeVarLong (long value, boolean optimizePositive) throws KryoException {
if (!optimizePositive) {
value = value << 1 ^ value >> 63;
}
if (value >>> 7 == 0) {
byteBuf.writeByte((byte)value);
return 1;
}
if (value >>> 14 == 0) {
ByteBuf buffer = byteBuf;
buffer.writeByte((byte)(value & 0x7F | 0x80));
buffer.writeByte((byte)(value >>> 7));
return 2;
}
if (value >>> 21 == 0) {
ByteBuf buffer = byteBuf;
buffer.writeByte((byte)(value & 0x7F | 0x80));
buffer.writeByte((byte)(value >>> 7 | 0x80));
buffer.writeByte((byte)(value >>> 14));
return 3;
}
if (value >>> 28 == 0) {
ByteBuf buffer = byteBuf;
buffer.writeByte((byte)(value & 0x7F | 0x80));
buffer.writeByte((byte)(value >>> 7 | 0x80));
buffer.writeByte((byte)(value >>> 14 | 0x80));
buffer.writeByte((byte)(value >>> 21));
return 4;
}
if (value >>> 35 == 0) {
ByteBuf buffer = byteBuf;
buffer.writeByte((byte)(value & 0x7F | 0x80));
buffer.writeByte((byte)(value >>> 7 | 0x80));
buffer.writeByte((byte)(value >>> 14 | 0x80));
buffer.writeByte((byte)(value >>> 21 | 0x80));
buffer.writeByte((byte)(value >>> 28));
return 5;
}
if (value >>> 42 == 0) {
ByteBuf buffer = byteBuf;
buffer.writeByte((byte)(value & 0x7F | 0x80));
buffer.writeByte((byte)(value >>> 7 | 0x80));
buffer.writeByte((byte)(value >>> 14 | 0x80));
buffer.writeByte((byte)(value >>> 21 | 0x80));
buffer.writeByte((byte)(value >>> 28 | 0x80));
buffer.writeByte((byte)(value >>> 35));
return 6;
}
if (value >>> 49 == 0) {
ByteBuf buffer = byteBuf;
buffer.writeByte((byte)(value & 0x7F | 0x80));
buffer.writeByte((byte)(value >>> 7 | 0x80));
buffer.writeByte((byte)(value >>> 14 | 0x80));
buffer.writeByte((byte)(value >>> 21 | 0x80));
buffer.writeByte((byte)(value >>> 28 | 0x80));
buffer.writeByte((byte)(value >>> 35 | 0x80));
buffer.writeByte((byte)(value >>> 42));
return 7;
}
if (value >>> 56 == 0) {
ByteBuf buffer = byteBuf;
buffer.writeByte((byte)(value & 0x7F | 0x80));
buffer.writeByte((byte)(value >>> 7 | 0x80));
buffer.writeByte((byte)(value >>> 14 | 0x80));
buffer.writeByte((byte)(value >>> 21 | 0x80));
buffer.writeByte((byte)(value >>> 28 | 0x80));
buffer.writeByte((byte)(value >>> 35 | 0x80));
buffer.writeByte((byte)(value >>> 42 | 0x80));
buffer.writeByte((byte)(value >>> 49));
return 8;
}
ByteBuf buffer = byteBuf;
buffer.writeByte((byte)(value & 0x7F | 0x80));
buffer.writeByte((byte)(value >>> 7 | 0x80));
buffer.writeByte((byte)(value >>> 14 | 0x80));
buffer.writeByte((byte)(value >>> 21 | 0x80));
buffer.writeByte((byte)(value >>> 28 | 0x80));
buffer.writeByte((byte)(value >>> 35 | 0x80));
buffer.writeByte((byte)(value >>> 42 | 0x80));
buffer.writeByte((byte)(value >>> 49 | 0x80));
buffer.writeByte((byte)(value >>> 56));
return 9;
}
// boolean
/** Writes a 1 byte boolean. */
@Override
public void writeBoolean (boolean value) throws KryoException {
byteBuf.writeBoolean(value);
}
// char
/** Writes a 2 byte char. */
@Override
public void writeChar (char value) throws KryoException {
byteBuf.writeChar(value);
}
// double
/** Writes an 8 byte double. */
@Override
public void writeDouble (double value) throws KryoException {
writeLong(Double.doubleToLongBits(value));
}
/** Writes a 1-9 byte double with reduced precision.
* @param optimizePositive If true, small positive numbers will be more efficient (1 byte) and small negative numbers will be
* inefficient (9 bytes). */
@Override
public int writeDouble (double value, double precision, boolean optimizePositive) throws KryoException {
return writeLong((long)(value * precision), optimizePositive);
}
/** Returns the number of bytes that would be written with {@link #writeInt(int, boolean)}. */
static public int intLength (int value, boolean optimizePositive) {
if (!optimizePositive) {
value = value << 1 ^ value >> 31;
}
if (value >>> 7 == 0) {
return 1;
}
if (value >>> 14 == 0) {
return 2;
}
if (value >>> 21 == 0) {
return 3;
}
if (value >>> 28 == 0) {
return 4;
}
return 5;
}
/** Returns the number of bytes that would be written with {@link #writeLong(long, boolean)}. */
static public int longLength (long value, boolean optimizePositive) {
if (!optimizePositive) {
value = value << 1 ^ value >> 63;
}
if (value >>> 7 == 0) {
return 1;
}
if (value >>> 14 == 0) {
return 2;
}
if (value >>> 21 == 0) {
return 3;
}
if (value >>> 28 == 0) {
return 4;
}
if (value >>> 35 == 0) {
return 5;
}
if (value >>> 42 == 0) {
return 6;
}
if (value >>> 49 == 0) {
return 7;
}
if (value >>> 56 == 0) {
return 8;
}
return 9;
}
}

View File

@ -0,0 +1,144 @@
package dorkbox.network.pipeline;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import java.util.List;
import dorkbox.network.util.SerializationManager;
import dorkbox.util.bytes.OptimizeUtilsByteBuf;
public class KryoDecoder extends ByteToMessageDecoder {
private final OptimizeUtilsByteBuf optimize;
private final SerializationManager kryoWrapper;
public KryoDecoder(SerializationManager kryoWrapper) {
super();
this.kryoWrapper = kryoWrapper;
optimize = OptimizeUtilsByteBuf.get();
}
protected Object readObject(SerializationManager kryoWrapper, ChannelHandlerContext ctx, ByteBuf in, int length) {
// no connection here because we haven't created one yet. When we do, we replace this handler with a new one.
return kryoWrapper.read(in, length);
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
OptimizeUtilsByteBuf optimize = this.optimize;
// Make sure if the length field was received,
// and read the length of the next object from the socket.
int lengthLength = optimize.canReadInt(in);
int readableBytes = in.readableBytes(); // full length of available bytes.
if (lengthLength == 0 || readableBytes < 2 || readableBytes < lengthLength) {
// The length field was not fully received - do nothing (wait for more...)
// This method will be invoked again when more packets are
// received and appended to the buffer.
return;
}
// The length field is in the buffer.
// save the writerIndex for local access
int writerIndex = in.writerIndex();
// Mark the current buffer position before reading the length fields
// because the whole frame might not be in the buffer yet.
// We will reset the buffer position to the marked position if
// there's not enough bytes in the buffer.
in.markReaderIndex();
// Read the length field.
int length = optimize.readInt(in, true);
readableBytes = in.readableBytes(); // have to adjust readable bytes, since we just read an int off the buffer.
if (length == 0) {
ctx.fireExceptionCaught(new IllegalStateException("Kryo DecoderTCP had a read length of 0"));
return;
}
// we can't test against a single "max size", since objects can back-up on the buffer.
// we must ABSOLUTELY follow a "max size" rule when encoding objects, however.
// Make sure if there's enough bytes in the buffer.
if (length > readableBytes) {
// The whole bytes were not received yet - return null.
// This method will be invoked again when more packets are
// received and appended to the buffer.
// Reset to the marked position to read the length field again
// next time.
in.resetReaderIndex();
// wait for the rest of the object to come in.
return;
}
// how many objects are on this buffer?
else if (readableBytes > length) {
// more than one!
// read the object off of the buffer. (save parts of the buffer so if it is too big, we can go back to it, and use it later on...)
// we know we have at least one object
int objectCount = 1;
int endOfObjectPosition = in.readerIndex() + length;
// set us up for the next object.
in.readerIndex(endOfObjectPosition);
// how many more objects?? The first time, it can be off, because we already KNOW it's > 0.
// (That's how we got here to begin with)
while (readableBytes > 0) {
objectCount++;
if (optimize.canReadInt(in) > 0) {
length = optimize.readInt(in, true);
if (length <= 0) {
// throw new IllegalStateException("Kryo DecoderTCP had a read length of 0");
objectCount--;
break;
}
endOfObjectPosition = in.readerIndex() + length;
// position the reader to look for the NEXT object
if (endOfObjectPosition <= writerIndex) {
in.readerIndex(endOfObjectPosition);
readableBytes = in.readableBytes();
} else {
objectCount--;
break;
}
} else {
objectCount--;
break;
}
}
// readerIndex is currently at the MAX place it can read data off the buffer.
// reset it to the spot BEFORE we started reading data from the buffer.
in.resetReaderIndex();
// System.err.println("Found " + objectCount + " objects in this buffer.");
// NOW add each one of the NEW objects to the array!
for (int i=0;i<objectCount;i++) {
length = optimize.readInt(in, true); // object LENGTH
// however many we need to
out.add(readObject(kryoWrapper, ctx, in, length));
}
// the buffer reader index will be at the correct location, since the read object method advances it.
} else {
// exactly one!
out.add(readObject(kryoWrapper, ctx, in, length));
}
}
}

View File

@ -0,0 +1,29 @@
package dorkbox.network.pipeline;
import dorkbox.network.connection.Connection;
import dorkbox.network.util.NetException;
import dorkbox.network.util.SerializationManager;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
// on client this is MessageToMessage *because of the UdpDecoder in the pipeline!)
public class KryoDecoderCrypto extends KryoDecoder {
public KryoDecoderCrypto(SerializationManager kryoWrapper) {
super(kryoWrapper);
}
@Override
protected Object readObject(SerializationManager kryoWrapper, ChannelHandlerContext ctx, ByteBuf in, int length) {
ChannelHandler last = ctx.pipeline().last();
if (last instanceof Connection) {
return kryoWrapper.readWithCryptoTcp((Connection) last, in, length);
} else {
// SHOULD NEVER HAPPEN!
throw new NetException("Tried to use kryo to READ an object with NO network connection!");
}
}
}

View File

@ -0,0 +1,68 @@
package dorkbox.network.pipeline;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import com.esotericsoftware.kryo.KryoException;
import dorkbox.network.util.NetException;
import dorkbox.network.util.SerializationManager;
import dorkbox.util.bytes.OptimizeUtilsByteBuf;
@Sharable
public class KryoEncoder extends MessageToByteEncoder<Object> {
private static final int reservedLengthIndex = 4;
private final SerializationManager kryoWrapper;
private final OptimizeUtilsByteBuf optimize;
public KryoEncoder(SerializationManager kryoWrapper) {
super();
this.kryoWrapper = kryoWrapper;
optimize = OptimizeUtilsByteBuf.get();
}
// the crypto writer will override this
protected void writeObject(SerializationManager kryoWrapper, ChannelHandlerContext ctx, Object msg, ByteBuf buffer) {
// no connection here because we haven't created one yet. When we do, we replace this handler with a new one.
kryoWrapper.write(buffer, msg);
}
@Override
protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception {
// we don't necessarily start at 0!!
int startIndex = out.writerIndex();
if (msg != null) {
// Write data. START at index = 4. This is to make room for the integer placed by the frameEncoder for TCP.
// DOES NOT SUPPORT NEGATIVE NUMBERS!
out.writeInt(0); // put an int in, which is the same size as reservedLengthIndex
try {
writeObject(kryoWrapper, ctx, msg, out);
// now set the frame (if it's TCP)!
int length = out.readableBytes() - startIndex - reservedLengthIndex; // (reservedLengthLength) 4 is the reserved space for the integer.
// specify the header.
OptimizeUtilsByteBuf optimize = this.optimize;
int lengthOfTheLength = optimize.intLength(length, true);
// 4 was the position specified by the kryoEncoder. It was to make room for the integer. DOES NOT SUPPORT NEGATIVE NUMBERS!
int newIndex = startIndex+reservedLengthIndex-lengthOfTheLength;
int oldIndex = out.writerIndex();
out.writerIndex(newIndex);
// do the optimized length thing!
optimize.writeInt(out, length, true);
out.setIndex(newIndex, oldIndex);
} catch (KryoException ex) {
ctx.fireExceptionCaught(new NetException("Unable to serialize object of type: " + msg.getClass().getName(), ex));
}
}
}
}

View File

@ -0,0 +1,29 @@
package dorkbox.network.pipeline;
import dorkbox.network.connection.Connection;
import dorkbox.network.util.NetException;
import dorkbox.network.util.SerializationManager;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
@Sharable
public class KryoEncoderCrypto extends KryoEncoder {
public KryoEncoderCrypto(SerializationManager kryoWrapper) {
super(kryoWrapper);
}
@Override
protected void writeObject(SerializationManager kryoWrapper, ChannelHandlerContext ctx, Object msg, ByteBuf buffer) {
ChannelHandler last = ctx.pipeline().last();
if (last instanceof Connection) {
kryoWrapper.writeWithCryptoTcp((Connection) last, buffer, msg);
} else {
// SHOULD NEVER HAPPEN!
throw new NetException("Tried to use kryo to WRITE an object with NO network connection (or wrong connection type!)!");
}
}
}

View File

@ -0,0 +1,33 @@
package dorkbox.network.pipeline.discovery;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.socket.DatagramPacket;
import io.netty.util.AttributeKey;
import java.net.InetSocketAddress;
import dorkbox.network.Broadcast;
public class ClientDiscoverHostHandler extends SimpleChannelInboundHandler<DatagramPacket> {
// This uses CHANNEL LOCAL to save the data.
public static final AttributeKey<InetSocketAddress> STATE = AttributeKey.valueOf(ClientDiscoverHostHandler.class, "Discover.state");
@Override
protected void channelRead0(ChannelHandlerContext context, DatagramPacket message) throws Exception {
ByteBuf data = message.content();
if (data.readableBytes() == 1 && data.readByte() == Broadcast.broadcastResponseID) {
context.channel().attr(STATE).set(message.sender());
context.channel().close();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext context, Throwable cause) throws Exception {
context.channel().close();
}
}

View File

@ -0,0 +1,23 @@
package dorkbox.network.pipeline.discovery;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.nio.NioDatagramChannel;
/**
* Creates a newly configured {@link ChannelPipeline} for a new channel.
*/
public class ClientDiscoverHostInitializer extends ChannelInitializer<NioDatagramChannel> {
private ClientDiscoverHostHandler clientDiscoverHostHandler;
public ClientDiscoverHostInitializer() {
clientDiscoverHostHandler = new ClientDiscoverHostHandler();
}
@Override
public void initChannel(NioDatagramChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("discoverHostHandler", clientDiscoverHostHandler);
}
}

View File

@ -0,0 +1,42 @@
package dorkbox.network.pipeline.udp;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.socket.DatagramPacket;
import io.netty.handler.codec.MessageToMessageDecoder;
import java.util.List;
import dorkbox.network.util.NetException;
import dorkbox.network.util.SerializationManager;
@Sharable
public class KryoDecoderUdp extends MessageToMessageDecoder<DatagramPacket> {
private final SerializationManager kryoWrapper;
public KryoDecoderUdp(SerializationManager kryoWrapper) {
this.kryoWrapper = kryoWrapper;
}
@Override
protected void decode(ChannelHandlerContext ctx, DatagramPacket msg, List<Object> out) throws Exception {
if (msg != null) {
ByteBuf data = msg.content();
if (data != null) {
// there is a REMOTE possibility that UDP traffic BEAT the TCP registration traffic, which means that THIS packet
// COULD be encrypted!
if (kryoWrapper.isEncrypted(data)) {
throw new NetException("Encrypted UDP packet recieved before registration complete. WHOOPS!");
}
// no connection here because we haven't created one yet. When we do, we replace this handler with a new one.
Object read = kryoWrapper.read(data, data.writerIndex());
out.add(read);
}
}
}
}

View File

@ -0,0 +1,37 @@
package dorkbox.network.pipeline.udp;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.socket.DatagramPacket;
import io.netty.handler.codec.MessageToMessageDecoder;
import java.util.List;
import dorkbox.network.connection.Connection;
import dorkbox.network.util.NetException;
import dorkbox.network.util.SerializationManager;
@Sharable
public class KryoDecoderUdpCrypto extends MessageToMessageDecoder<DatagramPacket> {
private final SerializationManager kryoWrapper;
public KryoDecoderUdpCrypto(SerializationManager kryoWrapper) {
this.kryoWrapper = kryoWrapper;
}
@Override
public void decode(ChannelHandlerContext ctx, DatagramPacket in, List<Object> out) throws Exception {
ChannelHandler last = ctx.pipeline().last();
if (last instanceof Connection) {
ByteBuf data = in.content();
Object object = kryoWrapper.readWithCryptoUdp((Connection) last, data, data.readableBytes());
out.add(object);
} else {
// SHOULD NEVER HAPPEN!
throw new NetException("Tried to use kryo to READ an object with NO network connection!");
}
}
}

View File

@ -0,0 +1,62 @@
package dorkbox.network.pipeline.udp;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.socket.DatagramPacket;
import io.netty.handler.codec.MessageToMessageEncoder;
import java.net.InetSocketAddress;
import java.util.List;
import com.esotericsoftware.kryo.KryoException;
import dorkbox.network.connection.EndPoint;
import dorkbox.network.util.NetException;
import dorkbox.network.util.SerializationManager;
@Sharable
// UDP uses messages --- NOT bytebuf!
// ONLY USED BY THE CLIENT (the server has it's own handler!)
public class KryoEncoderUdp extends MessageToMessageEncoder<Object> {
private final static int maxSize = EndPoint.udpMaxSize;
private SerializationManager kryoWrapper;
public KryoEncoderUdp(SerializationManager kryoWrapper) {
super();
this.kryoWrapper = kryoWrapper;
}
// the crypto writer will override this
protected void writeObject(SerializationManager kryoWrapper, ChannelHandlerContext ctx, Object msg, ByteBuf buffer) {
// no connection here because we haven't created one yet. When we do, we replace this handler with a new one.
kryoWrapper.write(buffer, msg);
}
@Override
protected void encode(ChannelHandlerContext ctx, Object msg, List<Object> out) throws Exception {
if (msg != null) {
try {
ByteBuf outBuffer = Unpooled.buffer(maxSize);
// no size info, since this is UDP, it is not segmented
writeObject(kryoWrapper, ctx, msg, outBuffer);
// have to check to see if we are too big for UDP!
if (outBuffer.readableBytes() > EndPoint.udpMaxSize) {
System.err.println("Object larger than MAX udp size! " + EndPoint.udpMaxSize + "/" + outBuffer.readableBytes());
throw new NetException("Object is TOO BIG FOR UDP! " + msg.toString() + " (" + EndPoint.udpMaxSize + "/" + outBuffer.readableBytes() + ")");
}
DatagramPacket packet = new DatagramPacket(outBuffer, (InetSocketAddress) ctx.channel().remoteAddress());
out.add(packet);
} catch (KryoException ex) {
throw new NetException("Unable to serialize object of type: " + msg.getClass().getName(), ex);
}
}
}
}

View File

@ -0,0 +1,29 @@
package dorkbox.network.pipeline.udp;
import dorkbox.network.connection.Connection;
import dorkbox.network.util.NetException;
import dorkbox.network.util.SerializationManager;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
@Sharable
public class KryoEncoderUdpCrypto extends KryoEncoderUdp {
public KryoEncoderUdpCrypto(SerializationManager kryoWrapper) {
super(kryoWrapper);
}
@Override
protected void writeObject(SerializationManager kryoWrapper, ChannelHandlerContext ctx, Object msg, ByteBuf buffer) {
ChannelHandler last = ctx.pipeline().last();
if (last instanceof Connection) {
kryoWrapper.writeWithCryptoUdp((Connection) last, buffer, msg);
} else {
// SHOULD NEVER HAPPEN!
throw new NetException("Tried to use kryo to WRITE an object with NO network connection!");
}
}
}

View File

@ -0,0 +1,12 @@
package dorkbox.network.rmi;
import java.lang.reflect.Method;
import com.esotericsoftware.kryo.Serializer;
class CachedMethod {
Method method;
@SuppressWarnings("rawtypes")
Serializer[] serializers;
}

View File

@ -0,0 +1,82 @@
package dorkbox.network.rmi;
import java.lang.reflect.Method;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.KryoException;
import com.esotericsoftware.kryo.KryoSerializable;
import com.esotericsoftware.kryo.Serializer;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
/** Internal message to invoke methods remotely. */
class InvokeMethod implements KryoSerializable, RmiMessages {
public int objectID;
public Method method;
public Object[] args;
// The top two bytes of the ID indicate if the remote invocation should respond with return values and exceptions,
// respectively. The rest is a six bit counter. This means up to 63 responses can be stored before undefined behavior
// occurs due to possible duplicate IDs.
public byte responseID;
@Override
@SuppressWarnings("rawtypes")
public void write (Kryo kryo, Output output) {
output.writeInt(objectID, true);
int methodClassID = kryo.getRegistration(method.getDeclaringClass()).getId();
output.writeInt(methodClassID, true);
CachedMethod[] cachedMethods = RmiBridge.getMethods(kryo, method.getDeclaringClass());
CachedMethod cachedMethod = null;
for (int i = 0, n = cachedMethods.length; i < n; i++) {
cachedMethod = cachedMethods[i];
if (cachedMethod.method.equals(method)) {
output.writeByte(i);
break;
}
}
if (cachedMethod == null) {
throw new KryoException("Cached method was null for class: " + method.getDeclaringClass().getName());
}
for (int i = 0, n = cachedMethod.serializers.length; i < n; i++) {
Serializer serializer = cachedMethod.serializers[i];
if (serializer != null)
kryo.writeObjectOrNull(output, args[i], serializer);
else
kryo.writeClassAndObject(output, args[i]);
}
output.writeByte(responseID);
}
@Override
public void read (Kryo kryo, Input input) {
objectID = input.readInt(true);
int methodClassID = input.readInt(true);
Class<?> methodClass = kryo.getRegistration(methodClassID).getType();
byte methodIndex = input.readByte();
CachedMethod cachedMethod;
try {
cachedMethod = RmiBridge.getMethods(kryo, methodClass)[methodIndex];
} catch (IndexOutOfBoundsException ex) {
throw new KryoException("Invalid method index " + methodIndex + " for class: " + methodClass.getName());
}
method = cachedMethod.method;
args = new Object[cachedMethod.serializers.length];
for (int i = 0, n = args.length; i < n; i++) {
Serializer<?> serializer = cachedMethod.serializers[i];
if (serializer != null)
args[i] = kryo.readObjectOrNull(input, method.getParameterTypes()[i], serializer);
else
args[i] = kryo.readClassAndObject(input);
}
responseID = input.readByte();
}
}

View File

@ -0,0 +1,10 @@
package dorkbox.network.rmi;
/**
* Internal message to return the result of a remotely invoked method.
*/
public class InvokeMethodResult implements RmiMessages {
public int objectID;
public byte responseID;
public Object result;
}

View File

@ -0,0 +1,302 @@
package dorkbox.network.rmi;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import dorkbox.network.connection.Connection;
import dorkbox.network.connection.Listener;
/** Handles network communication when methods are invoked on a proxy. */
class RemoteInvocationHandler implements InvocationHandler {
private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(RemoteInvocationHandler.class);
private final Connection connection;
final int objectID;
private int timeoutMillis = 3000;
private boolean nonBlocking = false;
private boolean transmitReturnValue = true;
private boolean transmitExceptions = true;
private Byte lastResponseID;
private byte nextResponseNum = 1;
private Listener<Connection, InvokeMethodResult> responseListener;
final ReentrantLock lock = new ReentrantLock();
final Condition responseCondition = lock.newCondition();
final ConcurrentHashMap<Byte, InvokeMethodResult> responseTable = new ConcurrentHashMap<Byte, InvokeMethodResult>();
public RemoteInvocationHandler(Connection connection, final int objectID) {
super();
this.connection = connection;
this.objectID = objectID;
responseListener = new Listener<Connection, InvokeMethodResult>() {
@Override
public void received (Connection connection, InvokeMethodResult invokeMethodResult) {
byte responseID = invokeMethodResult.responseID;
if (invokeMethodResult.objectID != objectID) {
// System.err.println("FAILED: " + responseID);
// logger.trace("{} FAILED to received data: {} with id ({})", connection, invokeMethodResult.result, invokeMethodResult.responseID);
return;
}
// System.err.println("Recieved: " + responseID);
// logger.trace("{} received data: {} with id ({})", connection, invokeMethodResult.result, invokeMethodResult.responseID);
responseTable.put(responseID, invokeMethodResult);
// System.err.println("L");
lock.lock();
try {
responseCondition.signalAll();
} finally {
lock.unlock();
// System.err.println("U");
}
}
@Override
public void disconnected(Connection connection) {
close();
}
};
connection.listeners().add(responseListener);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (this.connection == null ? 0 : this.connection.hashCode());
result = prime * result + (this.lastResponseID == null ? 0 : this.lastResponseID.hashCode());
result = prime * result + this.objectID;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
RemoteInvocationHandler other = (RemoteInvocationHandler) obj;
if (this.connection == null) {
if (other.connection != null) {
return false;
}
} else if (!this.connection.equals(other.connection)) {
return false;
}
if (this.lastResponseID == null) {
if (other.lastResponseID != null) {
return false;
}
} else if (!this.lastResponseID.equals(other.lastResponseID)) {
return false;
}
if (this.objectID != other.objectID) {
return false;
}
return true;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Exception {
if (method.getDeclaringClass() == RemoteObject.class) {
String name = method.getName();
if (name.equals("close")) {
close();
return null;
} else if (name.equals("setResponseTimeout")) {
timeoutMillis = (Integer)args[0];
return null;
} else if (name.equals("setNonBlocking")) {
nonBlocking = (Boolean)args[0];
return null;
} else if (name.equals("setTransmitReturnValue")) {
transmitReturnValue = (Boolean)args[0];
return null;
} else if (name.equals("setTransmitExceptions")) {
transmitExceptions = (Boolean)args[0];
return null;
} else if (name.equals("waitForLastResponse")) {
if (lastResponseID == null) {
throw new IllegalStateException("There is no last response to wait for.");
}
return waitForResponse(lastResponseID);
} else if (name.equals("getLastResponseID")) {
if (lastResponseID == null) {
throw new IllegalStateException("There is no last response ID.");
}
return lastResponseID;
} else if (name.equals("waitForResponse")) {
if (!transmitReturnValue && !transmitExceptions && nonBlocking) {
throw new IllegalStateException("This RemoteObject is currently set to ignore all responses.");
}
return waitForResponse((Byte)args[0]);
} else if (name.equals("getConnection")) {
return connection;
} else {
// Should never happen, for debugging purposes only
throw new RuntimeException("Invocation handler could not find RemoteObject method. Check ObjectSpace.java");
}
} else if (method.getDeclaringClass() == Object.class) {
if (method.getName().equals("toString")) {
return "<proxy>";
}
try {
return method.invoke(proxy, args);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
InvokeMethod invokeMethod = new InvokeMethod();
invokeMethod.objectID = objectID;
invokeMethod.method = method;
invokeMethod.args = args;
// The only time a invocation doesn't need a response is if it's async
// and no return values or exceptions are wanted back.
boolean needsResponse = transmitReturnValue || transmitExceptions || !nonBlocking;
if (needsResponse) {
byte responseID;
synchronized (this) {
// Increment the response counter and put it into the first six bits of the responseID byte
responseID = nextResponseNum++;
if (nextResponseNum == 64) {
nextResponseNum = 1; // Keep number under 2^6, avoid 0 (see else statement below)
}
}
// Pack return value and exception info into the top two bits
if (transmitReturnValue) {
responseID |= RmiBridge.kReturnValMask;
}
if (transmitExceptions) {
responseID |= RmiBridge.kReturnExMask;
}
invokeMethod.responseID = responseID;
} else {
invokeMethod.responseID = 0; // A response info of 0 means to not respond
}
connection.send().TCP(invokeMethod).flush();
if (logger.isDebugEnabled()) {
String argString = "";
if (args != null) {
argString = Arrays.deepToString(args);
argString = argString.substring(1, argString.length() - 1);
}
logger.debug(connection + " sent: " + method.getDeclaringClass().getSimpleName() +
"#" + method.getName() + "(" + argString + ")");
}
if (invokeMethod.responseID != 0) {
lastResponseID = invokeMethod.responseID;
}
if (nonBlocking) {
Class<?> returnType = method.getReturnType();
if (returnType.isPrimitive()) {
if (returnType == int.class) {
return 0;
}
if (returnType == boolean.class) {
return Boolean.FALSE;
}
if (returnType == float.class) {
return 0f;
}
if (returnType == char.class) {
return (char)0;
}
if (returnType == long.class) {
return 0l;
}
if (returnType == short.class) {
return (short)0;
}
if (returnType == byte.class) {
return (byte)0;
}
if (returnType == double.class) {
return 0d;
}
}
return null;
}
try {
Object result = waitForResponse(invokeMethod.responseID);
if (result != null && result instanceof Exception) {
throw (Exception)result;
} else {
return result;
}
} catch (TimeoutException ex) {
throw new TimeoutException("Response timed out: " + method.getDeclaringClass().getName() + "." + method.getName());
}
}
private Object waitForResponse(byte responseID) {
long endTime = System.currentTimeMillis() + timeoutMillis;
long remaining = timeoutMillis;
while (remaining > 0) {
// System.err.println("Waiting for: " + responseID);
if (responseTable.containsKey(responseID)) {
InvokeMethodResult invokeMethodResult = responseTable.get(responseID);
responseTable.remove(responseID);
lastResponseID = null;
return invokeMethodResult.result;
}
else {
// System.err.println("LL");
lock.lock();
try {
responseCondition.await(remaining, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
} finally {
lock.unlock();
// System.err.println("UU");
}
}
remaining = endTime - System.currentTimeMillis();
}
// only get here if we timeout
throw new TimeoutException("Response timed out.");
}
void close() {
connection.listeners().remove(responseListener);
}
}

View File

@ -0,0 +1,59 @@
package dorkbox.network.rmi;
import dorkbox.network.connection.Connection;
/** Provides access to various settings on a remote object.
* @see RmiBridge#getRemoteObject(dorkbox.networking.connection.interfaces.IConnection.Connection, int, Class...)
* @author Nathan Sweet <misc@n4te.com> */
public interface RemoteObject {
/** Sets the milliseconds to wait for a method to return value. Default is 3000. */
public void setResponseTimeout (int timeoutMillis);
/** Sets the blocking behavior when invoking a remote method. Default is false.
* @param nonBlocking If false, the invoking thread will wait for the remote method to return or timeout (default). If true,
* the invoking thread will not wait for a response. The method will return immediately and the return value should
* be ignored. If they are being transmitted, the return value or any thrown exception can later be retrieved with
* {@link #waitForLastResponse()} or {@link #waitForResponse(byte)}. The responses will be stored until retrieved, so
* each method call should have a matching retrieve. */
public void setNonBlocking (boolean nonBlocking);
/** Sets whether return values are sent back when invoking a remote method. Default is true.
* @param transmit If true, then the return value for non-blocking method invocations can be retrieved with
* {@link #waitForLastResponse()} or {@link #waitForResponse(byte)}. If false, then non-primitive return values for
* remote method invocations are not sent by the remote side of the connection and the response can never be
* retrieved. This can also be used to save bandwidth if you will not check the return value of a blocking remote
* invocation. Note that an exception could still be returned by {@link #waitForLastResponse()} or
* {@link #waitForResponse(byte)} if {@link #setTransmitExceptions(boolean)} is true. */
public void setTransmitReturnValue (boolean transmit);
/** Sets whether exceptions are sent back when invoking a remote method. Default is true.
* @param transmit If false, exceptions will be unhandled and rethrown as RuntimeExceptions inside the invoking thread. This is
* the legacy behavior. If true, behavior is dependent on whether {@link #setNonBlocking(boolean)}. If non-blocking
* is true, the exception will be serialized and sent back to the call site of the remotely invoked method, where it
* will be re-thrown. If non-blocking is false, an exception will not be thrown in the calling thread but instead can
* be retrieved with {@link #waitForLastResponse()} or {@link #waitForResponse(byte)}, similar to a return value. */
public void setTransmitExceptions (boolean transmit);
/** Waits for the response to the last method invocation to be received or the response timeout to be reached. Must not be
* called from the connection's update thread.
* @see RmiBridge#getRemoteObject(dorkbox.networking.connection.interfaces.IConnection.Connection, int, Class...) */
public Object waitForLastResponse ();
/** Gets the ID of response for the last method invocation. */
public byte getLastResponseID ();
/** Waits for the specified method invocation response to be received or the response timeout to be reached. Must not be called
* from the connection's update thread. Response IDs use a six bit identifier, with one identifier reserved for "no response".
* This means that this method should be called to get the result for a non-blocking call before an additional 63 non-blocking
* calls are made, or risk undefined behavior due to identical IDs.
* @see RmiBridge#getRemoteObject(dorkbox.networking.connection.interfaces.IConnection.Connection, int, Class...) */
public Object waitForResponse (byte responseID);
/** Causes this RemoteObject to stop listening to the connection for method invocation response messages. */
public void close ();
/** Returns the local connection for this remote object. */
public Connection getConnection ();
}

View File

@ -0,0 +1,38 @@
package dorkbox.network.rmi;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.Serializer;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import com.sun.xml.internal.ws.encoding.soap.SerializationException;
import dorkbox.network.connection.Connection;
/**
* Serializes an object registered with the RmiBridge so the receiving side
* gets a {@link RemoteObject} proxy rather than the bytes for the serialized
* object.
*
* @author Nathan Sweet <misc@n4te.com>
*/
public class RemoteObjectSerializer<T> extends Serializer<T> {
@Override
public void write(Kryo kryo, Output output, T object) {
@SuppressWarnings("unchecked")
Connection connection = (Connection) kryo.getContext().get(Connection.connection);
int id = RmiBridge.getRegisteredId(connection, object);
if (id == Integer.MAX_VALUE) {
throw new SerializationException("Object not found in an ObjectSpace: " + object);
}
output.writeInt(id, true);
}
@SuppressWarnings({"rawtypes","unchecked"})
@Override
public T read(Kryo kryo, Input input, Class type) {
int objectID = input.readInt(true);
Connection connection = (Connection) kryo.getContext().get(Connection.connection);
return (T) RmiBridge.getRemoteObject(connection, objectID, type);
}
}

View File

@ -0,0 +1,573 @@
package dorkbox.network.rmi;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.PriorityQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.Serializer;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import com.esotericsoftware.kryo.serializers.FieldSerializer;
import com.esotericsoftware.kryo.util.IntMap;
import dorkbox.network.connection.Connection;
import dorkbox.network.connection.EndPoint;
import dorkbox.network.connection.Listener;
import dorkbox.network.util.ObjectIntMap;
import dorkbox.network.util.SerializationManager;
/**
* Allows methods on objects to be invoked remotely over TCP. Objects are
* {@link #register(int, Object) registered} with an ID. The remote end of
* connections that have been {@link #addConnection(Connection) added} are
* allowed to {@link #getRemoteObject(Connection, int, Class) access} registered
* objects.
* <p>
* It costs at least 2 bytes more to use remote method invocation than just
* sending the parameters. If the method has a return value which is not
* {@link RemoteObject#setNonBlocking(boolean) ignored}, an extra byte is
* written. If the type of a parameter is not final (note primitives are final)
* then an extra byte is written for that parameter.
*
* @author Nathan Sweet <misc@n4te.com>, Nathan Robinson
*/
public class RmiBridge {
private static final String OBJECT_ID = "objectID";
static CopyOnWriteArrayList<RmiBridge> instances = new CopyOnWriteArrayList<RmiBridge>();
private static final HashMap<Class<?>, CachedMethod[]> methodCache = new HashMap<Class<?>, CachedMethod[]>();
static final byte kReturnValMask = (byte) 0x80; // 1000 0000
static final byte kReturnExMask = (byte) 0x40; // 0100 0000
private static final int N_THREADS = 5;
private static final int POOL_SIZE = 5;
private static final Executor defaultExectutor = new ThreadPoolExecutor(N_THREADS, POOL_SIZE,
5, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(N_THREADS * POOL_SIZE));
// can be access by DIFFERENT threads.
volatile IntMap<Object> idToObject = new IntMap<Object>();
volatile ObjectIntMap<Object> objectToID = new ObjectIntMap<Object>();
private CopyOnWriteArrayList<Connection> connections = new CopyOnWriteArrayList<Connection>();
private Executor executor;
// the name of who created this object space.
private final org.slf4j.Logger logger;
private final String name;
private final Listener<Connection, InvokeMethod> invokeListener= new Listener<Connection, InvokeMethod>() {
@Override
public void received(final Connection connection, final InvokeMethod invokeMethod) {
boolean found = false;
Iterator<Connection> iterator = connections.iterator();
while (iterator.hasNext()) {
Connection c = iterator.next();
if (c == connection) {
found = true;
break;
}
}
// The InvokeMethod message is not for a connection in this ObjectSpace.
if (!found) {
return;
}
final Object target = idToObject.get(invokeMethod.objectID);
if (target == null) {
logger.warn("Ignoring remote invocation request for unknown object ID: {}",
invokeMethod.objectID);
return;
}
if (executor == null) {
defaultExectutor.execute(new Runnable() {
@Override
public void run() {
invoke(connection,
target,
invokeMethod);
}
});
} else {
executor.execute(new Runnable() {
@Override
public void run() {
invoke(connection,
target,
invokeMethod);
}
});
}
}
@Override
public void disconnected(Connection connection) {
removeConnection(connection);
}
};
/**
* Creates an ObjectSpace with no connections. Connections must be
* {@link #connectionConnected(Connection) added} to allow the remote end of
* the connections to access objects in this ObjectSpace.
* <p>
* For safety, this should ONLY be called by {@link EndPoint#getRmiBridge() }
*/
public RmiBridge(String name) {
this.name = "RMI - " + name + " (remote)";
logger = org.slf4j.LoggerFactory.getLogger(this.name);
Class<?> callerClass = sun.reflect.Reflection.getCallerClass(2);
// starts with will allow for anonymous inner classes.
if (callerClass != null && callerClass.getName().startsWith(EndPoint.class.getName())) {
instances.addIfAbsent(this);
} else {
throw new RuntimeException("It is UNSAFE to access this constructor DIRECTLY. Please use Endpoint.getRmiBridge()");
}
}
/**
* Sets the executor used to invoke methods when an invocation is received
* from a remote endpoint. By default, no executor is set and invocations
* occur on the network thread, which should not be blocked for long.
*
* @param executor
* May be null.
*/
public void setExecutor(Executor executor) {
this.executor = executor;
}
/**
* Registers an object to allow the remote end of the ObjectSpace's
* connections to access it using the specified ID.
* <p>
* If a connection is added to multiple ObjectSpaces, the same object ID
* should not be registered in more than one of those ObjectSpaces.
*
* @param objectID
* Must not be Integer.MAX_VALUE.
* @see #getRemoteObject(Connection, int, Class...)
*/
public void register(int objectID, Object object) {
if (objectID == Integer.MAX_VALUE) {
throw new IllegalArgumentException("objectID cannot be Integer.MAX_VALUE.");
}
if (object == null) {
throw new IllegalArgumentException("object cannot be null.");
}
idToObject.put(objectID, object);
objectToID.put(object, objectID);
logger.trace("Object registered with ObjectSpace as {}:{}", objectID, object);
}
/**
* Causes this ObjectSpace to stop listening to the connections for method
* invocation messages.
*/
public void close() {
Iterator<Connection> iterator = connections.iterator();
while (iterator.hasNext()) {
Connection connection = iterator.next();
connection.listeners().remove(invokeListener);
}
instances.remove(this);
logger.trace("Closed ObjectSpace.");
}
/**
* Removes an object. The remote end of the ObjectSpace's connections will
* no longer be able to access it.
*/
public void remove(int objectID) {
Object object = idToObject.remove(objectID);
if (object != null) {
objectToID.remove(object, 0);
}
logger.trace("Object {} removed from ObjectSpace: {}", objectID, object);
}
/**
* Removes an object. The remote end of the ObjectSpace's connections will
* no longer be able to access it.
*/
public void remove(Object object) {
if (!idToObject.containsValue(object, true)) {
return;
}
int objectID = idToObject.findKey(object, true, -1);
idToObject.remove(objectID);
objectToID.remove(object, 0);
logger.trace("Object {} removed from ObjectSpace: {}", objectID, object);
}
/**
* Allows the remote end of the specified connection to access objects
* registered in this ObjectSpace.
*/
public void addConnection(Connection connection) {
if (connection == null) {
throw new IllegalArgumentException("connection cannot be null.");
}
connections.addIfAbsent(connection);
connection.listeners().add(invokeListener);
logger.trace("Added connection to ObjectSpace: {}", connection);
}
/**
* Removes the specified connection, it will no longer be able to access
* objects registered in this ObjectSpace.
*/
public void removeConnection(Connection connection) {
if (connection == null) {
throw new IllegalArgumentException("connection cannot be null.");
}
connection.listeners().remove(invokeListener);
connections.remove(connection);
logger.trace("Removed connection from ObjectSpace: {}", connection);
}
/**
* Invokes the method on the object and, if necessary, sends the result back
* to the connection that made the invocation request. This method is
* invoked on the update thread of the {@link EndPoint} for this ObjectSpace
* and unless an {@link #setExecutor(Executor) executor} has been set.
*
* @param connection
* The remote side of this connection requested the invocation.
*/
protected void invoke(Connection connection, Object target, InvokeMethod invokeMethod) {
if (logger.isDebugEnabled()) {
String argString = "";
if (invokeMethod.args != null) {
argString = Arrays.deepToString(invokeMethod.args);
argString = argString.substring(1, argString.length() - 1);
}
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(connection.toString()).append(" received: ").append(target.getClass().getSimpleName());
stringBuilder.append(":").append(invokeMethod.objectID);
stringBuilder.append("#").append(invokeMethod.method.getName());
stringBuilder.append("(").append(argString).append(")");
logger.debug(stringBuilder.toString());
}
byte responseID = invokeMethod.responseID;
boolean transmitReturnVal = (responseID & kReturnValMask) == kReturnValMask;
boolean transmitExceptions = (responseID & kReturnExMask) == kReturnExMask;
Object result = null;
Method method = invokeMethod.method;
try {
result = method.invoke(target, invokeMethod.args);
// Catch exceptions caused by the Method#invoke
} catch (InvocationTargetException ex) {
if (transmitExceptions) {
// added to prevent a stack overflow when references is false
// (because cause = "this").
// See:
// https://groups.google.com/forum/?fromgroups=#!topic/kryo-users/6PDs71M1e9Y
Throwable cause = ex.getCause();
cause.initCause(null);
result = cause;
} else {
throw new RuntimeException("Error invoking method: " + method.getDeclaringClass().getName() + "."
+ method.getName(), ex);
}
} catch (Exception ex) {
throw new RuntimeException("Error invoking method: " + method.getDeclaringClass().getName() + "."
+ method.getName(), ex);
}
if (responseID == 0) {
return;
}
InvokeMethodResult invokeMethodResult = new InvokeMethodResult();
invokeMethodResult.objectID = invokeMethod.objectID;
invokeMethodResult.responseID = responseID;
// Do not return non-primitives if transmitReturnVal is false
if (!transmitReturnVal && !invokeMethod.method.getReturnType().isPrimitive()) {
invokeMethodResult.result = null;
} else {
invokeMethodResult.result = result;
}
// System.err.println("Sending: " + invokeMethod.responseID);
connection.send().TCP(invokeMethodResult).flush();
// logger.error("{} sent data: {} with id ({})", connection, result,
// invokeMethod.responseID);
// if (invokeMethod.responseID == -52) {
// System.err.println("ASDASD");
// }
}
/**
* Identical to {@link #getRemoteObject(C, int, Class...)} except returns
* the object cast to the specified interface type. The returned object
* still implements {@link RemoteObject}.
*/
@SuppressWarnings({"unchecked"})
static public <T, C extends Connection> T getRemoteObject(final C connection, int objectID, Class<T> iface) {
return (T) getRemoteObject(connection, objectID, new Class<?>[] {iface});
}
/**
* Returns a proxy object that implements the specified interfaces. Methods
* invoked on the proxy object will be invoked remotely on the object with
* the specified ID in the ObjectSpace for the specified connection. If the
* remote end of the connection has not {@link #addConnection(Connection)
* added} the connection to the ObjectSpace, the remote method invocations
* will be ignored.
* <p>
* Methods that return a value will throw {@link TimeoutException} if the
* response is not received with the
* {@link RemoteObject#setResponseTimeout(int) response timeout}.
* <p>
* If {@link RemoteObject#setNonBlocking(boolean) non-blocking} is false
* (the default), then methods that return a value must not be called from
* the update thread for the connection. An exception will be thrown if this
* occurs. Methods with a void return value can be called on the update
* thread.
* <p>
* If a proxy returned from this method is part of an object graph sent over
* the network, the object graph on the receiving side will have the proxy
* object replaced with the registered object.
*
* @see RemoteObject
*/
public static RemoteObject getRemoteObject(Connection connection, int objectID, Class<?>... ifaces) {
if (connection == null) {
throw new IllegalArgumentException("connection cannot be null.");
}
if (ifaces == null) {
throw new IllegalArgumentException("ifaces cannot be null.");
}
Class<?>[] temp = new Class<?>[ifaces.length + 1];
temp[0] = RemoteObject.class;
System.arraycopy(ifaces, 0, temp, 1, ifaces.length);
return (RemoteObject) Proxy.newProxyInstance(RmiBridge.class.getClassLoader(),
temp,
new RemoteInvocationHandler(connection, objectID));
}
static CachedMethod[] getMethods(Kryo kryo, Class<?> type) {
CachedMethod[] cachedMethods = methodCache.get(type);
if (cachedMethods != null) {
return cachedMethods;
}
ArrayList<Method> allMethods = new ArrayList<Method>();
Class<?> nextClass = type;
while (nextClass != null && nextClass != Object.class) {
Collections.addAll(allMethods, nextClass.getDeclaredMethods());
nextClass = nextClass.getSuperclass();
}
PriorityQueue<Method> methods = new PriorityQueue<Method>(Math.max(1, allMethods.size()),
new Comparator<Method>() {
@Override
public int compare(Method o1, Method o2) {
// Methods are sorted so they can be represented as an index.
int diff = o1.getName().compareTo(o2.getName());
if (diff != 0) {
return diff;
}
Class<?>[] argTypes1 = o1.getParameterTypes();
Class<?>[] argTypes2 = o2.getParameterTypes();
if (argTypes1.length > argTypes2.length) {
return 1;
}
if (argTypes1.length < argTypes2.length) {
return -1;
}
for (int i = 0; i < argTypes1.length; i++) {
diff = argTypes1[i].getName().compareTo(argTypes2[i].getName());
if (diff != 0) {
return diff;
}
}
throw new RuntimeException("Two methods with same signature!"); // Impossible.
}
});
for (int i = 0, n = allMethods.size(); i < n; i++) {
Method method = allMethods.get(i);
int modifiers = method.getModifiers();
if (Modifier.isStatic(modifiers)) {
continue;
}
if (Modifier.isPrivate(modifiers)) {
continue;
}
if (method.isSynthetic()) {
continue;
}
methods.add(method);
}
int n = methods.size();
cachedMethods = new CachedMethod[n];
for (int i = 0; i < n; i++) {
CachedMethod cachedMethod = new CachedMethod();
cachedMethod.method = methods.poll();
// Store the serializer for each final parameter.
Class<?>[] parameterTypes = cachedMethod.method.getParameterTypes();
cachedMethod.serializers = new Serializer<?>[parameterTypes.length];
for (int ii = 0, nn = parameterTypes.length; ii < nn; ii++) {
if (kryo.isFinal(parameterTypes[ii])) {
cachedMethod.serializers[ii] = kryo.getSerializer(parameterTypes[ii]);
}
}
cachedMethods[i] = cachedMethod;
}
methodCache.put(type, cachedMethods);
return cachedMethods;
}
/**
* Returns the first object registered with the specified ID in any of the
* ObjectSpaces the specified connection belongs to.
*/
static Object getRegisteredObject(Connection connection, int objectID) {
CopyOnWriteArrayList<RmiBridge> instances = RmiBridge.instances;
for (RmiBridge objectSpace : instances) {
// Check if the connection is in this ObjectSpace.
Iterator<Connection> iterator = objectSpace.connections.iterator();
while (iterator.hasNext()) {
Connection c = iterator.next();
if (c != connection) {
continue;
}
// Find an object with the objectID.
Object object = objectSpace.idToObject.get(objectID);
if (object != null) {
return object;
}
}
}
return null;
}
/**
* Returns the first ID registered for the specified object with any of the
* ObjectSpaces the specified connection belongs to, or Integer.MAX_VALUE
* if not found.
*/
public static int getRegisteredId(Connection connection, Object object) {
CopyOnWriteArrayList<RmiBridge> instances = RmiBridge.instances;
for (RmiBridge objectSpace : instances) {
// Check if the connection is in this ObjectSpace.
Iterator<Connection> iterator = objectSpace.connections.iterator();
while (iterator.hasNext()) {
Connection c = iterator.next();
if (c != connection) {
continue;
}
// Find an ID with the object.
int id = objectSpace.objectToID.get(object, Integer.MAX_VALUE);
if (id != Integer.MAX_VALUE) {
return id;
}
}
}
return Integer.MAX_VALUE;
}
/**
* Registers the classes needed to use ObjectSpaces. This should be called
* before any connections are opened.
*/
public static <C extends Connection> void registerClasses(final SerializationManager smanager) {
smanager.registerForRmiClasses(new RmiRegisterClassesCallback() {
@Override
public void registerForClasses(Kryo kryo) {
kryo.register(Object[].class);
kryo.register(InvokeMethod.class);
FieldSerializer<InvokeMethodResult> resultSerializer = new FieldSerializer<InvokeMethodResult>(kryo, InvokeMethodResult.class) {
@Override
public void write(Kryo kryo, Output output, InvokeMethodResult result) {
super.write(kryo, output, result);
output.writeInt(result.objectID, true);
}
@Override
public InvokeMethodResult read(Kryo kryo, Input input, Class<InvokeMethodResult> type) {
InvokeMethodResult result = super.read(kryo, input, type);
result.objectID = input.readInt(true);
return result;
}
};
resultSerializer.removeField(OBJECT_ID);
kryo.register(InvokeMethodResult.class, resultSerializer);
kryo.register(InvocationHandler.class, new Serializer<Object>() {
@Override
public void write(Kryo kryo, Output output, Object object) {
RemoteInvocationHandler handler = (RemoteInvocationHandler) Proxy.getInvocationHandler(object);
output.writeInt(handler.objectID, true);
}
@Override
@SuppressWarnings({"unchecked"})
public Object read(Kryo kryo, Input input, Class<Object> type) {
int objectID = input.readInt(true);
Connection connection = (Connection) kryo.getContext().get(Connection.connection);
Object object = getRegisteredObject(connection, objectID);
if (object == null) {
final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(RmiBridge.class);
logger.warn("Unknown object ID {} for connection: {}", objectID, connection);
}
return object;
}
});
}
});
}
}

View File

@ -0,0 +1,6 @@
package dorkbox.network.rmi;
// used by RMI. This is to keep track and not throw errors when connections are notified of it's arrival
public interface RmiMessages {
}

View File

@ -0,0 +1,7 @@
package dorkbox.network.rmi;
import com.esotericsoftware.kryo.Kryo;
public interface RmiRegisterClassesCallback {
public void registerForClasses(Kryo kryo);
}

View File

@ -0,0 +1,7 @@
package dorkbox.network.rmi;
import com.esotericsoftware.kryo.Serializer;
public interface SerializerRegistration<T extends Serializer<?>> {
public void register(T serializer);
}

View File

@ -0,0 +1,26 @@
package dorkbox.network.rmi;
/** Thrown when a method with a return value is invoked on a remote object and the response is not received with the
* {@link RemoteObject#setResponseTimeout(int) response timeout}.
* @see RmiBridge#getRemoteObject(com.esotericsoftware.kryonet.Connection, int, Class...)
* @author Nathan Sweet <misc@n4te.com> */
public class TimeoutException extends RuntimeException {
private static final long serialVersionUID = -3526277240277423682L;
public TimeoutException () {
super();
}
public TimeoutException (String message, Throwable cause) {
super(message, cause);
}
public TimeoutException (String message) {
super(message);
}
public TimeoutException (Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,112 @@
/*
* Copyright 2010 Martin Grotzke
*
* 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 &quot;AS IS&quot; 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.util;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.Serializer;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
/**
* A kryo {@link Serializer} for lists created via {@link Arrays#asList(Object...)}.
* <p>
* Note: This serializer does not support cyclic references, so if one of the objects
* gets set the list as attribute this might cause an error during deserialization.
* </p>
*
* @author <a href="mailto:martin.grotzke@javakaffee.de">Martin Grotzke</a>
*/
public class ArraysAsListSerializer extends Serializer<List<?>> {
private Field _arrayField;
public ArraysAsListSerializer() {
try {
_arrayField = Class.forName( "java.util.Arrays$ArrayList" ).getDeclaredField( "a" );
_arrayField.setAccessible( true );
} catch ( final Exception e ) {
throw new RuntimeException( e );
}
// Immutable causes #copy(obj) to return the original object
setImmutable(true);
}
@Override
public List<?> read(final Kryo kryo, final Input input, final Class<List<?>> type) {
final int length = input.readInt(true);
Class<?> componentType = kryo.readClass( input ).getType();
if (componentType.isPrimitive()) {
componentType = getPrimitiveWrapperClass(componentType);
}
try {
final Object items = Array.newInstance( componentType, length );
for( int i = 0; i < length; i++ ) {
Array.set(items, i, kryo.readClassAndObject( input ));
}
return Arrays.asList( (Object[])items );
} catch ( final Exception e ) {
throw new RuntimeException( e );
}
}
@Override
public void write(final Kryo kryo, final Output output, final List<?> obj) {
try {
final Object[] array = (Object[]) _arrayField.get( obj );
output.writeInt(array.length, true);
final Class<?> componentType = array.getClass().getComponentType();
kryo.writeClass( output, componentType );
for( final Object item : array ) {
kryo.writeClassAndObject( output, item );
}
} catch ( final RuntimeException e ) {
// Don't eat and wrap RuntimeExceptions because the ObjectBuffer.write...
// handles SerializationException specifically (resizing the buffer)...
throw e;
} catch ( final Exception e ) {
throw new RuntimeException( e );
}
}
private static Class<?> getPrimitiveWrapperClass(final Class<?> c) {
if (c.isPrimitive()) {
if (c.equals(Long.TYPE)) {
return Long.class;
} else if (c.equals(Integer.TYPE)) {
return Integer.class;
} else if (c.equals(Double.TYPE)) {
return Double.class;
} else if (c.equals(Float.TYPE)) {
return Float.class;
} else if (c.equals(Boolean.TYPE)) {
return Boolean.class;
} else if (c.equals(Character.TYPE)) {
return Character.class;
} else if (c.equals(Short.TYPE)) {
return Short.class;
} else if (c.equals(Byte.TYPE)) {
return Byte.class;
}
}
return c;
}
}

View File

@ -0,0 +1,201 @@
package dorkbox.network.util;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.util.Util;
public class BinaryListReferenceResolver implements com.esotericsoftware.kryo.ReferenceResolver {
protected final List<Object> readObjects = new ArrayList<Object>();
private int[] writtenObjectsHashes = new int[10];
// objects, then values
private Object[] writtenObjectsAndValues = new Object[20];
private int size = 0;
private int primaryArraySize = 0;
@Override
public void setKryo(Kryo kryo) {
}
private static int binarySearch(int[] array, int startIndex, int endIndex, int value) {
int low = startIndex, mid = -1, high = endIndex - 1;
while (low <= high) {
mid = low + high >>> 1;
if (value > array[mid]) {
low = mid + 1;
} else if (value == array[mid]) {
return mid;
} else {
high = mid - 1;
}
}
if (mid < 0) {
int insertPoint = endIndex;
for (int index = startIndex; index < endIndex; index++) {
if (value < array[index]) {
insertPoint = index;
}
}
return -insertPoint - 1;
}
return -mid - (value < array[mid] ? 1 : 2);
}
@Override
public int addWrittenObject(Object object) {
int id = size;
int hash = System.identityHashCode(object);
int idx = binarySearch(writtenObjectsHashes, 0, primaryArraySize, hash);
if (idx < 0) {
idx = -(idx + 1);
if (primaryArraySize == writtenObjectsHashes.length) {
int[] newHashArray = new int[writtenObjectsHashes.length * 3 / 2];
System.arraycopy(writtenObjectsHashes, 0, newHashArray, 0, writtenObjectsHashes.length);
writtenObjectsHashes = newHashArray;
Object[] newObjectArray = new Object[newHashArray.length * 2];
System.arraycopy(writtenObjectsAndValues, 0, newObjectArray, 0, writtenObjectsAndValues.length);
writtenObjectsAndValues = newObjectArray;
}
for (int i = writtenObjectsHashes.length - 1; i > idx; i--) {
int j = 2 * i;
writtenObjectsHashes[i] = writtenObjectsHashes[i - 1];
writtenObjectsAndValues[j] = writtenObjectsAndValues[j - 2];
writtenObjectsAndValues[j + 1] = writtenObjectsAndValues[j - 1];
}
writtenObjectsHashes[idx] = hash;
writtenObjectsAndValues[2 * idx] = object;
writtenObjectsAndValues[2 * idx + 1] = id;
primaryArraySize++;
size++;
return id;
} else {
idx = 2 * idx; // objects and values array has bigger indexes
if (writtenObjectsAndValues[idx + 1] instanceof Integer) {
// single slot
if (writtenObjectsAndValues[idx] == object) {
return (Integer) writtenObjectsAndValues[idx + 1];
} else {
Object[] keys = new Object[4];
int[] values = new int[4];
keys[0] = writtenObjectsAndValues[idx];
values[0] = (Integer) writtenObjectsAndValues[idx + 1];
keys[1] = object;
values[1] = id;
writtenObjectsAndValues[idx] = keys;
writtenObjectsAndValues[idx + 1] = values;
size++;
return id;
}
} else {
// multiple entry slot
Object[] keys = (Object[]) writtenObjectsAndValues[idx];
for (int i = 0; i < keys.length; i++) {
if (keys[i] == object) {
return ((int[]) writtenObjectsAndValues[idx + 1])[i];
}
if (keys[i] == null) {
keys[i] = object;
((int[]) writtenObjectsAndValues[idx + 1])[i] = id;
size++;
return id;
}
}
// expand
Object[] newKeys = new Object[keys.length * 3 / 2];
System.arraycopy(keys, 0, newKeys, 0, keys.length);
newKeys[keys.length] = object;
int[] newValues = new int[keys.length * 3 / 2];
System.arraycopy(writtenObjectsAndValues[idx + 1], 0, newValues, 0, keys.length);
writtenObjectsAndValues[idx] = newKeys;
writtenObjectsAndValues[idx + 1] = newValues;
size++;
return id;
}
}
}
@Override
public int getWrittenId(Object object) {
int hash = System.identityHashCode(object);
int idx = binarySearch(writtenObjectsHashes, 0, primaryArraySize, hash);
if (idx < 0) {
return -1;
} else {
idx = 2 * idx; // objects and values array has bigger indexes
if (writtenObjectsAndValues[idx + 1] instanceof Integer) {
// single slot
if (writtenObjectsAndValues[idx] == object) {
return (Integer) writtenObjectsAndValues[idx + 1];
} else {
return -1;
}
} else {
// multiple entry slot
Object[] keys = (Object[]) writtenObjectsAndValues[idx];
for (int i = 0; i < keys.length; i++) {
if (keys[i] == object) {
return ((int[]) writtenObjectsAndValues[idx + 1])[i];
}
if (keys[i] == null) {
return -1;
}
}
return -1;
}
}
}
@Override
@SuppressWarnings("rawtypes")
public int nextReadId(Class type) {
int id = readObjects.size();
readObjects.add(null);
return id;
}
@Override
public void setReadObject(int id, Object object) {
readObjects.set(id, object);
}
@Override
@SuppressWarnings("rawtypes")
public Object getReadObject(Class type, int id) {
return readObjects.get(id);
}
@Override
public void reset() {
readObjects.clear();
size = 0;
primaryArraySize = 0;
writtenObjectsAndValues = new Object[20];
writtenObjectsHashes = new int[10];
}
/** Returns false for all primitive wrappers. */
@Override
@SuppressWarnings("rawtypes")
public boolean useReferences(Class type) {
return !Util.isWrapperClass(type) &&
!type.equals(String.class) &&
!type.equals(Date.class) &&
!type.equals(BigDecimal.class) &&
!type.equals(BigInteger.class);
}
public void addReadObject(int id, Object object) {
while (id >= readObjects.size()) {
readObjects.add(null);
}
readObjects.set(id, object);
}
}

View File

@ -0,0 +1,37 @@
package dorkbox.network.util;
import java.util.Arrays;
/**
* Necessary to provide equals and hashcode for byte arrays (if they are to be used in a map/set/etc)
*/
public final class ByteArrayWrapper {
private final byte[] data;
public ByteArrayWrapper(byte[] data) {
if (data == null) {
throw new NullPointerException();
}
int length = data.length;
this.data = new byte[length];
// copy so it's immutable as a key.
System.arraycopy(data, 0, this.data, 0, length);
}
public byte[] getBytes() {
return this.data;
}
@Override
public boolean equals(Object other) {
if (!(other instanceof ByteArrayWrapper)) {
return false;
}
return Arrays.equals(this.data, ((ByteArrayWrapper) other).data);
}
@Override
public int hashCode() {
return Arrays.hashCode(this.data);
}
}

View File

@ -0,0 +1,32 @@
package dorkbox.network.util;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public abstract class ConcurrentHashMapFactory<K, V> extends ConcurrentHashMap<K, V> implements ConcurrentMap<K, V> {
private static final long serialVersionUID = -1796263935845885270L;
public ConcurrentHashMapFactory() {
}
public abstract V createNewOject(Object... args);
public final V getOrCreate(K key, Object... args) {
V orig = get(key);
if (orig == null) {
// It's OK to construct a new object that ends up not being used
orig = createNewOject(args);
V putByOtherThreadJustNow = putIfAbsent(key, orig);
if (putByOtherThreadJustNow != null) {
// Some other thread "won"
orig = putByOtherThreadJustNow;
} else {
// This thread was the winner
}
}
return orig;
}
}

View File

@ -0,0 +1,22 @@
package dorkbox.network.util;
public class InitializationException extends Exception {
private static final long serialVersionUID = -3743402298699150389L;
public InitializationException() {
super();
}
public InitializationException(String message, Throwable cause) {
super(message, cause);
}
public InitializationException(String message) {
super(message);
}
public InitializationException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,845 @@
package dorkbox.network.util;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.compression.CompressionException;
import io.netty.handler.codec.compression.SnappyAccess;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.zip.DataFormatException;
import java.util.zip.Deflater;
import java.util.zip.Inflater;
import org.bouncycastle.crypto.engines.AESFastEngine;
import com.esotericsoftware.kryo.ClassResolver;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.KryoException;
import com.esotericsoftware.kryo.Registration;
import com.esotericsoftware.kryo.Serializer;
import com.esotericsoftware.kryo.serializers.CollectionSerializer;
import com.esotericsoftware.kryo.util.MapReferenceResolver;
import dorkbox.network.connection.Connection;
import dorkbox.network.pipeline.ByteBufInput;
import dorkbox.network.pipeline.ByteBufOutput;
import dorkbox.network.rmi.RmiRegisterClassesCallback;
import dorkbox.network.rmi.SerializerRegistration;
import dorkbox.util.crypto.Crypto;
import dorkbox.util.crypto.bouncycastle.GCMBlockCipher_ByteBuf;
/**
* Threads reading/writing, it messes up a single instance.
* it is possible to use a single kryo with the use of synchronize, however - that defeats the point of multi-threaded
*/
public class KryoSerializationManager implements SerializationManager {
private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(KryoSerializationManager.class);
private static final boolean ENABLE_SNAPPY = false;
/**
* Specify if we want KRYO to use unsafe memory for serialization, or to use the ASM backend. Unsafe memory use is WAY faster, but is
* limited to the "same endianess" on all endpoints, and unsafe DOES NOT work on android.
*/
public static boolean useUnsafeMemory = false;
/**
* The minimum amount that we'll consider actually attempting to compress.
* This value is preamble + the minimum length our Snappy service will
* compress (instead of just emitting a literal).
*/
private static final int MIN_COMPRESSIBLE_LENGTH = 18;
private enum ChunkType {
COMPRESSED_DATA,
UNCOMPRESSED_DATA,
RESERVED_UNSKIPPABLE,
RESERVED_SKIPPABLE
}
/** bit masks */
private static final int compression = 1 << 0;
private static final int crypto = 1 << 1;
private final Object instanceWaitLock = new Object();
private final Integer numberOfInstances;
// private final int maxSize;
// compression options
private static final int compressionLevel = 6;
private final SnappyAccess[] snappys;
private final Deflater[] deflaters;
private final Inflater[] inflaters;
private final Kryo[] kryos;
private final AtomicBoolean[] kryoLocks;
private final ByteBufInput[] inputBuffers;
private final ByteBufOutput[] outputBuffers;
// lazy allocate the buffers!
private ByteBuf[] tmpBuffers1;
private ByteBuf[] tmpBuffers2;
private GCMBlockCipher_ByteBuf[] aesEngines;
public KryoSerializationManager() {
this(Runtime.getRuntime().availableProcessors() * 4);
}
public KryoSerializationManager(int numberOfInstances) {
this.numberOfInstances = numberOfInstances;
this.snappys = new SnappyAccess[numberOfInstances];
this.deflaters = new Deflater[numberOfInstances];
this.inflaters = new Inflater[numberOfInstances];
this.kryos = new Kryo[numberOfInstances];
this.kryoLocks = new AtomicBoolean[numberOfInstances];
this.inputBuffers = new ByteBufInput[numberOfInstances];
this.outputBuffers = new ByteBufOutput[numberOfInstances];
this.tmpBuffers1 = new ByteBuf[numberOfInstances];
this.tmpBuffers2 = new ByteBuf[numberOfInstances];
this.aesEngines = new GCMBlockCipher_ByteBuf[numberOfInstances];
// we HAVE to pre-allocate the KRYOs
boolean useAsm = !useUnsafeMemory;
for (int i=0;i<numberOfInstances;i++) {
this.kryos[i] = new Kryo();
this.kryos[i].setAsmEnabled(useAsm);
this.kryoLocks[i] = new AtomicBoolean(false);
}
}
/**
* If true, each appearance of an object in the graph after the first is
* stored as an integer ordinal. When set to true,
* {@link MapReferenceResolver} is used. This enables references to the same
* object and cyclic graphs to be serialized, but typically adds overhead of
* one byte per object. Default is true.
*
* @return The previous value.
*/
@Override
public boolean setReferences(boolean references) {
boolean previous = references;
for (Kryo k : this.kryos) {
previous = k.setReferences(references);
}
return previous;
}
/**
* If true, an exception is thrown when an unregistered class is
* encountered. Default is false.
* <p>
* If false, when an unregistered class is encountered, its fully qualified
* class name will be serialized and the
* {@link #addDefaultSerializer(Class, Class) default serializer} for the
* class used to serialize the object. Subsequent appearances of the class
* within the same object graph are serialized as an int id.
* <p>
* Registered classes are serialized as an int id, avoiding the overhead of
* serializing the class name, but have the drawback of needing to know the
* classes to be serialized up front.
*/
@Override
public void setRegistrationRequired(boolean registrationRequired) {
for (Kryo k : this.kryos) {
k.setRegistrationRequired(registrationRequired);
}
}
/**
* Registers the class using the lowest, next available integer ID and the
* {@link Kryo#getDefaultSerializer(Class) default serializer}. If the class
* is already registered, the existing entry is updated with the new
* serializer. Registering a primitive also affects the corresponding
* primitive wrapper.
* <p>
* Because the ID assigned is affected by the IDs registered before it, the
* order classes are registered is important when using this method. The
* order must be the same at deserialization as it was for serialization.
*/
@Override
public void register(Class<?> clazz) {
for (Kryo k : this.kryos) {
k.register(clazz);
}
}
/**
* Registers the class using the lowest, next available integer ID and the
* specified serializer. If the class is already registered, the existing
* entry is updated with the new serializer. Registering a primitive also
* affects the corresponding primitive wrapper.
* <p>
* Because the ID assigned is affected by the IDs registered before it, the
* order classes are registered is important when using this method. The
* order must be the same at deserialization as it was for serialization.
*/
@Override
public void register(Class<?> clazz, Serializer<?> serializer) {
for (Kryo k : this.kryos) {
k.register(clazz, serializer);
}
}
/**
* <b>primarily used by RMI</b> It is not common to call this method!
* <p>
* Registers the class using the lowest, next available integer ID and the
* {@link Kryo#SerializerRegistration(Class) serializer}. If the class
* is already registered, the existing entry is updated with the new
* serializer. Registering a primitive also affects the corresponding
* primitive wrapper.
* <p>
* Because the ID assigned is affected by the IDs registered before it, the
* order classes are registered is important when using this method. The
* order must be the same at deserialization as it was for serialization.
*/
@Override
@SuppressWarnings({"rawtypes","unchecked"})
public void registerSerializer(Class<?> clazz, SerializerRegistration registration) {
for (Kryo k : this.kryos) {
Registration reg = k.register(clazz);
registration.register(reg.getSerializer());
}
}
/**
* Necessary to register classes for RMI.
*/
@Override
public void registerForRmiClasses(RmiRegisterClassesCallback callback) {
for (Kryo kryo : this.kryos) {
callback.registerForClasses(kryo);
}
}
/**
* If the class is not registered and {@link SerializationManager#setRegistrationRequired(boolean)} is false, it is
* automatically registered using the {@link SerializationManager#addDefaultSerializer(Class, Class) default serializer}.
*
* @throws IllegalArgumentException
* if the class is not registered and {@link SerializationManager#setRegistrationRequired(boolean)} is true.
* @see ClassResolver#getRegistration(Class)
*/
@Override
public Registration getRegistration(Class<?> clazz) {
Registration r = null;
for (Kryo k : this.kryos) {
r = k.getRegistration(clazz);
}
return r;
}
/**
* Registers the class using the specified ID and serializer. If the ID is
* already in use by the same type, the old entry is overwritten. If the ID
* is already in use by a different type, a {@link KryoException} is thrown.
* Registering a primitive also affects the corresponding primitive wrapper.
* <p>
* IDs must be the same at deserialization as they were for serialization.
*
* @param id
* Must be >= 0. Smaller IDs are serialized more efficiently. IDs
* 0-8 are used by default for primitive types and String, but
* these IDs can be repurposed.
*/
@Override
public Registration register(Class<?> type, Serializer<?> serializer, int id) {
Registration r = null;
for (Kryo k : this.kryos) {
r = k.register(type, serializer, id);
}
return r;
}
/**
* attempt to allocate the given index. This MUST be wrapped in a synchronized call.!
*/
private final void allocateLazy(int index) {
// keyed off the snappy instance
if (this.snappys[index] != null) {
return;
}
this.snappys[index] = new SnappyAccess();
this.deflaters[index] = new Deflater(compressionLevel, true);
this.inflaters[index] = new Inflater(true);
this.inputBuffers[index] = new ByteBufInput();
this.outputBuffers[index] = new ByteBufOutput();
this.tmpBuffers1[index] = Unpooled.buffer(1024);
this.tmpBuffers2[index] = Unpooled.buffer(1024);
this.aesEngines[index] = new GCMBlockCipher_ByteBuf(new AESFastEngine());
// from the list-serve email. This offers 8x performance in resolving references over the default impl.
this.kryos[index].setReferenceResolver(new BinaryListReferenceResolver());
// necessary for the transport of exceptions.
CollectionSerializer serializer = new CollectionSerializer();
this.kryos[index].register(ArrayList.class, serializer);
UnmodifiableCollectionsSerializer.registerSerializers(this.kryos[index]);
}
/**
* Determines if this buffer is encrypted or not.
*/
@Override
public final boolean isEncrypted(ByteBuf buffer) {
// read off the magic byte
byte magicByte = buffer.getByte(buffer.readerIndex());
return (magicByte & crypto) == crypto;
}
/**
* Waits until a kryo is available to write, using CAS operations to prevent having to synchronize.
*
* No crypto and no sqeuence number
*
* There is a small speed penalty if there were no kryo's available to use.
*/
@Override
public final void write(ByteBuf buffer, Object message) {
write0(null, buffer, message, false);
}
/**
* Waits until a kryo is available to write, using CAS operations to prevent having to synchronize.
*
* There is a small speed penalty if there were no kryo's available to use.
*/
@Override
public final void writeWithCryptoTcp(Connection connection, ByteBuf buffer, Object message) {
if (connection == null) {
throw new NetException("Unable to perform crypto when NO network connection!");
}
write0(connection, buffer, message, true);
}
/**
* Waits until a kryo is available to write, using CAS operations to prevent having to synchronize.
*
* There is a small speed penalty if there were no kryo's available to use.
*/
@Override
public final void writeWithCryptoUdp(Connection connection, ByteBuf buffer, Object message) {
if (connection == null) {
throw new NetException("Unable to perform crypto when NO network connection!");
}
write0(connection, buffer, message, true);
}
/**
* @param isTcp false if UDP or if we don't care.
*/
@SuppressWarnings("unchecked")
private final void write0(Connection connection, ByteBuf buffer, Object message, boolean doCrypto) {
nextAvailable:
while (true) {
for (int i=0;i<this.numberOfInstances;i++) {
boolean wasAvailable = this.kryoLocks[i].compareAndSet(false, true);
if (wasAvailable) {
allocateLazy(i);
byte magicByte = (byte) 0x00000000;
ByteBuf bufferWithData = this.tmpBuffers1[i];
ByteBuf bufferTempData = this.tmpBuffers2[i];
// write the object to the TEMP buffer! this will be compressed with snappy
this.outputBuffers[i].setBuffer(bufferWithData);
// connection will ALWAYS be of type IConnection or NULL.
// used by RMI/some serializers to determine which connection wrote this object
this.kryos[i].getContext().put(Connection.connection, connection);
this.kryos[i].writeClassAndObject(this.outputBuffers[i], message);
// release resources
this.outputBuffers[i].setBuffer((ByteBuf)null);
// save off how much data the object took + the length of the (possible) sequence.
int length = bufferWithData.writerIndex(); // it started at ZERO (since it's written to the temp buffer.
// snappy compression
// tmpBuffer2 = compress(tmpBuffer1)
if (length > MIN_COMPRESSIBLE_LENGTH) {
if (ENABLE_SNAPPY) {
snappyCompress(bufferWithData, bufferTempData, length, this.snappys[i]);
} else {
compress(bufferWithData, bufferTempData, length, this.deflaters[i]);
}
// check to make sure that it was WORTH compressing, like what I had before
int compressedLength = bufferTempData.readableBytes();
if (compressedLength < length) {
// specify we compressed data
magicByte = (byte) (magicByte | compression);
length = compressedLength;
// swap buffers
ByteBuf tmp = bufferWithData;
bufferWithData = bufferTempData;
bufferTempData = tmp;
} else {
// "copy" (do nothing)
bufferWithData.readerIndex(0); // have to reset the reader
}
} else {
// "copy" (do nothing)
}
// at this point, we have 2 options for *bufferWithData*
// compress -> tmpBuffers2 has data
// copy -> tmpBuffers1 has data
// AES CRYPTO
if (doCrypto) {
logger.trace("Encrypting data with - AES {}", connection);
length = Crypto.AES.encrypt(this.aesEngines[i], connection.getCryptoParameters(),
bufferWithData, bufferTempData, length);
// swap buffers
ByteBuf tmp = bufferWithData;
bufferWithData = bufferTempData;
bufferTempData = tmp;
bufferTempData.clear();
// only needed for server UDP connections to determine if the data is encrypted or not.
magicByte = (byte) (magicByte | crypto);
}
/// MOVE EVERYTHING TO THE PROPER BYTE BUF
// write out the "magic" byte.
buffer.writeByte(magicByte); // leave space for the magic magicByte
// transfer the tmpBuffer (if necessary) back into the "primary" buffer.
buffer.writeBytes(bufferWithData);
// don't forget the clear the temp buffers!
this.tmpBuffers1[i].clear();
this.tmpBuffers2[i].clear();
this.kryoLocks[i].set(false);
break nextAvailable;
}
}
logger.trace("Waiting for another WRITE Kryo. It was full.");
// none were available. wait a small amount of time and try again
synchronized (this.instanceWaitLock) {
try {
this.instanceWaitLock.wait(20L);
} catch (InterruptedException e) {
break nextAvailable;
}
}
}
}
/**
* Reads an object from the buffer.
*
* No crypto and no sequence number
*
* @param connection can be NULL
* @param length should ALWAYS be the length of the expected object!
*/
@Override
public final Object read(ByteBuf buffer, int length) {
return read0(null, buffer, length, false);
}
/**
* Reads an object from the buffer.
*
* Crypto + sequence number
*
* @param connection can be NULL
* @param length should ALWAYS be the length of the expected object!
*/
@Override
public final Object readWithCryptoTcp(Connection connection, ByteBuf buffer, int length) {
if (connection == null) {
throw new NetException("Unable to perform crypto when NO network connection!");
}
return read0(connection, buffer, length, true);
}
/**
* Reads an object from the buffer.
*
* Crypto + sequence number
*
* @param connection can be NULL
* @param length should ALWAYS be the length of the expected object!
*/
@Override
public final Object readWithCryptoUdp(Connection connection, ByteBuf buffer, int length) {
if (connection == null) {
throw new NetException("Unable to perform crypto when NO network connection!");
}
return read0(connection, buffer, length, true);
}
/**
* @param isTcp false if UDP or if we don't care.
*/
@SuppressWarnings("unchecked")
private final Object read0(Connection connection, ByteBuf buffer, int length, boolean doCrypto) {
while (true) {
for (int i=0;i<this.numberOfInstances;i++) {
boolean wasAvailable = this.kryoLocks[i].compareAndSet(false, true);
////////////////
// Note: we CANNOT write BACK to "buffer" since there could be additional data on it!
////////////////
if (wasAvailable) {
allocateLazy(i);
// read off the magic byte
int startPosition = buffer.readerIndex();
byte magicByte = buffer.readByte();
// adjust for the magic byte
startPosition++;
length--;
int originalLength = length;
int originalStartPos = startPosition;
ByteBuf bufferWithData = buffer;
ByteBuf bufferTempData = this.tmpBuffers2[i];
// AES CRYPTO STUFF
if (doCrypto) {
if ((magicByte & crypto) != crypto) {
throw new NetException("Unable to perform crypto when data does not to use crypto!");
}
logger.trace("Decrypting data with - AES " + connection);
Crypto.AES.decrypt(this.aesEngines[i], connection.getCryptoParameters(),
bufferWithData, bufferTempData, length);
// since we "nuked" the start position, we have to make sure the compressor picks up the change.
startPosition = 0;
// swap buffers
bufferWithData = bufferTempData;
bufferTempData = this.tmpBuffers2[i];
}
// did we compress it??
if ((magicByte & compression) == compression) {
if (ENABLE_SNAPPY) {
snappyDecompress(bufferWithData, bufferTempData, this.snappys[i]);
} else {
decompress(bufferWithData, bufferTempData, this.inflaters[i]);
}
// swap buffers
ByteBuf tmp = bufferWithData;
bufferWithData = bufferTempData;
bufferTempData = tmp;
if (buffer == bufferTempData) {
bufferTempData = this.tmpBuffers2[i];
}
} else {
// "copy" (do nothing)
}
// read the object from the buffer.
this.inputBuffers[i].setBuffer(bufferWithData);
Object readClassAndObject = null;
try {
// connection will ALWAYS be of type IConnection or NULL.
// used by RMI/some serializers to determine which connection read this object
this.kryos[i].getContext().put(Connection.connection, connection);
readClassAndObject = this.kryos[i].readClassAndObject(this.inputBuffers[i]);
return readClassAndObject;
} catch (KryoException ex) {
throw new NetException("Unable to deserialize buffer", ex);
} finally {
// release resources
this.inputBuffers[i].setBuffer((ByteBuf)null);
// make sure the end of the buffer is in the correct spot.
// move the reader index to the end of the object (since we are reading encrypted data
// this just has to happen before the length field is reassigned.
buffer.readerIndex(originalStartPos + originalLength);
// don't forget the clear the temp buffers!
this.tmpBuffers1[i].clear();
this.tmpBuffers2[i].clear();
this.kryoLocks[i].set(false);
}
}
}
logger.trace("Waiting for another READ Kryo. It was full.");
// none were available. wait a small amount of time and try again
synchronized (this.instanceWaitLock) {
try {
this.instanceWaitLock.wait(20L);
} catch (InterruptedException e) {
return null;
}
}
}
}
private static void compress(ByteBuf inputBuffer, ByteBuf outputBuffer, int length, Deflater compress) {
byte[] in = new byte[inputBuffer.readableBytes()];
inputBuffer.readBytes(in);
compress.reset();
compress.setInput(in);
compress.finish();
byte[] out = new byte[1024];
int numBytes = out.length;
while (numBytes == out.length) {
numBytes = compress.deflate(out, 0, out.length);
outputBuffer.writeBytes(out, 0, numBytes);
}
}
private static void decompress(ByteBuf inputBuffer, ByteBuf outputBuffer, Inflater decompress) {
byte[] in = new byte[inputBuffer.readableBytes()];
inputBuffer.readBytes(in);
decompress.reset();
decompress.setInput(in);
byte[] out = new byte[1024];
int numBytes = out.length;
while (numBytes == out.length) {
try {
numBytes = decompress.inflate(out, 0, out.length);
} catch (DataFormatException e) {
logger.error("Error inflating data.", e);
throw new NetException(e.getCause());
}
outputBuffer.writeBytes(out, 0, numBytes);
}
}
private static void snappyCompress(ByteBuf inputBuffer, ByteBuf outputBuffer, int length, SnappyAccess snappy) {
// compress the tempBuffer (which has our object serialized inside it)
// If we have lots of available data, break it up into smaller chunks
int dataLength = length;
while (true) {
final int lengthIdx = outputBuffer.writerIndex() + 1;
if (dataLength < MIN_COMPRESSIBLE_LENGTH) {
ByteBuf slice = inputBuffer.readSlice(dataLength);
writeUnencodedChunk(slice, outputBuffer, dataLength);
break;
}
outputBuffer.writeInt(0);
if (dataLength > Short.MAX_VALUE) {
ByteBuf slice = inputBuffer.readSlice(Short.MAX_VALUE);
calculateAndWriteChecksum(slice, outputBuffer);
snappy.encode(slice, outputBuffer, Short.MAX_VALUE);
setChunkLength(outputBuffer, lengthIdx);
dataLength -= Short.MAX_VALUE;
} else {
ByteBuf slice = inputBuffer.readSlice(dataLength);
calculateAndWriteChecksum(slice, outputBuffer);
snappy.encode(slice, outputBuffer, dataLength);
setChunkLength(outputBuffer, lengthIdx);
break;
}
}
}
private static void snappyDecompress(ByteBuf inputBuffer, ByteBuf outputBuffer, SnappyAccess snappy) {
try {
int idx = inputBuffer.readerIndex();
final int inSize = inputBuffer.writerIndex() - idx;
if (inSize < 4) {
// We need to be at least able to read the chunk type identifier (one byte),
// and the length of the chunk (3 bytes) in order to proceed
return;
}
final int chunkTypeVal = inputBuffer.getUnsignedByte(idx);
final ChunkType chunkType = mapChunkType((byte) chunkTypeVal);
final int chunkLength = ByteBufUtil.swapMedium(inputBuffer.getUnsignedMedium(idx + 1));
switch (chunkType) {
case RESERVED_SKIPPABLE:
if (inSize < 4 + chunkLength) {
// TODO: Don't keep skippable bytes
return;
}
inputBuffer.skipBytes(4 + chunkLength);
break;
case RESERVED_UNSKIPPABLE:
// The spec mandates that reserved unskippable chunks must immediately
// return an error, as we must assume that we cannot decode the stream
// correctly
throw new CompressionException("Found reserved unskippable chunk type: 0x" + Integer.toHexString(chunkTypeVal));
case UNCOMPRESSED_DATA:
if (chunkLength > 65536 + 4) {
throw new CompressionException("Received UNCOMPRESSED_DATA larger than 65540 bytes");
}
if (inSize < 4 + chunkLength) {
return;
}
inputBuffer.skipBytes(4);
{
int checksum = ByteBufUtil.swapInt(inputBuffer.readInt());
validateChecksum(checksum, inputBuffer, inputBuffer.readerIndex(), chunkLength - 4);
outputBuffer.writeBytes(inputBuffer, chunkLength - 4);
}
break;
case COMPRESSED_DATA:
if (inSize < 4 + chunkLength) {
return;
}
inputBuffer.skipBytes(4);
{
int checksum = ByteBufUtil.swapInt(inputBuffer.readInt());
int oldWriterIndex = inputBuffer.writerIndex();
int uncompressedStart = outputBuffer.writerIndex();
try {
inputBuffer.writerIndex(inputBuffer.readerIndex() + chunkLength - 4);
snappy.decode(inputBuffer, outputBuffer);
} finally {
inputBuffer.writerIndex(oldWriterIndex);
}
int uncompressedLength = outputBuffer.writerIndex() - uncompressedStart;
validateChecksum(checksum, outputBuffer, uncompressedStart, uncompressedLength);
}
snappy.reset();
break;
}
} catch (Exception e) {
throw new NetException("Unable to decompress SNAPPY data!! " + e.getMessage());
}
}
/**
* Decodes the chunk type from the type tag byte.
*
* @param type The tag byte extracted from the stream
* @return The appropriate {@link ChunkType}, defaulting to {@link ChunkType#RESERVED_UNSKIPPABLE}
*/
static ChunkType mapChunkType(byte type) {
if (type == 0) {
return ChunkType.COMPRESSED_DATA;
} else if (type == 1) {
return ChunkType.UNCOMPRESSED_DATA;
} else if ((type & 0x80) == 0x80) {
return ChunkType.RESERVED_SKIPPABLE;
} else {
return ChunkType.RESERVED_UNSKIPPABLE;
}
}
/**
* Computes the CRC32 checksum of the supplied data, performs the "mask" operation
* on the computed checksum, and then compares the resulting masked checksum to the
* supplied checksum.
*
* @param expectedChecksum The checksum decoded from the stream to compare against
* @param data The input data to calculate the CRC32 checksum of
* @throws CompressionException If the calculated and supplied checksums do not match
*/
static void validateChecksum(int expectedChecksum, ByteBuf data) {
validateChecksum(expectedChecksum, data, data.readerIndex(), data.readableBytes());
}
/**
* Computes the CRC32 checksum of the supplied data, performs the "mask" operation
* on the computed checksum, and then compares the resulting masked checksum to the
* supplied checksum.
*
* @param expectedChecksum The checksum decoded from the stream to compare against
* @param data The input data to calculate the CRC32 checksum of
* @throws CompressionException If the calculated and supplied checksums do not match
*/
static void validateChecksum(int expectedChecksum, ByteBuf data, int offset, int length) {
final int actualChecksum = SnappyAccess.calculateChecksum(data, offset, length);
if (actualChecksum != expectedChecksum) {
throw new CompressionException(
"mismatching checksum: " + Integer.toHexString(actualChecksum) +
" (expected: " + Integer.toHexString(expectedChecksum) + ')');
}
}
private static void writeUnencodedChunk(ByteBuf in, ByteBuf out, int dataLength) {
out.writeByte(1);
writeChunkLength(out, dataLength + 4);
calculateAndWriteChecksum(in, out);
out.writeBytes(in, dataLength);
}
private static void setChunkLength(ByteBuf out, int lengthIdx) {
int chunkLength = out.writerIndex() - lengthIdx - 3;
if (chunkLength >>> 24 != 0) {
throw new CompressionException("compressed data too large: " + chunkLength);
}
out.setMedium(lengthIdx, ByteBufUtil.swapMedium(chunkLength));
}
/**
* Writes the 2-byte chunk length to the output buffer.
*
* @param out The buffer to write to
* @param chunkLength The length to write
*/
private static void writeChunkLength(ByteBuf out, int chunkLength) {
out.writeMedium(ByteBufUtil.swapMedium(chunkLength));
}
/**
* Calculates and writes the 4-byte checksum to the output buffer
*
* @param slice The data to calculate the checksum for
* @param out The output buffer to write the checksum to
*/
private static void calculateAndWriteChecksum(ByteBuf slice, ByteBuf out) {
out.writeInt(ByteBufUtil.swapInt(SnappyAccess.calculateChecksum(slice)));
}
}

View File

@ -0,0 +1,42 @@
package dorkbox.network.util;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
/**
* The default thread factory with names.
*/
public class NamedThreadFactory implements ThreadFactory {
private static final AtomicInteger poolId = new AtomicInteger();
// permit this to be changed!
/**
* Stack size must be specified in bytes. Default is 8k
*/
public static int stackSizeForNettyThreads = 8192;
private final AtomicInteger nextId = new AtomicInteger();
final ThreadGroup group;
final String namePrefix;
public NamedThreadFactory(String poolNamePrefix, ThreadGroup group) {
this.group = group;
namePrefix = poolNamePrefix + '-' + poolId.incrementAndGet();
}
@Override
public Thread newThread(Runnable r) {
// stack size is arbitrary based on JVM implementation. Default is 0
// 8k is the size of the android stack. Depending on the version of android, this can either change, or will always be 8k
// To be honest, 8k is pretty reasonable for an asynchronous/event based system (32bit) or 16k (64bit)
// Setting the size MAY or MAY NOT have any effect!!!
Thread t = new Thread(group, r, namePrefix + '-' + nextId.incrementAndGet(), stackSizeForNettyThreads);
if (!t.isDaemon()) {
t.setDaemon(true);
}
if (t.getPriority() != Thread.MAX_PRIORITY) {
t.setPriority(Thread.MAX_PRIORITY);
}
return t;
}
}

View File

@ -0,0 +1,22 @@
package dorkbox.network.util;
public class NetException extends RuntimeException {
private static final long serialVersionUID = 2963139576811306988L;
public NetException() {
super();
}
public NetException(String message, Throwable cause) {
super(message, cause);
}
public NetException(String message) {
super(message);
}
public NetException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,81 @@
package dorkbox.network.util;
import io.netty.buffer.ByteBuf;
import com.esotericsoftware.kryo.Registration;
import com.esotericsoftware.kryo.Serializer;
import dorkbox.network.connection.Connection;
import dorkbox.network.rmi.RmiRegisterClassesCallback;
import dorkbox.network.rmi.SerializerRegistration;
public class NullSerializationManager implements SerializationManager {
@Override
public boolean setReferences(boolean references) {
return false;
}
@Override
public void setRegistrationRequired(boolean registrationRequired) {
}
@Override
public void register(Class<?> clazz) {
}
@Override
public void register(Class<?> clazz, Serializer<?> serializer) {
}
@Override
@SuppressWarnings("rawtypes")
public void registerSerializer(Class<?> clazz, SerializerRegistration registration) {
}
@Override
public Registration register(Class<?> type, Serializer<?> serializer, int id) {
return null;
}
@Override
public Registration getRegistration(Class<?> clazz) {
return null;
}
@Override
public boolean isEncrypted(ByteBuf buffer) {
return false;
}
@Override
public void write(ByteBuf buffer, Object message) {
}
@Override
public void writeWithCryptoTcp(Connection connection, ByteBuf buffer, Object message) {
}
@Override
public void writeWithCryptoUdp(Connection connection, ByteBuf buffer, Object message) {
}
@Override
public Object read(ByteBuf buffer, int length) {
return null;
}
@Override
public Object readWithCryptoTcp(Connection connection, ByteBuf buffer, int length) {
return null;
}
@Override
public Object readWithCryptoUdp(Connection connection, ByteBuf buffer, int length) {
return null;
}
@Override
public void registerForRmiClasses(RmiRegisterClassesCallback callback) {
}
}

View File

@ -0,0 +1,559 @@
package dorkbox.network.util;
import java.util.Random;
import com.esotericsoftware.kryo.util.ObjectMap;
/** An unordered map where the values are ints. This implementation is a cuckoo hash map using 3 hashes, random walking, and a
* small stash for problematic keys. Null keys are not allowed. No allocation is done except when growing the table size. <br>
* <br>
* This map performs very fast get, containsKey, and remove (typically O(1), worst case O(log(n))). Put may be a bit slower,
* depending on hash collisions. Load factors greater than 0.91 greatly increase the chances the map will have to rehash to the
* next higher POT size.
* @author Nathan Sweet */
public class ObjectIntMap<K> {
@SuppressWarnings("unused")
private static final int PRIME1 = 0xbe1f14b1;
private static final int PRIME2 = 0xb4b82e39;
private static final int PRIME3 = 0xced1c241;
static Random random = new Random();
public int size;
K[] keyTable;
int[] valueTable;
int capacity, stashSize;
private float loadFactor;
private int hashShift, mask, threshold;
private int stashCapacity;
private int pushIterations;
/** Creates a new map with an initial capacity of 32 and a load factor of 0.8. This map will hold 25 items before growing the
* backing table. */
public ObjectIntMap () {
this(32, 0.8f);
}
/** Creates a new map with a load factor of 0.8. This map will hold initialCapacity * 0.8 items before growing the backing
* table. */
public ObjectIntMap (int initialCapacity) {
this(initialCapacity, 0.8f);
}
/** Creates a new map with the specified initial capacity and load factor. This map will hold initialCapacity * loadFactor items
* before growing the backing table. */
@SuppressWarnings("unchecked")
public ObjectIntMap (int initialCapacity, float loadFactor) {
if (initialCapacity < 0) {
throw new IllegalArgumentException("initialCapacity must be >= 0: " + initialCapacity);
}
if (initialCapacity > 1 << 30) {
throw new IllegalArgumentException("initialCapacity is too large: " + initialCapacity);
}
capacity = ObjectMap.nextPowerOfTwo(initialCapacity);
if (loadFactor <= 0) {
throw new IllegalArgumentException("loadFactor must be > 0: " + loadFactor);
}
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
mask = capacity - 1;
hashShift = 31 - Integer.numberOfTrailingZeros(capacity);
stashCapacity = Math.max(3, (int)Math.ceil(Math.log(capacity)) * 2);
pushIterations = Math.max(Math.min(capacity, 8), (int)Math.sqrt(capacity) / 8);
keyTable = (K[])new Object[capacity + stashCapacity];
valueTable = new int[keyTable.length];
}
/** Creates a new map identical to the specified map. */
public ObjectIntMap (ObjectIntMap<? extends K> map) {
this(map.capacity, map.loadFactor);
stashSize = map.stashSize;
System.arraycopy(map.keyTable, 0, keyTable, 0, map.keyTable.length);
System.arraycopy(map.valueTable, 0, valueTable, 0, map.valueTable.length);
size = map.size;
}
public void put (K key, int value) {
if (key == null) {
throw new IllegalArgumentException("key cannot be null.");
}
K[] keyTable = this.keyTable;
// Check for existing keys.
int hashCode = key.hashCode();
int index1 = hashCode & mask;
K key1 = keyTable[index1];
if (key.equals(key1)) {
valueTable[index1] = value;
return;
}
int index2 = hash2(hashCode);
K key2 = keyTable[index2];
if (key.equals(key2)) {
valueTable[index2] = value;
return;
}
int index3 = hash3(hashCode);
K key3 = keyTable[index3];
if (key.equals(key3)) {
valueTable[index3] = value;
return;
}
// Update key in the stash.
for (int i = capacity, n = i + stashSize; i < n; i++) {
if (key.equals(keyTable[i])) {
valueTable[i] = value;
return;
}
}
// Check for empty buckets.
if (key1 == null) {
keyTable[index1] = key;
valueTable[index1] = value;
if (size++ >= threshold) {
resize(capacity << 1);
}
return;
}
if (key2 == null) {
keyTable[index2] = key;
valueTable[index2] = value;
if (size++ >= threshold) {
resize(capacity << 1);
}
return;
}
if (key3 == null) {
keyTable[index3] = key;
valueTable[index3] = value;
if (size++ >= threshold) {
resize(capacity << 1);
}
return;
}
push(key, value, index1, key1, index2, key2, index3, key3);
}
/** Skips checks for existing keys. */
private void putResize (K key, int value) {
// Check for empty buckets.
int hashCode = key.hashCode();
int index1 = hashCode & mask;
K key1 = keyTable[index1];
if (key1 == null) {
keyTable[index1] = key;
valueTable[index1] = value;
if (size++ >= threshold) {
resize(capacity << 1);
}
return;
}
int index2 = hash2(hashCode);
K key2 = keyTable[index2];
if (key2 == null) {
keyTable[index2] = key;
valueTable[index2] = value;
if (size++ >= threshold) {
resize(capacity << 1);
}
return;
}
int index3 = hash3(hashCode);
K key3 = keyTable[index3];
if (key3 == null) {
keyTable[index3] = key;
valueTable[index3] = value;
if (size++ >= threshold) {
resize(capacity << 1);
}
return;
}
push(key, value, index1, key1, index2, key2, index3, key3);
}
private void push (K insertKey, int insertValue, int index1, K key1, int index2, K key2, int index3, K key3) {
K[] keyTable = this.keyTable;
int[] valueTable = this.valueTable;
int mask = this.mask;
// Push keys until an empty bucket is found.
K evictedKey;
int evictedValue;
int i = 0, pushIterations = this.pushIterations;
do {
// Replace the key and value for one of the hashes.
switch (random.nextInt(3)) {
case 0:
evictedKey = key1;
evictedValue = valueTable[index1];
keyTable[index1] = insertKey;
valueTable[index1] = insertValue;
break;
case 1:
evictedKey = key2;
evictedValue = valueTable[index2];
keyTable[index2] = insertKey;
valueTable[index2] = insertValue;
break;
default:
evictedKey = key3;
evictedValue = valueTable[index3];
keyTable[index3] = insertKey;
valueTable[index3] = insertValue;
break;
}
// If the evicted key hashes to an empty bucket, put it there and stop.
int hashCode = evictedKey.hashCode();
index1 = hashCode & mask;
key1 = keyTable[index1];
if (key1 == null) {
keyTable[index1] = evictedKey;
valueTable[index1] = evictedValue;
if (size++ >= threshold) {
resize(capacity << 1);
}
return;
}
index2 = hash2(hashCode);
key2 = keyTable[index2];
if (key2 == null) {
keyTable[index2] = evictedKey;
valueTable[index2] = evictedValue;
if (size++ >= threshold) {
resize(capacity << 1);
}
return;
}
index3 = hash3(hashCode);
key3 = keyTable[index3];
if (key3 == null) {
keyTable[index3] = evictedKey;
valueTable[index3] = evictedValue;
if (size++ >= threshold) {
resize(capacity << 1);
}
return;
}
if (++i == pushIterations) {
break;
}
insertKey = evictedKey;
insertValue = evictedValue;
} while (true);
putStash(evictedKey, evictedValue);
}
private void putStash (K key, int value) {
if (stashSize == stashCapacity) {
// Too many pushes occurred and the stash is full, increase the table size.
resize(capacity << 1);
put(key, value);
return;
}
// Store key in the stash.
int index = capacity + stashSize;
keyTable[index] = key;
valueTable[index] = value;
stashSize++;
size++;
}
/** @param defaultValue Returned if the key was not associated with a value. */
public int get (K key, int defaultValue) {
int hashCode = key.hashCode();
int index = hashCode & mask;
if (!key.equals(keyTable[index])) {
index = hash2(hashCode);
if (!key.equals(keyTable[index])) {
index = hash3(hashCode);
if (!key.equals(keyTable[index])) {
return getStash(key, defaultValue);
}
}
}
return valueTable[index];
}
private int getStash (K key, int defaultValue) {
K[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++) {
if (key.equals(keyTable[i])) {
return valueTable[i];
}
}
return defaultValue;
}
/** Returns the key's current value and increments the stored value. If the key is not in the map, defaultValue + increment is
* put into the map. */
public int getAndIncrement (K key, int defaultValue, int increment) {
int hashCode = key.hashCode();
int index = hashCode & mask;
if (!key.equals(keyTable[index])) {
index = hash2(hashCode);
if (!key.equals(keyTable[index])) {
index = hash3(hashCode);
if (!key.equals(keyTable[index])) {
return getAndIncrementStash(key, defaultValue, increment);
}
}
}
int value = valueTable[index];
valueTable[index] = value + increment;
return value;
}
private int getAndIncrementStash (K key, int defaultValue, int increment) {
K[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++) {
if (key.equals(keyTable[i])) {
int value = valueTable[i];
valueTable[i] = value + increment;
return value;
}
}
put(key, defaultValue + increment);
return defaultValue;
}
public int remove (K key, int defaultValue) {
int hashCode = key.hashCode();
int index = hashCode & mask;
if (key.equals(keyTable[index])) {
keyTable[index] = null;
int oldValue = valueTable[index];
size--;
return oldValue;
}
index = hash2(hashCode);
if (key.equals(keyTable[index])) {
keyTable[index] = null;
int oldValue = valueTable[index];
size--;
return oldValue;
}
index = hash3(hashCode);
if (key.equals(keyTable[index])) {
keyTable[index] = null;
int oldValue = valueTable[index];
size--;
return oldValue;
}
return removeStash(key, defaultValue);
}
int removeStash (K key, int defaultValue) {
K[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++) {
if (key.equals(keyTable[i])) {
int oldValue = valueTable[i];
removeStashIndex(i);
size--;
return oldValue;
}
}
return defaultValue;
}
void removeStashIndex (int index) {
// If the removed location was not last, move the last tuple to the removed location.
stashSize--;
int lastIndex = capacity + stashSize;
if (index < lastIndex) {
keyTable[index] = keyTable[lastIndex];
valueTable[index] = valueTable[lastIndex];
}
}
/** Reduces the size of the backing arrays to be the specified capacity or less. If the capacity is already less, nothing is
* done. If the map contains more items than the specified capacity, the next highest power of two capacity is used instead. */
public void shrink (int maximumCapacity) {
if (maximumCapacity < 0) {
throw new IllegalArgumentException("maximumCapacity must be >= 0: " + maximumCapacity);
}
if (size > maximumCapacity) {
maximumCapacity = size;
}
if (capacity <= maximumCapacity) {
return;
}
maximumCapacity = ObjectMap.nextPowerOfTwo(maximumCapacity);
resize(maximumCapacity);
}
/** Clears the map and reduces the size of the backing arrays to be the specified capacity if they are larger. */
public void clear (int maximumCapacity) {
if (capacity <= maximumCapacity) {
clear();
return;
}
size = 0;
resize(maximumCapacity);
}
public void clear () {
K[] keyTable = this.keyTable;
for (int i = capacity + stashSize; i-- > 0;) {
keyTable[i] = null;
}
size = 0;
stashSize = 0;
}
/** Returns true if the specified value is in the map. Note this traverses the entire map and compares every value, which may be
* an expensive operation. */
public boolean containsValue (int value) {
int[] valueTable = this.valueTable;
for (int i = capacity + stashSize; i-- > 0;) {
if (valueTable[i] == value) {
return true;
}
}
return false;
}
public boolean containsKey (K key) {
int hashCode = key.hashCode();
int index = hashCode & mask;
if (!key.equals(keyTable[index])) {
index = hash2(hashCode);
if (!key.equals(keyTable[index])) {
index = hash3(hashCode);
if (!key.equals(keyTable[index])) {
return containsKeyStash(key);
}
}
}
return true;
}
private boolean containsKeyStash (K key) {
K[] keyTable = this.keyTable;
for (int i = capacity, n = i + stashSize; i < n; i++) {
if (key.equals(keyTable[i])) {
return true;
}
}
return false;
}
/** Returns the key for the specified value, or null if it is not in the map. Note this traverses the entire map and compares
* every value, which may be an expensive operation. */
public K findKey (int value) {
int[] valueTable = this.valueTable;
for (int i = capacity + stashSize; i-- > 0;) {
if (valueTable[i] == value) {
return keyTable[i];
}
}
return null;
}
/** Increases the size of the backing array to acommodate the specified number of additional items. Useful before adding many
* items to avoid multiple backing array resizes. */
public void ensureCapacity (int additionalCapacity) {
int sizeNeeded = size + additionalCapacity;
if (sizeNeeded >= threshold) {
resize(ObjectMap.nextPowerOfTwo((int)(sizeNeeded / loadFactor)));
}
}
@SuppressWarnings("unchecked")
private void resize (int newSize) {
int oldEndIndex = capacity + stashSize;
capacity = newSize;
threshold = (int)(newSize * loadFactor);
mask = newSize - 1;
hashShift = 31 - Integer.numberOfTrailingZeros(newSize);
stashCapacity = Math.max(3, (int)Math.ceil(Math.log(newSize)) * 2);
pushIterations = Math.max(Math.min(newSize, 8), (int)Math.sqrt(newSize) / 8);
K[] oldKeyTable = keyTable;
int[] oldValueTable = valueTable;
keyTable = (K[])new Object[newSize + stashCapacity];
valueTable = new int[newSize + stashCapacity];
int oldSize = size;
size = 0;
stashSize = 0;
if (oldSize > 0) {
for (int i = 0; i < oldEndIndex; i++) {
K key = oldKeyTable[i];
if (key != null) {
putResize(key, oldValueTable[i]);
}
}
}
}
private int hash2 (int h) {
h *= PRIME2;
return (h ^ h >>> hashShift) & mask;
}
private int hash3 (int h) {
h *= PRIME3;
return (h ^ h >>> hashShift) & mask;
}
@Override
public String toString () {
if (size == 0) {
return "{}";
}
StringBuilder buffer = new StringBuilder(32);
buffer.append('{');
K[] keyTable = this.keyTable;
int[] valueTable = this.valueTable;
int i = keyTable.length;
while (i-- > 0) {
K key = keyTable[i];
if (key == null) {
continue;
}
buffer.append(key);
buffer.append('=');
buffer.append(valueTable[i]);
break;
}
while (i-- > 0) {
K key = keyTable[i];
if (key == null) {
continue;
}
buffer.append(", ");
buffer.append(key);
buffer.append('=');
buffer.append(valueTable[i]);
}
buffer.append('}');
return buffer.toString();
}
}

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