Added the network project
This commit is contained in:
commit
6df9f268ef
|
@ -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>
|
|
@ -0,0 +1,4 @@
|
|||
/classes/
|
||||
*.crt
|
||||
*.ini
|
||||
*.dat
|
|
@ -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>
|
|
@ -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.
|
||||
|
||||
|
||||
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 + "]";
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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() + "]";
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 ();
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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.");
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 " <== ";
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 " ==> ";
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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 " <== ";
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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!");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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!)!");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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!");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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!");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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 ();
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package dorkbox.network.rmi;
|
||||
|
||||
import com.esotericsoftware.kryo.Kryo;
|
||||
|
||||
public interface RmiRegisterClassesCallback {
|
||||
public void registerForClasses(Kryo kryo);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package dorkbox.network.rmi;
|
||||
|
||||
import com.esotericsoftware.kryo.Serializer;
|
||||
|
||||
public interface SerializerRegistration<T extends Serializer<?>> {
|
||||
public void register(T serializer);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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 "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
package dorkbox.network.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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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)));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
}
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue